Skip to content

Latest commit

 

History

History
1002 lines (681 loc) · 54.7 KB

File metadata and controls

1002 lines (681 loc) · 54.7 KB

四、使用集合

Python 提供了许多进程整个集合的函数。它们可以应用于序列(列表或元组)、集合、映射和生成器表达式的可编辑结果。我们将从函数编程的角度来看 Python 的集合处理特性。

我们将从查看 iterables 和一些使用 iterables 的简单函数开始。我们将研究一些设计模式,用递归函数以及显式的for循环来处理可重用项和序列。我们将研究如何使用生成器表达式将标量函数应用于数据集合。

在本章中,我们将向您展示如何对集合使用以下函数的示例:

  • any()all()
  • len()sum(),以及一些与这些函数相关的高阶统计处理
  • zip()和一些相关技术来构造和展平数据列表
  • reversed()
  • enumerate()

前四个函数可以称为缩减:它们将集合缩减为单个值。其他三个函数zip()reversed()enumerate()是映射;它们从现有集合生成新集合。在下一章中,我们将介绍更多的映射和归约函数,它们使用附加函数作为参数来自定义处理。

在本章中,我们将首先研究使用生成器表达式处理数据的方法。然后,我们将应用不同种类的集合级函数来展示它们如何简化迭代处理的语法。我们还将介绍一些不同的数据重组方法。

在下一章中,我们将重点介绍如何使用高阶集合函数进行类似的处理。

函数变体概述

我们需要区分两大类功能,如下所示:

  • 标量函数:它们应用于单个值并计算单个结果。诸如abs()pow()和整个math模块等函数都是标量函数的示例。
  • 采集功能:用于可编辑的采集。

我们可以将收集函数进一步细分为三个子种:

  • 归约:使用一个函数将集合中的值折叠在一起,得到一个最终值。例如,如果我们将(+操作折叠成一个整数序列,这将计算总和。这也可以称为聚合函数,因为它为输入集合生成单个聚合值。
  • 映射:将标量函数应用于集合的每个单独项;结果是一个大小相同的集合。
  • 过滤器:将标量函数应用于集合的所有项,以拒绝某些项并传递其他项。结果是输入的一个子集。

我们将使用这个概念框架来描述使用内置集合函数的方式。

与 iterables 合作

如前几章所述,Python 的for循环可用于集合。在处理诸如元组、列表、映射和集合等物化集合时,for循环涉及状态的显式管理。虽然这偏离了纯函数式编程,但它反映了 Python 的必要优化。如果我们假设状态管理被本地化为一个迭代器对象,该迭代器对象是作为for语句求值的一部分创建的,那么我们就可以利用这个特性,而不会偏离纯粹的函数式编程。例如,如果我们在loop的缩进体之外使用for循环变量,我们就偏离了纯粹的函数式编程,利用了这个状态控制变量。

我们将在第 6 章递归和归约中回到这一点。这是一个重要的主题,我们将在这里用一个使用生成器的快速示例来简单介绍一下。

for循环可移植处理的一个常见应用是unwrap(process(wrap(iterable)))设计模式。wrap()函数将首先将 iterable 的每个项转换为两个元组,其中包含派生的排序键和原始项。然后,我们可以将这两个元组项作为单个包装值进行处理。最后,我们将使用一个unwrap()函数来丢弃用于包装的值,这将恢复原始项。

这种情况经常发生在函数上下文中,因此有两个函数被大量使用;它们是:

fst = lambda x: x[0] 
snd = lambda x: x[1] 

这两个函数从两个元组中选择第一个和第二个值,这两个函数对于process()unwrap()函数都很方便。

另一个常见模式是wrap3(wrap2(wrap1()))。在本例中,我们从简单元组开始,然后用其他结果包装它们,以构建更大、更复杂的元组。这个主题的一个常见变体是从源对象构建新的、更复杂的namedtuple实例。我们可以将这两种模式总结为吸积设计模式——一种吸积衍生值的项目。

作为一个例子,考虑使用吸积模式与一个简单的纬度和经度值序列一起工作。第一步将路径上表示为(latlon)对的简单点转换为成对的支腿(beginend)。结果中的每一对将表示为((latlon)、(latlon)。fst(item)的值为起始位置;snd(item)的值是集合中每个项目的每个值的结束位置。

在接下来的部分中,我们将向您展示如何创建一个生成器函数,该函数将迭代文件的内容。此 iterable 将包含我们将处理的原始输入数据。

一旦我们有了原始数据,后面的部分将展示如何用沿着腿的haversine距离来装饰每条腿。wrap(wrap(iterable()))设计的最终结果将是三个元组的序列——(latlon、(latlondistance)。然后,我们可以分析最长和最短距离、边界矩形和其他摘要的结果。

解析 XML 文件

我们将首先解析一个可扩展标记语言XML文件)来获得原始的纬度和经度对。这将向您展示如何封装 Python 的一些不太实用的特性,以创建一个可移植的值序列。

我们将使用xml.etree模块。解析后,生成的ElementTree对象有一个findall()方法,该方法将遍历可用值。

我们将寻找构造,例如以下 XML 示例:

<Placemark><Point> 
<coordinates>-76.33029518659048,
    37.54901619777347,0</coordinates> 
</Point></Placemark> 

文件将有许多<Placemark>标记,每个标记内都有一个点和坐标结构。这是典型的包含地理信息的锁孔标记语言KML文件)。

解析 XML 文件可以在两个抽象级别上进行。在较低级别,我们需要定位 XML 文件中的各种标记、属性值和内容。在更高的层次上,我们希望从文本和属性值中生成有用的对象。

可通过以下方式实现较低级别的处理:

import xml.etree.ElementTree as XML
from typing import Text, List, TextIO, Iterable
def row_iter_kml(file_obj: TextIO) -> Iterable[List[Text]]:
    ns_map= {
        "ns0": "http://www.opengis.net/kml/2.2",
        "ns1": "http://www.google.com/kml/ext/2.2"}
    path_to_points= ("./ns0:Document/ns0:Folder/ns0:Placemark/"
             "ns0:Point/ns0:coordinates")
    doc= XML.parse(file_obj)
    return (comma_split(Text(coordinates.text))
            for coordinates in 
            doc.findall(path_to_points, ns_map))

此函数需要通过with语句打开的文件中的文本。结果是一个生成器,它从纬度/经度对创建列表对象。作为 XML 处理的一部分,此函数包括一个简单的静态dict对象ns_map,它为我们将要搜索的 XML 标记提供namespace映射信息。本词典将通过ElementTree.findall()方法使用。

解析的本质是一个生成器函数,它使用doc.findall()定位的标记序列。然后,comma_split()函数处理这个标记序列,将文本值梳理成逗号分隔的组件。

comma_split()函数是字符串split()方法的函数版本,如下所示:

def comma_split(text: Text) -> List[Text]:
    return text.split(",")

我们使用了函数包装器来强调稍微统一的语法。我们还添加了显式类型提示,以明确文本将转换为文本值列表。在没有类型提示的情况下,split()有两种可能的定义。本方法适用于bytesstr。我们使用了Text类型名称,它是 Python 3 中str的别名。

row_iter_kml()函数的结果是一系列数据行。每一行将是一个由三个字符串组成的列表-latitudelongitudealtitude沿该路径的一个路径点。这还没有直接的用处。我们需要做更多的处理来获得latitudelongitude,并将这两个数字转换成有用的浮点值。

这种元组(或列表)的 iterable 序列的思想允许我们以简单统一的方式处理某些类型的数据文件。在第 3 章函数、迭代器和生成器中,我们了解了逗号分隔值CSV文件如何轻松处理为元组行。在第 6 章递归和归约中,我们将重新讨论解析思想,以比较这些不同的示例。

前面函数的输出类似于以下示例:

[['-76.33029518659048', '37.54901619777347', '0'], 
 ['-76.27383399999999', '37.840832', '0'], 
 ['-76.459503', '38.331501', '0'], 
 *etc.* 
 ['-76.47350299999999', '38.976334', '0']]

每一行都是使用(,)拆分的<ns0:coordinates>标记的源文本,该标记是文本内容的一部分。这些值是东西经度、南北纬度和海拔高度。我们将对该函数的输出应用一些附加函数,以创建该数据的可用子集。

在更高级别解析文件

一旦我们解析了将 XML 转换为 Python 的低级语法,我们就可以将原始数据重新构造为 Python 程序中可用的数据。这种结构适用于 XML、JavaScript 对象表示法JSON)、CSV 以及数据序列化的各种物理格式。

我们的目标是编写一小套生成器函数,将解析后的数据转换为应用程序可以使用的形式。生成器函数包括由row_iter_kml()函数找到的文本的一些简单转换,如下所示:

  • 丢弃altitude也可以表述为仅保留latitudelongitude
  • 将订单从longitudelatitude更改为latitudelongitude

通过定义效用函数,我们可以使这两种转换具有更大的语法一致性,如下所示:

def pick_lat_lon(
  lon: Text, lat: Text, alt: Text) -> Tuple[Text, Text]:
    return lat, lon

我们创建了一个函数来获取三个参数值,并从其中两个参数值创建了一个元组。类型提示比函数本身更复杂。

我们可以按如下方式使用此功能:

from typing import Text, List, Iterable

Rows = Iterable[List[Text]]
LL_Text = Tuple[Text, Text]
def lat_lon_kml(row_iter: Rows) -> Iterable[LL_Text]:
    return (pick_lat_lon(*row) for row in row_iter)

此函数将pick_lat_lon()函数应用于源迭代器中的每一行。我们使用*row将第三行元组的每个元素分配给pick_lat_lon()函数的单独参数。然后,该函数可以从每个三元组中提取并重新排序两个相关值。

为了简化函数定义,我们定义了两个类型别名:RowsLL_Text。这些类型别名可以简化函数定义。还可以重用它们,以确保多个相关函数都使用相同类型的对象。

这种功能设计允许我们用它的等价物自由地替换任何函数,这使得重构非常简单。当我们提供各种函数的替代实现时,我们试图实现这一目标。原则上,一个聪明的函数式语言编译器可以做一些替换,作为优化过程的一部分。

这些函数可以组合起来解析文件并构建我们可以使用的结构。下面是一些可用于此目的的代码的示例:

url = "file:./Winter%202012-2013.kml"
with urllib.request.urlopen(url) as source:
    v1= tuple(lat_lon_kml(row_iter_kml(source)))
print(v1)

此脚本使用urllib命令打开源代码。在本例中,它是一个本地文件。但是,我们也可以在远程服务器上打开 KML 文件。我们使用这种文件打开的目的是确保无论数据源是什么,我们的处理都是统一的。

该脚本是围绕对 KML 源代码进行低级解析的两个函数构建的。row_iter_kml(source)表达式生成一系列文本列。lat_lon_kml()函数将提取latitudelongitude值并重新排序。这将创建一个中间结果,为进一步处理奠定基础。后续处理独立于原始格式。

运行此操作时,我们会看到如下结果:

(('37.54901619777347', '-76.33029518659048'), 
 ('37.840832', '-76.27383399999999'), 
 ('38.331501', '-76.459503'), 
 ('38.330166', '-76.458504'), 
 ('38.976334', '-76.47350299999999'))

我们使用一种几乎完全是函数式的方法从复杂的 XML 文件中提取了latitudelongitude值。由于结果是可移植的,我们可以继续使用函数编程技术来处理从文件中检索到的每个点。

我们明确地将低级 XML 解析和高级数据重组分开。XML 解析产生了字符串结构的通用元组。这与 CSV 解析器的输出兼容。在使用 SQL 数据库时,我们将使用类似的元组结构。这使我们能够为更高级别的处理编写代码,可以处理来自各种来源的数据。

我们将向您展示一系列转换,以将这些数据从字符串集合重新排列为路线上的航路点集合。这将涉及许多转换。我们需要重新构造数据,并将strings值转换为floating-point值。我们还将研究一些简化和澄清后续处理步骤的方法。我们将在后面的章节中使用这个数据集,因为它非常复杂。

将序列中的项目配对

一个常见的重组要求是在一个序列中从点中创建开始-停止对。给定一个序列,我们还需要创建一个成对序列。第一项和第二项形成一对。第二项和第三项构成下一对。在进行时间序列分析时,我们可能会结合更广泛分离的值。在本例中,这些对是紧邻的值。

成对序列将允许我们使用每一对,通过一个简单的haversine函数应用,计算点到点的距离。该技术还用于在图形应用程序中将点的路径转换为一系列线段。

为什么要将项目配对?为什么不这样做:

begin= next(iterable)
for end in iterable:
    compute_something(begin, end)
    begin = end  

显然,这将把数据的每一段作为开始-结束对进行处理。然而,处理函数和重构数据的循环是紧密绑定的,这使得重用变得比必要的更复杂。配对算法很难单独测试,因为它绑定到compute_something()函数。

这种组合功能也限制了我们重新配置应用程序的能力。注入compute_something()函数的替代实现并非易事。此外,我们还有一个显式的状态,begin变量,这使得生命可能变得复杂。如果我们试图在loop的主体中添加特征,如果忽略一个点,我们很容易无法正确设置begin变量。filter()函数引入一个if语句,该语句可能导致更新begin变量时出错。

我们通过分离这个简单的配对函数来实现更好的重用。从长远来看,这是我们的目标之一。如果我们建立一个有用的原语库,比如这个配对函数,我们可以更快更自信地解决问题。

有许多方法可以将路线上的点配对,为每条航段创建起点和终点信息。我们将在这里看一些,然后在第 5 章高阶函数第 7 章Itertools 模块中再次讨论这一点。可以使用递归以纯函数的方式创建对。

以下代码是用于沿路线配对点的函数的一个版本:

from typing import Iterator, Any
Item_Iter = Iterator[Any]
Pairs_Iter = Iterator[Tuple[float, float]]
def pairs(iterator: Item_Iter) -> Pairs_Iter:
    def pair_from(
            head: Any, 
            iterable_tail: Item_Iter) -> Pairs_Iter:
        nxt= next(iterable_tail)
        yield head, nxt
        yield from pair_from(nxt, iterable_tail)

    try:
        return pair_from(next(iterator), iterator)
    except StopIteration:
         return iter([])

基本工作由内部pair_from()功能完成。这适用于迭代器头部的项加上迭代器对象本身。它生成第一对,从 iterable 中弹出下一项,然后递归调用自身以生成任何其他对。

类型提示说明参数iterator必须为Item_Iter类型。结果是Pairs_Iter类型,是两个元组的迭代器,其中每个项都是float类型。这些是mypy程序用来检查代码是否正常工作的提示。类型提示声明包含在typing模块中。

输入必须是响应next()函数的迭代器。要使用集合对象,必须显式使用iter()函数从集合创建迭代器。

我们已经从pairs()函数调用了pair_from()函数。pairs()函数通过从迭代器参数获取初始项来确保初始化得到正确处理。在罕见的空迭代器情况下,对next()的初始调用将引发StopIteration异常;这种情况将创建一个空的 iterable。

Python's iterable recursion involves a for loop to properly consume and yield the results from the recursion. If we try to use a simpler-looking return pair_from(nxt, iterable_tail) statement, we'll see that it does not properly consume the iterable and yield all of the values. Recursion in a generator function requires yield from a statement to consume the resulting iterable. For this, use yield from recursive_iter(args). Something like return recursive_iter(args) will return only a generator object; it doesn't evaluate the function to return the generated values.

我们执行尾部调用优化的策略是用生成器表达式替换递归。我们可以清楚地将这个递归优化为一个简单的for循环。以下代码是另一个版本的函数,用于沿路线将点配对:

from typing import Iterator, Any, Iterable, TypeVar
T_ = TypeVar('T_')
Pairs_Iter = Iterator[Tuple[T_, T_]]
def legs(lat_lon_iter: Iterator[T_]) -> Pairs_Iter:
    begin = next(lat_lon_iter)
    for end in lat_lon_iter:
        yield begin, end
        begin = end  

这个版本非常快,没有堆栈限制。它独立于任何特定类型的序列,因为它将配对序列生成器发出的任何东西。由于循环中没有处理函数,我们可以根据需要重用legs()函数。

使用TypeVar函数创建的类型变量T_用于精确说明legs()函数如何重构数据。提示表示输入类型在输出时保留。输入类型为某个任意类型的Iterator``T_;输出将包括相同类型的元组T_。该函数不隐含任何其他转换。

beginend变量保持计算状态。有状态变量的使用不符合在函数编程中使用不可变对象的理想。优化很重要。它对函数的用户也是不可见的,这使它成为一个 Pythonic 函数的混合体。

我们可以将此函数视为产生以下类型的成对序列的函数:

list[0:1], list[1:2], list[2:3], ..., list[-2:]

此功能的另一个视图如下所示:

zip(list, list[1:])

虽然信息丰富,但此基于 zip 的版本仅适用于序列对象。legs()pairs()函数适用于任何 iterable,包括序列对象。

显式使用 iter()函数

纯函数的观点是,我们所有的 ITerable 都可以用递归函数处理,其中状态仅仅是递归调用堆栈。实际上,Python 可重用性通常会涉及对其他for循环的评估。有两种常见情况:集合对象和 iterables。使用集合对象时,迭代器对象由for语句创建。使用生成器函数时,生成器函数是迭代器,并保持其自身的内部状态。通常,从 Python 编程的角度来看,它们是等价的。在极少数情况下,通常情况下,我们必须使用显式的next()函数 2 的情况并不完全相同。

前面显示的legs()函数有一个明确的next()求值,以从 iterable 中获取第一个值。这对于生成器函数、表达式和其他可重用项非常有效。它不适用于序列对象,如元组或lists

以下代码包含三个示例,以澄清next()iter()函数的使用:

>>> list(legs(x for x in range(3)))
[(0, 1), (1, 2)]
>>> list(legs([0,1,2]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in legs
TypeError: 'list' object is not an iterator
>>> list(legs(iter([0,1,2])))
[(0, 1), (1, 2)]  

在第一种情况下,我们将legs()函数应用于 iterable。在本例中,iterable 是一个生成器表达式。这是基于本章前面示例的预期行为。项目正确配对,从三个航路点创建两条航段。

在第二种情况下,我们尝试将legs()函数应用于序列。这导致了一个错误。虽然list对象和 iterable 在for语句中使用时是等价的,但它们并不是处处等价的。序列不是迭代器;它没有实现next()功能。然而,for语句通过自动从序列创建迭代器,优雅地处理了这个问题。

为了使第二种情况起作用,我们需要从list对象显式创建迭代器。这允许legs()函数通过list项从迭代器获取第一项。iter()函数将从列表中创建迭代器。

扩展简单循环

我们有两种扩展,我们可以将它们分解成一个简单的循环。我们先来看一个filter分机。在这种情况下,我们可能拒绝进一步考虑这些值。它们可能是数据异常值,也可能是格式不正确的源数据。然后,我们将通过执行一个简单的转换从原始对象创建新对象来查看映射源数据。在本例中,我们将把字符串转换为浮点数。然而,使用映射扩展简单的for语句的想法适用于许多情况。我们将研究重构上述pairs()函数。如果我们需要调整点的顺序以丢弃一个值,该怎么办?这将引入一个拒绝某些数据值的filter扩展。

我们正在设计的循环只返回对,而不执行任何其他与应用程序相关的处理—复杂性最低。简单意味着我们不太可能混淆处理状态。

向该设计添加filter扩展可能类似于以下代码段:

from typing import Iterator, Any, Iterable
Pairs_Iter = Iterator[Tuple[float, float]]
LL_Iter = Iterable[
    Tuple[Tuple[float, float], Tuple[float, float]]]
def legs_filter(lat_lon_iter: Pairs_Iter) -> LL_Iter:
    begin = next(lat_lon_iter)
    for end in lat_lon_iter:
        if #some rule for rejecting:
            continue
        yield begin, end
        begin = end  

我们插入了一个处理规则来拒绝某些值。由于loop仍然简洁且富有表现力,我们相信处理过程会正确完成。此外,我们可以很容易地为此函数编写测试,因为无论对的长期目标是什么,测试结果都适用于任何 iterable。

我们还没有提供太多关于#some rule for rejecting代码的信息。这是一种使用beginend或两个变量来拒绝进一步考虑该点的条件。例如,它可能会拒绝begin == end以避免零长度支腿。

下一次重构将引入到循环的额外映射。在设计不断发展时,添加映射是很常见的。在我们的例子中,我们有一系列的string值。我们需要将这些值转换为float值,以便以后使用。这是一个显示设计模式的相对简单的映射。

以下是通过包装生成器函数的生成器表达式处理此数据映射的一种方法:

trip = list(
    legs(
        (float(lat), float(lon)) 
        for lat,lon in lat_lon_kml(row_iter_kml(source))
    )
)

我们已经将legs()函数应用于生成器表达式,该表达式通过lat_lon_kml()函数的输出创建float值。我们也可以用相反的顺序来阅读。lat_lon_kml()函数的输出被转换成一对float值,然后被转换成legs序列。

这开始变得复杂起来。我们这里有大量的嵌套函数。我们将float()legs()list()应用于数据生成器。重构复杂表达式的一种常见方法是将生成器表达式与任何具体化的集合分离。我们可以执行以下操作来简化表达式:

ll_iter = (
    (float(lat), float(lon)) 
    for lat,lon in lat_lon_kml(row_iter_kml(source))
)
print(tuple(legs(ll_iter)))

我们已将生成器函数分配给名为ll_iter的变量。此变量不是集合对象;这是一个项目的生成器。我们不是用list理解来创建对象。我们只是将生成器表达式指定给一个变量名。然后我们在另一个表达式中使用了flt变量。

tuple()方法的评估实际上会导致建立一个适当的对象,以便我们可以打印输出。flt变量的对象仅在需要时创建。

我们可能还想做其他重构。一般来说,数据的来源是我们经常想要改变的。在我们的示例中,lat_lon_kml()函数紧密绑定在表达式的其余部分。当我们有不同的数据源时,这使得重用变得困难。

如果float()操作是我们想要参数化的,以便我们可以重用它,那么我们可以围绕生成器表达式定义一个函数。我们将把一些处理提取到一个单独的函数中,只是为了对操作进行分组。在我们的例子中,字符串对到浮点对对于特定的源数据是唯一的。我们可以将字符串表达式中的复杂浮点重写为更简单的函数,例如:

from typing import Iterator, Tuple, Text, Iterable
Text_Iter = Iterable[Tuple[Text, Text]]
LL_Iter = Iterable[Tuple[float, float]]
def float_from_pair(lat_lon_iter: Text_Iter) -> LL_Iter:
    return (
        (float(lat), float(lon)) 
        for lat,lon in lat_lon_iter
    )

float_from_pair()函数将float()函数应用于 iterable 中每个项的第一个和第二个值,从而产生一个由输入值创建的两个浮点元组。我们依赖 Python 的for语句来分解两个元组。

类型提示坚持输入与Text_Iter类型别名匹配,它必须是Text值对的可匹配源。结果使用了LL_Iter类型别名。这必须是一对float值的 iterable。LL_Iter类型别名可在一组复杂的函数定义中的其他地方使用。

我们可以在以下上下文中使用此函数:

legs(
    float_from_pair(
        lat_lon_kml(
            row_iter_kml(source))))  

我们将创建由来自 KML 文件的float值构建的legs。可视化处理相当容易,因为过程中的每个阶段都是一个简单的前缀函数。每个函数的输入是嵌套处理步骤中下一个函数的输出。

在解析时,我们通常有string值的序列。对于数字应用程序,我们需要将strings转换为floatintDecimal值。这通常涉及将一个函数(如float_from_pair()函数)插入到一个表达式序列中,以清理源数据。

我们以前的输出都是字符串;它看起来像以下代码段:

(('37.54901619777347', '-76.33029518659048'), 
 ('37.840832', '-76.27383399999999'), 
 ... 
 ('38.976334', '-76.47350299999999'))  

我们需要如下代码段中的数据,其中有浮点数:

(((37.54901619777347, -76.33029518659048), 
 (37.840832, -76.273834)), ((37.840832, -76.273834), 
 ... 
 ((38.330166, -76.458504), (38.976334, -76.473503)))

我们需要创建一个更简单的转换函数管道。在这里,我们到达了flt= ((float(lat), float(lon)) for lat,lon in lat_lon_kml(...))。我们可以利用函数的替换规则,用具有相同值的函数(在本例中为float_from_pair(lat_lon_kml(...))替换复杂表达式,如(float(lat), float(lon)) for lat,lon in lat_lon_kml(...))。这种重构使我们能够确保简化与更复杂的表达式具有相同的效果。

我们将在第 5 章高阶函数中看到一些简化。我们将在第 6 章递归和归约中重新讨论这一点,以了解如何将这些简化应用于文件解析问题。

将生成器表达式应用于标量函数

我们将研究一种更复杂的生成器表达式,用于将数据值从一种数据映射到另一种数据。在本例中,我们将对生成器创建的单个数据值应用一个相当复杂的函数。

我们将这些非生成器函数称为标量,因为它们处理简单的原子值。要处理数据集合,将在生成器表达式中嵌入标量函数。

为了继续前面开始的示例,我们将提供一个haversine函数,然后使用生成器表达式将标量haversine()函数应用于 KML 文件中的成对序列。

haversine()函数的代码如下:

from math import radians, sin, cos, sqrt, asin
from typing import Tuple

MI= 3959
NM= 3440
KM= 6371

Point = Tuple[float, float]
def haversine(p1: Point, p2: Point, R: float=NM) -> float:
    lat_1, lon_1= p1
    lat_2, lon_2= p2
    Δ_lat = radians(lat_2 - lat_1)
    Δ_lon = radians(lon_2 - lon_1)
    lat_1 = radians(lat_1)
    lat_2 = radians(lat_2)
    a = sqrt(
        sin(Δ_lat/2)**2 +
        cos(lat_1)*cos(lat_2)*sin(Δ_lon/2)**2
    )
    c = 2*asin(a)
    return R * c  

这是一个从万维网复制的相对简单的实现。起点和终点都有类型提示。返回值还提供了一个提示。Point = Tuple[float, float]的明确使用使得mypy工具能够确认该功能使用正确。

下面的代码是我们如何使用函数集合来检查一些 KML 数据并生成一系列距离:

trip= (
    (start, end, round(haversine(start, end),4))
    for start,end in
        legs(float_from_pair(lat_lon_kml()))
)

for start, end, dist in trip:
    print(start, end, dist)

处理的本质是分配给trip变量的生成器表达式。我们已经组合了三个元组,包括开始、结束和从开始到结束的距离。起始和结束对来自legs()功能。legs()函数使用从 KML 文件中提取的latitude-longitude对生成的floating-point数据。

输出类似于以下命令片段:

(37.54901619777347, -76.33029518659048) (37.840832, -76.273834) 17.7246
(37.840832, -76.273834) (38.331501, -76.459503) 30.7382
(38.331501, -76.459503) (38.845501, -76.537331) 31.0756
(36.843334, -76.298668) (37.549, -76.331169) 42.3962
(37.549, -76.331169) (38.330166, -76.458504) 47.2866
(38.330166, -76.458504) (38.976334, -76.473503) 38.8019

每个单独的处理步骤都被简洁地定义。同样,概述可以简洁地表示为函数和生成器表达式的组合。

显然,我们可能希望对这些数据应用几个进一步的处理步骤。当然,第一种方法是使用字符串的format()方法来生成外观更好的输出。

更重要的是,我们希望从这些数据中提取一些聚合值。我们将这些值称为可用数据的归约。我们希望减少数据以获得最大和最小纬度,例如,显示这条路线的最北端和最南端。我们希望减少数据以获得一条腿的最大距离以及所有人的总距离legs

我们在使用 Python 时遇到的问题是,trip变量中的输出生成器只能使用一次。我们无法轻松地对这些详细数据进行几次缩减。我们可以使用itertools.tee()多次使用 iterable。然而,对于每次缩减,读取和解析 KML 文件似乎是浪费。

我们可以通过具体化中间结果来提高处理效率。我们将在下一节中介绍这一点。然后,我们将看到如何计算可用数据的多重缩减。

使用 any()和 all()作为归约

any()all()功能提供boolean还原能力。这两个函数都将值集合减少为单个TrueFalseall()功能确保所有值均为Trueany()功能确保至少有一个值为True

这些函数与表示数理逻辑的普遍量词和存在量词密切相关。例如,我们可能希望断言给定集合中的所有元素都有一个属性。这方面的一种形式主义可能如下所示:

我们将其理解为*对于 S 中的所有 x,函数 Prime(x),*为真。我们在逻辑表达式前面放了一个量词。

在 Python 中,我们稍微改变项目的顺序以转录逻辑表达式,如下所示:

all(isprime(x) for x in someset)  

这将针对x的每个不同值评估isprime(x)函数,并将值集合减少为单个TrueFalse

any()功能与存在量词有关。如果要断言集合中没有值是素数,可以使用以下两个等价表达式之一:

第一种说法是,并非S中的所有元素都是素数。第二个版本断言在S中存在一个不是素数的元素。这两个元素是等价的,即如果不是所有元素都是素数,那么一个元素必须是非素数。

在 Python 中,我们可以切换术语的顺序,并将其转录为工作代码,如下所示:

not all(isprime(x) for x in someset)
any(not isprime(x) for x in someset)  

因为它们是等价的,所以有两个原因让我们更喜欢其中一个:性能和清晰度。性能几乎相同,因此归结为清晰度。以下哪项最清楚地说明了这种情况?

all()函数可以描述为一组值的and缩减。结果类似于在给定的值序列之间折叠and运算符。类似地,any()功能可以描述为or减少。当我们在第 10 章Functools 模块中查看reduce()函数时,我们将返回到这种通用归约。这里没有最好的答案;这是一个什么对目标受众来说最具可读性的问题。

我们还需要研究这些函数的退化情况。如果序列没有元素怎么办?all(())all([])的值是多少?

如果我们问,“所有的元素都在一个空集素数中吗?那么答案是什么?为了获得这方面的指导,我们将稍微展开这个问题,并看看恒等元素的概念。

如果我们问,“所有元素都在一个空集素数中吗?所有元素都在SomeSet素数中吗?”我们会得到一个关于如何进行的提示。我们正在对一个空集进行and缩减,并对SomeSet进行and缩减:

事实证明,and操作符可以自由分布。我们可以将其改写为,作为两个集合的并集,然后对其求值为素数:

显然,。如果我们将一个集合S与一个空集合合并,则得到原始集合S。空集可称为联合标识元素。这与零是加法标识元素的方式平行:

类似地,any(())必须是or标识元素,即False。如果我们考虑乘法标识元素 1,其中,那么all(())必须是True

以下代码演示 Python 遵循以下规则:

>>> all(())
True
>>> any(())
False  

Python 为我们提供了一些非常好的工具来执行涉及逻辑的处理。我们有内置的andornot操作符。但是,我们也有这些面向集合的any()all()功能。

使用 len()和 sum()

len()sum()函数提供了两个简单的归约——元素计数和序列中元素的总和。这两个函数在数学上相似,但它们的 Python 实现却截然不同。

从数学上讲,我们可以观察到这种冷静的平行性。len()函数返回集合中每个值的 1 之和。

sum()函数返回集合中每个值的x之和。

sum()函数适用于任何 iterable。len()函数不适用于 iterables;它只适用于序列。这些函数实现中的这种小小的不对称性对于统计算法来说有点尴尬。

对于空序列,这两个函数都返回正确的零加法标识元素:

>>> sum(())
0

当然,sum(())返回一个整数零。当使用其他数值类型时,整数零将强制为可用数据的正确类型。

使用总和和计数进行统计

算术平均值的定义基于sum()len()有一个看似微不足道的定义,如下所示:

def mean(items):
    return sum(items)/len(items)

虽然很优雅,但这实际上并不适用于 iterables。此定义仅适用于支持len()功能的集合。在尝试编写适当的类型注释时,很容易发现这一点。mean(items: Iterable)->float的定义不起作用,因为Iterable类型不支持len()

事实上,我们很难根据可比性进行简单的均值或标准差计算。在 Python 中,我们要么具体化序列对象,要么求助于更复杂的操作。

定义需要如下所示:

from collections import Sequence
def mean(items: Sequence) -> float:
    return sum(items)/len(items)

这包括适当的类型提示,以确保sum()len()都能工作。

在以下定义中,我们有一些可选的、优雅的平均值和标准偏差表达式:

import math
s0 = len(data)  # sum(x**0 for x in data)
s1 = sum(data)  # sum(x**1 for x in data)
s2 = sum(x**2 for x in data)

mean = s1/s0
stdev = math.sqrt(s2/s0 - (s1/s0)**2)  

这三个总和,s0s1s2具有整齐、平行的结构。我们可以很容易地从两个和中计算出平均值。标准差有点复杂,但它是基于三个可用的总和。

这种令人愉快的对称性也适用于更复杂的统计函数,例如相关性,甚至最小二乘线性回归。

两组样本之间的相关矩可根据其标准值计算。以下是计算标准化值的函数:

def z(x: float, m_x: float, s_x: float) -> float:
    return (x-m_x)/s_x

计算简单,从每个样本x中减去平均值μ_x,然后除以标准偏差σ_x。这给了我们一个以西格玛为单位测量的值,σ。预计大约三分之二的时间会出现一个值±1σ。较大的值应该不太常见。超出±3σ的值应在少于百分之一的时间内出现。

我们可以按如下方式使用此标量函数:

>>> d = [2, 4, 4, 4, 5, 5, 7, 9]
>>> list(z(x, mean(d), stdev(d)) for x in d)
[-1.5, -0.5, -0.5, -0.5, 0.0, 0.0, 1.0, 2.0]

我们实现了一个list,它由基于变量d中一些原始数据的标准化分数组成。我们使用生成器表达式将标量函数z()应用于序列对象。

mean()stdev()函数仅基于前面所示的示例:

def mean(samples: Sequence) -> float:
    return s1(samples)/s0(samples)
def stdev(samples: Sequence) -> float:
    N= s0(samples) 
    return sqrt((s2(samples)/N)-(s1(samples)/N)**2)

类似地,可以定义三个求和函数,而不是 lambda 形式,如以下代码所示:

def s0(samples: Sequence) -> float:
    return sum(1 for x in samples) # or len(data)
def s1(samples: Sequence) -> float:
    return sum(x for x in samples) # or sum(data)
def s2(samples: Sequence) -> float:
    return sum(x*x for x in samples)  

虽然这非常有表现力和简洁,但有点令人沮丧,因为我们不能在这里简单地使用 iterable。当计算平均值时,需要 iterable 的和和和 iterable 的计数。对于标准差,需要两个和和一个可数计数。对于这种统计处理,我们必须具体化一个序列对象,以便能够多次检查数据。

以下代码显示了如何计算两组样本之间的相关性:

def corr(samples1: Sequence, samples2: Sequence) -> float:
    m_1, s_1 = mean(samples1), stdev(samples1)
    m_2, s_2 = mean(samples2), stdev(samples2)
    z_1 = (z( x, m_1, s_1 ) for x in samples1)
    z_2 = (z( x, m_2, s_2 ) for x in samples2)
    r = (sum(zx1*zx2 for zx1, zx2 in zip(z_1, z_2)) 
        / len(samples1))
    return r

此相关函数收集两组样本的基本统计摘要:平均值和标准偏差。鉴于这些总结,我们定义了两个生成函数,它们将为每组样本创建标准化值。然后,我们可以使用zip()函数(见下一个示例)将两个规范化值序列中的项目配对,并计算这两个规范化值的乘积。标准化分数乘积的平均值就是相关性。

以下代码是收集两组样本之间相关性的示例:

>>> # Height (m)
>>> xi= [1.47, 1.50, 1.52, 1.55, 1.57, 1.60, 1.63, 1.65,
...    1.68, 1.70, 1.73, 1.75, 1.78, 1.80, 1.83,]
>>> # Mass (kg)
>>> yi= [52.21,53.12,54.48,55.84,57.20,58.57,59.93,61.29,
...    63.11, 64.47, 66.28, 68.10, 69.92, 72.19, 74.46,] 
>>> round(corr( xi, yi ), 5)
0.99458  

我们已经向您展示了两个数据点序列,xiyi。相关度在**.99**以上,这表明两个序列之间有很强的相关性。

这向我们展示了函数式编程的优势之一。我们已经创建了一个方便的统计模块,使用了六个函数,这些函数的定义都是单个表达式。反例是corr()函数,它可以简化为一个非常长的表达式。此函数中的每个内部变量只使用一次;局部变量可以替换为创建它的表达式的副本和粘贴。这向我们展示了corr()函数有一个函数设计,尽管它是用 Python 的六行代码编写的。

使用 zip()构造和展平序列

zip()函数将来自多个迭代器或序列的值交错。它将根据每个n输入可比数或序列中的值创建n元组。我们在上一节中使用它来交错两组样本中的数据点,创建两个元组。

The zip() function is a generator. It does not materialize a resulting collection.

以下是显示zip()函数功能的代码示例:

>>> xi= [1.47, 1.50, 1.52, 1.55, 1.57, 1.60, 1.63, 1.65,
... 1.68, 1.70, 1.73, 1.75, 1.78, 1.80, 1.83,] 
>>> yi= [52.21, 53.12, 54.48, 55.84, 57.20, 58.57, 59.93, 61.29,
... 63.11, 64.47, 66.28, 68.10, 69.92, 72.19, 74.46,] 
>>> zip( xi, yi )
<zip object at 0x101d62ab8>
>>> list(zip( xi, yi ))
[(1.47, 52.21), (1.5, 53.12), (1.52, 54.48), 
 (1.55, 55.84), (1.57, 57.2), (1.6, 58.57), 
 (1.63, 59.93), (1.65, 61.29), (1.68, 63.11), 
 (1.7, 64.47), (1.73, 66.28), (1.75, 68.1), 
 (1.78, 69.92), (1.8, 72.19), (1.83, 74.46)]  

zip()功能有许多边缘情况。我们必须就其行为提出以下问题:

  • 如果没有争论,会发生什么?
  • 只有一个论点会发生什么?
  • 当序列长度不同时会发生什么?

与其他函数一样,例如any()all()len()sum(),我们希望在对空序列应用约简时得到一个标识值。例如,sum(())应该为零。这个概念告诉我们zip()的身份值应该是什么。

显然,这些边缘情况中的每一个都必须产生某种可匹配的输出。下面是一些阐明行为的代码示例。首先,空参数列表:

>>> zip()
<zip object at 0x101d62ab8>
>>> list(_)
[]

我们可以看到没有参数的zip()函数是一个生成器函数,但不会有任何项。这符合输出是可测试的要求。

接下来,我们将尝试一个 iterable:

>>> zip( (1,2,3) )
<zip object at 0x101d62ab8>
>>> list(_)
[(1,), (2,), (3,)]

在这种情况下,zip()函数从每个输入值发出一个元组。这也很有道理。

最后,我们来看看zip()函数使用的不同长度list方法:

>>> list(zip((1, 2, 3), ('a', 'b')))
[(1, 'a'), (2, 'b')]

这一结果值得商榷。为什么要截断?为什么不用None值填充较短的列表?zip()功能的替代定义在itertools模块中作为zip_longest()功能提供。我们将在第 8 章Itertools 模块中了解这一点。

解压缩已压缩的序列

我们可以插入zip()映射可以反转。我们将研究几种解压元组集合的方法。

We can't fully unzip an iterable of tuples, since we might want to make multiple passes over the data. Depending on our needs, we may need to materialize the iterable to extract multiple values.

第一种方法我们已经见过很多次了:我们可以使用生成器函数来解压元组序列。例如,假设以下对是具有两个元组的序列对象:

p0= (x[0] for x in pairs)
p1= (x[1] for x in pairs)  

这将创建两个序列。p0序列具有每两个元组的第一个元素;p1序列每两个元组有第二个元素。

在某些情况下,我们可以使用for循环的多重赋值来分解元组。以下是计算乘积之和的示例:

sum(p0*p1 for for p0, p1 in pairs)  

我们使用for语句将每两个元组分解为p0p1

展平序列

有时,我们会压缩需要展平的数据。例如,我们的输入可以是一个包含列数据的文件。看起来是这样的:

2     3      5      7     11     13     17     19     23     29
31    37     41     43    47     53     59     61     67     71
...

我们可以很容易地使用(line.split() for line in file)创建序列。该序列中的每一项都是一行值中的 10 项元组。

这将以 10 个值的块创建数据。情况如下:

>>> blocked = list(line.split() for line in file)
>>> blocked
[['2', '3', '5', '7', '11', '13', '17', '19', '23', '29'], ['31', '37', '41', '43', '47', '53', '59', '61', '67', '71'], ['179', '181', '191', '193', '197', '199', '211', '223', '227', '229']]

这只是一个开始,但还不完整。我们想把数字分成一个简单的序列。输入中的每一项都是一个 10 元组;我们不希望一次分解一个项目。

对于这种展平,我们可以使用两级生成器表达式,如以下代码段所示:

>>> (x for line in blocked for x in line)
<generator object <genexpr> at 0x101cead70>
>>> list(_)
['2', '3', '5', '7', '11', '13', '17', '19', '23', '29', '31', 
 '37', '41', '43', '47', '53', '59', '61', '67', '71',
 ... ]

第一个for子句将blocked列表中的 10 个值分配给line变量。第二个for子句将line变量中的每个字符串分配给x变量。最后一个生成器是分配给x变量的值序列。

我们可以通过以下简单的重写来理解这一点:

def flatten(data: Iterable[Iterable[Any]]) -> Iterable[Any]:
    for line in data:
        for x in line:
            yield x

这个转换向我们展示了生成器表达式是如何工作的。第一个for子句(for line in data逐步遍历数据中的每 10 个元组。第二条for条款(for x in line逐步通过第一条for条款中的每一项。

此表达式将序列结构的序列展平为单个序列。更一般地说,它将包含一个 iterable 的任何 iterable 展平为一个单一的、平坦的 iterable。它将适用于列表列表、集合列表或任何其他嵌套 iterables 组合。

构造平坦序列

有时,我们会有原始数据,这是一个简单的值列表,我们希望将这些值组合成子组。这有点复杂。我们可以使用itertools模块的groupby()功能来实现这一点。这必须等到第 8 章ITerols 模块之后。

假设我们有一套简单的公寓list,如下所示:

flat= ['2', '3', '5', '7', '11', '13', '17', '19', '23', '29', 
 '31', '37', '41', '43', '47', '53', '59', '61', '67', '71', 
 ... ]

我们可以编写嵌套的生成器函数,从平面数据构建序列结构序列。为此,我们需要一个可以多次使用的迭代器。该表达式类似于以下代码段:

>>> flat_iter = iter(flat)
>>> (tuple(next(flat_iter) for i in range(5)) 
...     for row in range(len(flat)//5)
... )
<generator object <genexpr> at 0x101cead70>
>>> list(_)
[('2', '3', '5', '7', '11'), 
 ('13', '17', '19', '23', '29'), 
 ('31', '37', '41', '43', '47'), 
 ('53', '59', '61', '67', '71'), 
 ...
]

首先,我们创建一个迭代器,该迭代器存在于我们将用于创建序列的两个循环之外。生成器表达式使用tuple(next(flat_iter) for i in range(5))flat_iter变量中的 iterable 值创建五项元组。此表达式嵌套在另一个生成器中,该生成器将内部循环重复适当次数以创建所需的值序列。

仅当平面列表被平均分割时,此操作才有效。如果最后一行有部分元素,我们需要分别处理它们。

我们可以使用这种函数将数据分组到相同大小的元组中,最后是奇数大小的元组,使用以下定义:

ItemType = TypeVar("ItemType")
Flat = Sequence[ItemType]
Grouped = List[Tuple[ItemType, ...]]

def group_by_seq(n: int, sequence: Flat) -> Grouped:
    flat_iter=iter(sequence)
    full_sized_items = list( tuple(next(flat_iter) 
        for i in range(n))
            for row in range(len(sequence)//n))
    trailer = tuple(flat_iter)
    if trailer:
        return full_sized_items + [trailer]
    else:
        return full_sized_items  

group_by_seq()函数中,建立初始list并将其分配给变量full_sized_items。此列表中的每个tuple的大小为n。如果有剩余,则使用尾随项构建长度非零的tuple,我们可以将其附加到全尺寸项的list。如果拖车tuple的长度为零,则可以安全地忽略它。

类型提示包括一个类型变量的泛型定义ItemType。类型变量的目的是显示该函数的输入类型将从函数返回。字符串序列或浮点数序列都可以正常工作。

输入汇总为项目的Sequence。输出为Tuples项的List项。这些项目都是通用类型,用ItemType类型变量描述。

这并不像我们所看到的其他算法那样简单实用。我们可以将其改写为一个更简单的生成器函数,生成一个 iterable 而不是 list。

以下代码使用while循环作为尾部递归优化的一部分:

ItemType = TypeVar("ItemType")
Flat_Iter = Iterator[ItemType]
Grouped_Iter = Iterator[Tuple[ItemType, ...]]

def group_by_iter(n: int, iterable: Flat_Iter) -> Grouped_Iter:
     row = tuple(next(iterable) for i in range(n))
     while row:
         yield row
         row = tuple(next(iterable) for i in range(n))

我们已经从输入 iterable 创建了一行所需的长度。在输入 iterable 的末尾,tuple(next(iterable) for i in range(n))的值将是一个长度为零的元组。这可能是递归定义的基本情况。这被手动优化为while语句的终止条件。

类型提示已经过修改,以反映迭代器的工作方式。它不限于序列。因为它显式地使用了next(),所以必须像这样使用:group_by_iter(7, iter(flat))iter()函数必须用于从集合创建迭代器。

构造平面序列–另一种方法

假设我们有一个简单的平面list,我们想从这个列表中创建一对。 以下为所需数据:

flat= ['2', '3', '5', '7', '11', '13', '17', '19', '23', '29', 
 '31', '37', '41', '43', '47', '53', '59', '61', '67', '71',... ]

我们可以使用列表切片创建对,如下所示:

zip(flat[0::2], flat[1::2])

切片flat[0::2]是所有的偶数位置。切片flat[1::2]是所有奇数位置。如果我们把这些压缩在一起,我们会得到一个两元组。索引[0]处的项目是来自第一个偶数位置的值,然后索引[1]处的项目是来自第一个奇数位置的值。如果元素的数量是偶数,这将很好地生成成对。如果项目总数为奇数,则该项目将被丢弃;有一个简便的解决办法。

这个表达式的优点是很短。上一节中显示的函数是解决相同问题的较长方法。

这种方法可以推广。我们可以使用*(args)方法生成一系列必须压缩在一起的序列。如下所示:

zip(*(flat[i::n] for i in range(n)))

这将生成n切片-flat[0::n]flat[1::n]flat[2::n]等,以及flat[n-1::n]。这个切片集合成为zip()的参数,然后它会交错每个切片的值。

回想一下,zip()在最短的list处截断序列。这意味着,如果list不是分组因子nlen(flat)%n != 0)的偶数倍,这是最后一个切片,那么它的长度将与其他部分不同,其他部分都将被截断。这很少是我们想要的。

如果我们使用itertools.zip_longest()方法,那么我们将看到最后的元组将填充足够的None值,使其具有n长度。在某些情况下,这种填充是可以接受的。在其他情况下,额外的值是不可取的。

list分组数据的切片方法是另一种方法,用于将数据的平面序列构造为块。由于它是一个通用的解决方案,与上一节中的函数相比,它似乎没有提供太多的优势。作为一个专门用于从一个扁平的楦头生成两个元组的解决方案,它非常简单。

使用 reversed()更改顺序

有时我们需要一个相反的顺序。Python 为我们提供了两种方法:reversed()函数和具有反向索引的切片。

例如,考虑将基转换为十六进制或二进制。以下代码是一个简单的转换函数:

def digits(x: int, b: int) -> Iterator[int]:
    if x == 0: return
    yield x % b
    for d in digits(x//b, b):
        yield d  

此函数使用递归产生从最低有效到最高有效的数字。x%b的值将是基础bx的最低有效位。

我们可以将其形式化如下:

在许多情况下,我们更希望以相反的顺序生成数字。我们可以用reversed()函数包装此函数,以交换数字顺序:

def to_base(x: int, b: int) -> Iterator[int]:
    return reversed(tuple(digits(x, b)))

The reversed() function produces an iterable, but the argument value must be a sequence object. The function then yields the items from that object in the reverse order.

我们可以用切片做类似的事情,比如tuple(digits(x, b))[::-1]。但是,切片不是迭代器。切片是从另一个物化对象构建的物化对象。在这种情况下,对于如此小的值集合,区别很小。由于reversed()函数使用较少的内存,因此对于较大的集合可能是有利的。

使用 enumerate()包含序列号

Python 提供了enumerate()函数,用于将索引信息应用于序列或 iterable 中的值。它执行一种特殊的包裹,可以用作unwrap(process(wrap(data)))设计模式的一部分。

它看起来像以下代码段:

>>> xi
[1.47, 1.5, 1.52, 1.55, 1.57, 1.6, 1.63, 1.65, 1.68, 1.7, 1.73, 
 1.75, 1.78, 1.8, 1.83]
>>> list(enumerate(xi))
[(0, 1.47), (1, 1.5), (2, 1.52), (3, 1.55), (4, 1.57), 
 (5, 1.6), (6, 1.63), (7, 1.65), (8, 1.68), (9, 1.7), 
 (10, 1.73), (11, 1.75), (12, 1.78), (13, 1.8), (14, 1.83)]  

enumerate()函数将每个输入item转换成一对,带有序列号和原始item。它与以下内容大致相似:

zip(range(len(source)), source)

enumerate()的一个重要特征是,结果是一个 iterable,它与任何 iterable 输入一起工作。

例如,在研究统计处理时,enumerate()函数可以方便地将单个值序列转换为更合适的时间序列,方法是在每个样本前加上一个数字。

总结

在本章中,我们看到了使用一些内置缩减的详细方法。

我们使用了any()all()来进行基本的逻辑处理。这些是使用简单运算符(如orand)进行简化的简洁示例。

我们还研究了数字缩减,例如,len()sum()。我们已经应用了这些函数来创建一些高阶统计处理。我们将在第 6 章递归和归约中回到这些归约。

我们还研究了一些内置映射。

zip()功能合并多个序列。这使我们可以在构建和扁平化更复杂的数据结构的上下文中使用它。正如我们将在后面章节的示例中看到的,嵌套数据在某些情况下很有用,而平面数据在其他情况下很有用。

enumerate()函数将一个 iterable 映射为两个元组的序列。每两个元组在索引[0]处有序列号,在索引[1]处有原始值。

reversed()函数迭代序列对象中的项,其原始顺序颠倒。有些算法在以一个顺序生成结果时效率更高,但我们希望以相反的顺序呈现这些结果。

在下一章中,我们将研究使用附加函数作为参数来自定义其处理的映射和归约函数。接受函数作为参数的函数是高阶函数的第一个示例。我们还将讨论返回函数的函数。