内容


在 Python 中按需处理数据,第 1 部分

Python 迭代器和生成器

了解如何在 Python 中以随需应变方式代替预处理方式来高效处理数据

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 在 Python 中按需处理数据,第 1 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:在 Python 中按需处理数据,第 1 部分

敬请期待该系列的后续内容。

幸运的是,在当今时代,各种市场因素已将内存、磁盘甚至 CPU 容量的价格统统压缩至原先难以想象的低价。但与此同时,诸如大数据、AI 和认知计算这类蓬勃发展的应用,正在以超快的速度推升我们对于这些资源的需求。令人感觉有点啼笑皆非的是,当计算资源较充足时,开发人员了解如何降低资源耗用量来保持竞争力却变得愈加重要。

近二十年来,Python 始终是一种热门编程语言,主要原因就在于它非常易于学习。短短一小时,您就能学会如何轻松操作列表和字典。但令人生厌的是,这种利用列表和字典来解决众多问题的方法不免有些幼稚,它很快就会为您带来麻烦,迫使您不得不设法扩展应用规模,因为稍有不慎,Python 就会变得比其他编程语言更加耗用资源。

好消息是 Python 有一些非常实用的功能,有助于提升处理效率。其中许多功能都基于 Python 的迭代器协议,这也是本教程的重要主题。在此基础上将构建一整套教程,总共包含四个部分,主要向您展示如何利用 Python 来高效处理大型数据集。

您应熟悉 Python 基础知识,如条件、循环、函数、异常、列表和字典。此系列教程是围绕 Python 3 开展的;要运行代码,就需要使用 Python 3.5 或更高版本。

迭代器

您最初接触到的 Python 循环可能类似以下代码:

for ix in range(10): 
    print(ix)

Python 的 for 语句在所谓的迭代器上运行。迭代器是可重复调用以生成一系列值的对象。如果 in 关键字后的值尚未成为迭代器,那么 for 会尝试将其转换为迭代器。内置 range 函数就是可以转换为迭代器的一个示例。它可生成一系列数字,for 循环会对这些项进行迭代,依次将每个项分配到变量 ix

现在,可通过更仔细地查看像 range 这样的迭代器来深入了解 Python。在 Python 解释器中输入以下内容:

r = range(10)

您现在已完成了 range 迭代器的初始化,但仅此而已。接下来,要求此迭代器提供其首个值。在 Python 中使用内置 next 函数来要求迭代器提供一个值。

>>> r = range(10)
>>> print(next(r))
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
TypeError: 'range' object is not an iterator

此异常表明您必须要先将对象转换为迭代器,然后才能将其用作迭代器。可以使用内置 iter 函数来执行此操作。

r = iter(range(10))
print(next(r))

此时会打印 0,这与您的预期相符。接下来再次输入 print(next(r)),将会打印 1,以此类推。继续在此行中输入。在这一点上,值得高兴的是,对于大部分系统,只需按 Python 解释器上的向上箭头即可检索最近使用的命令,然后按 Enter 键即可重复执行命令,您甚至还可以在按 Enter 键之前随意对其进行调整。

在此情况下,最终将得到类似如下的结果:

>>> print(next(r))
9
>>> print(next(r))
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
StopIteration

我们要求的范围仅为 10 个整数,因此在它生成 9 之后即告结束。此迭代器不会立即执行任何操作来指示它已结束,但任何后续的 next() 调用都将生成 StopIteration 异常。就像处理其他任何异常一样,您可以选择自行编写代码对其进行处理。在迭代器 r 用完后,尝试输入以下代码。

try: 
    print(next(r))
except StopIteration as e: 
    print("That's all folks!")

它会打印消息“That's all folks!”for 语句使用 StopIteration 异常来确定何时退出循环。

其他可迭代对象

range 只是可转换为迭代器的一种对象。以下解释器会话演示了如何将各种标准类型解释为迭代器。

>>> it = iter([1,2,3])
>>> print(next(it))
1
>>> it = iter((1,2,3))
>>> print(next(it))
1
>>> it = iter({1: 'a', 2: 'b', 3: 'c'})
>>> print(next(it))
1
>>> it = iter({'a': 1, 'b': 2, 'c': 3})
>>> print(next(it))
a
>>> it = iter(set((1,2,3)))
>>> print(next(it))
1
>>> it = iter('xyz')
>>> print(next(it))
x

对于列表或元组,这是非常直接的过程。字典仅对其键进行迭代,当然无法保证顺序。对于集合也无法保证迭代顺序,即使在此示例中,来自迭代器的首个项碰巧也是用于构造集合的元组中的首个项。字符串会对其字符进行迭代。所有此类对象都称为可迭代对象

正如您可能想到的,并非每个 Python 对象都可转换为迭代器。

>>> it = iter(1)
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>> it = iter(None)
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not iterable

当然,最棒的是您可以生成自己的迭代器类型。您只需使用某些明确指定的方法来定义一个类即可。 这超出了本系列教程的范围,但没关系,因为要自行创建自定义迭代器,最直接的方法并不是采用一个特殊类,而是采用称为生成器函数的特殊函数。接下来就讨论生成器函数。

生成器

您已熟悉函数的概念,函数需要一些自变量,并返回值或 None。可能存在多个出口点、多个返回语句或者只有函数的最后一个缩进行,这与 return None 相同,但每次运行此函数时,只会基于此函数中的条件选中其中一个出口点。

生成器函数是特殊类型的函数,交互方式更为复杂,但对于调用此类函数的代码而言则更为实用。以下是可供您粘贴到自己的解释器会话中的简单示例:

def gen123(): 
    yield 2
    yield 5
    yield 9

以上是一个自动的生成器函数,因为其主体中包含至少一条 yield 语句。这一细微差别是将常规函数转变为生成器函数的唯一因素,这有点棘手,因为常规函数与生成器函数之间存在巨大差异。

以类似任何其他函数的方式来调用生成器函数:

>>> it = gen123()
>>> print(it)
<generator object gen123 at 0x10ccccba0>

此函数调用会立即返回,并且不含函数主体中指定的值。调用生成器函数始终会返回所谓的生成器对象。生成器对象是一种迭代器,可从生成器函数主体中的 yield 语句生成各种值。 在标准术语中,生成器对象会生成一系列值。让我们从先前的代码片段中深入挖掘生成器对象。

>>> print(next(it))
2
>>> print(next(it))
5
>>> print(next(it))
9
>>> print(next(it))
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
StopIteration

每次对该对象调用 next() 时,都会返回下一个 yield 值,直至没有任何其他值为止,在此情况下,将返回 StopIterator 异常。当然,由于它是一个迭代器,因而可在 for 循环中使用。只要记得创建新的生成器对象即可,因为第一个生成器对象已耗尽。

>>> it = gen123()
>>> for ix in it: 
...print(ix)
... 
1
2
3
>>> for ix in gen123(): 
...print(ix)
... 
1
2
3

生成器函数自变量

生成器函数接受自变量,这些自变量可传递到生成器主体中。粘贴以下生成器函数。

def gen123plus(x): 
    yield x + 1
    yield x + 2
    yield x + 3

现在,对其试用其他自变量,例如:

>>> for ix in gen123plus(10): 
...print(ix)
... 
11
12
13

对生成器对象进行迭代时,其函数处于暂挂状态,并在您执行时恢复,这样就为 Python 函数引入了新的概念。现在,您实际上可以通过重叠方式从多个函数运行代码。以下面的会话为例。

>>> it1 = gen123plus(10)
>>> it2 = gen123plus(20)
>>> print(next(it1))
11
>>> print(next(it2))
21
>>> print(next(it1))
12
>>> print(next(it1))
13
>>> print(next(it2))
22
>>> print(next(it1))
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
StopIteration
>>> print(next(it2))
23
>>> print(next(it2))
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
StopIteration

我从一个生成器函数创建两个生成器对象。随后,我可从其中任一对象获取下一个项,并留意每个对象独立暂挂和恢复的方式。它们在各个方面都彼此独立,包括进入 StopIteration 的方式。

务必仔细研究此会话,直至您确实掌握整个过程为止。一旦掌握了这一过程,您也就真正对生成器有了基础的认识,并明白了它们如此强大的原因所在。

请记住,您还可以使用所有常用的定位和关键字自变量功能。

生成器函数中的局部状态

您可在生成器函数中通过条件、循环和局部变量执行所有常规操作,构建极其复杂的专用迭代器。

让我们通过下一个示例来轻松一下。我们都对受制于天气感到十分厌烦。让我们来自行创造一些天气。清单 1 是一个气象模拟器,可打印一系列晴天或雨天,有时还带有一些注释。

提到天气,晴天后通常还是晴天,雨天后通常还是雨天。您可通过随机选择次日天气来对此进行模拟,但天气保持不变的概率较高。可以用“反复无常”一词来形容天气变化情况。在此生成器函数中,有一个自变量为 volatility,它的值应在 0 到 1 之间。这个自变量的值越低,天气从前一天到后一天保持不变的可能性就越高。 在此清单中,volatility 设置为 0.2,这意味着平均 80% 的天气前后两天应保持不变。

此清单具有一项附加功能,如果连续三天以上为晴天,或者连续三天以上为雨天,它会发布一些注释。

清单 1. 清单 1.气象模拟器
import random

def weathermaker(volatility, days): 
    '''
    Yield a series of messages giving the day's weather and occasional commentary

    volatility - a float between 0 and 1; the greater this number the greater
                    the likelihood that the weather will change on each given day
    days - number of days for which to generate weather
    '''
    #Always start as if yesterday were sunny
    current_weather = 'sunny'
    #First item is the probability that the weather will stay the same
    #Second item is the probability that the weather will change
    #The higher the volatility the greater the likelihood of change
    weights = [1.0-volatility, volatility]
    #For fun track how many sunny days in a row there have been
    sunny_run = 1
    #How many rainy days in a row there have been
    rainy_run = 0
    for day in range(days): 
        #Figure out the opposite of the current weather
        other_weather = 'rainy' if current_weather == 'sunny' else 'sunny'
        #Set up to choose the next day's weather.First set up the choices
        choose_from = [current_weather, other_weather]
        #random.choices returns a list of random choices based on the weights
        #By default a list of 1 item, so we grab that first and only item with [0]
        current_weather = random.choices(choose_from, weights)[0]
        yield 'today it is ' + current_weather
        if current_weather == 'sunny': 
            #Check for runs of three or more sunny days
            sunny_run += 1
            rainy_run = 0
            if sunny_run >= 3: 
                yield "Uh oh! We're getting thirsty!"
        else: 
            #Check for runs of three or more rainy days
            rainy_run += 1
            sunny_run = 0
            if rainy_run >= 3: 
                yield "Rain, rain go away!"
    return

#Create a generator object and print its series of messages
for msg in weathermaker(0.2, 10): 
    print(msg)

weathermaker 函数使用了许多常用编程功能,并且还解释了生成器的一些很有趣的特性。生成的项数并不固定。可能会与天数一样少,也可能会因连续晴天或雨天而添加的注释导致数量增加。这些都是在不同条件分支下生成的。

运行此清单后,应显示如下内容:

$ python weathermaker.py
today it is sunny
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is rainy
today it is sunny
today it is rainy
today it is rainy
today it is rainy
Rain, rain go away!
today it is rainy
Rain, rain go away!

当然,这是随机的,每次天气保持不变的几率都可达到 80%,因此可以轻易获得以下结果:

$ python weathermaker.py
today it is sunny
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!
today it is sunny
Uh oh! We're getting thirsty!

请花些时间自行尝试一下,首先为 volatilitydays 传入不同的值,然后对生成器函数代码本身进行调整。试验是确保您真正了解生成器运作方式的最佳途径。

我希望这个有趣的示例通过生成器的一些强大功能,可以激发您的想像力。当然无需生成器也可以编写上述代码,但这种方法不仅更易于表达、通常更高效,并且除了清单底部的简单循环之外,还能通过其他有趣的方法来复用 weathermaker 生成器。

生成器表达式

生成器的常见用途是在一个迭代器上进行迭代,并通过某种方式对其进行操纵,生成经过修改的迭代器。

让我们来编写一个生成器,此生成器采用一个迭代器,并根据提供的一组替换值按顺序替换找到的值。

def substituter(seq, substitutions): 
    for item in seq: 
        if item in substitutions: 
            yield substitutions[item]
        else: 
            yield item

在以下会话中,您可以看到此生成器的使用方式示例:

>>> s = 'hello world and everyone in the world'
>>> subs = {'hello': 'goodbye', 'world': 'galaxy'}
>>> for word in substituter(s.split(), subs): 
...print(word, end=' ')
... 
goodbye galaxy and everyone in the galaxy

同样,请花些时间来自行尝试,试试其他循环操作,直至您明确理解生成器的工作方式为止。

这种操作极为常见,以致于 Python 为它提供了一种易用的语法,即生成器表达式。以下是使用生成器表达式实现的先前会话。

>>> words = ( subs.get(item, item) for item in s.split() )
>>> for word in words: 
...print(word, end=' ')
... 
goodbye galaxy and everyone in the galaxy

简而言之,只要您使用括号将 for 表达式括起,那么它就会变成一个生成器表达式。生成的对象即为生成器对象,在此情况下此对象被分配至 words。有时,您最后会使用某些更有趣的 Python 内容来适应此类表达式。在此情况下,我对字典使用 get 方法,此方法会查找一个键,但允许我指定未找到此键时返回的默认值。我要求系统返回 item 的替换值(如果能找到),否则仅按现状返回 item

列表解析概述

您可能已熟悉列表解析。以下是相似的语法,但使用了方括号。列表解析的结果就是一个列表:

>>> mylist = [ ix for ix in range(10, 20) ]
>>> print(mylist)
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

生成器表达式的语法与之相似,但它返回的是生成器对象:

>>> mygen = ( ix for ix in range(10, 20) )
>>> print(mygen)
<generator object <genexpr> at 0x10ccccba0>
>>> print(next(mygen))
10

以上两个示例之间的实际差异主要在于,第一个示例中创建的列表自创建之后便存在,会占用所有必要内存来存储其值。生成器表达式不会使用这么多的存储空间,而是处于暂挂状态,当对其进行迭代时才恢复,就像生成器函数的主体一样。事实上,它允许您按需获取数据,而不是预先为您存储所有数据。

打个比方,您全家每年喝 200 加仑牛奶,但您并不想在自家地下室建一个储藏设施来储藏这么多牛奶,而是根据需要每次到店里买 1 加仑。使用生成器代替不断构建列表,就有点像去杂货店购买而不是自建仓库一样。

此外,还有字典表达式,但这不在本教程的讨论范围内。请注意,您可以轻松将生成器表达式转换为列表,有时这是一次性耗用生成器的一种方式,但若稍有不慎,这也可能导致通过创建需要大量内存的列表而违背了使用生成器的本意。

>>> mygen = ( ix for ix in range(10, 20) )
>>> print(list(mygen))
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

在此系列教程中,有时我将从生成器构建列表,以便于快速展示。

过滤和链接

您可以在生成器表达式中使用简单的条件从输入迭代器中过滤掉某些项。以下示例可生成从 1 到 20 之间不是 2 和 3 的倍数的所有数字。它使用易用的 math.gcd 函数,返回这两个整数的最大公约数。例如,如果某个数字和 2 的最大公约数为 1,那么此数字不是 2 的倍数。

>>> import math
>>> notby2or3 = ( n for n in range(1, 20) if math.gcd(n, 2) == 1 and math.gcd(n, 3) == 1 )
>>> print(list(notby2or3))
[1, 5, 7, 11, 13, 17, 19]

您可以看到正确将 if 表达式直接插入生成器表达式的方式。请注意,您还可以在生成器表达式中嵌套 for 表达式。同样,生成器表达式只是生成器函数的紧凑语法,如果需要真正复杂的生成器表达式,最终可能会仅使用生成器函数生成更可读的代码。

您可将生成器对象链接在一起,包括生成器表达式。

>>> notby2or3 = ( n for n in range(1, 20) if math.gcd(n, 2) == 1 and math.gcd(n, 3) == 1 )
>>> squarednotby2or3 = ( n*n for n in notby2or3 )
>>> print(list(squarednotby2or3))
[1, 25, 49, 121, 169, 289, 361]

此类链接的生成器模式既强大又高效。在上述示例中,第一行定义了生成器对象,但未完成其中任何工作。第二行定义了第二个生成器对象,此对象引用第一个对象,但未完成这些对象的任何工作。直到请求完整迭代(在此情况下,由 list 构造函数发出请求),方才完成所有工作。这种根据需要执行迭代工作的构想称为惰性求值,它是使用生成器精心设计的代码的特征之一。但请记住,使用此类生成器和生成器表达式链时,必须由某项实际触发迭代。在本例中,由 list 函数触发。也可由 for 循环触发。设置完各种生成器后忘记触发迭代是一种很容易犯的错误,在这种情况下,您可能冥思苦想也找不到代码不运行的原因。

惰性的价值

在本教程中,您学习了迭代器的基础知识,同时也了解了关于迭代器、生成器函数和表达式的一些最有趣的来源。

当然,即使不使用任何生成器,您也可以编写本教程中的所有代码,但通过学习使用生成器,就思考如何处理任何持续发展的概念或数据而言,提供了更灵活、更高效的方式。同样以我先前的比喻为例,每次按需从商店买一加仑牛奶,显然要比自建仓库储藏一年的供应量更合理。虽然开发人员将此同等方法称为惰性求值,但惰性更多的是表示您获取自己所需内容的时机。每隔一天去一次杂货店似乎算不上懒惰。同样,有时编写使用生成器的代码需要更多的工作量,甚至更加烧脑,但更易于扩展的处理方式也会随之带来诸多优势。

学习使用迭代器和生成器是掌握 Python 的一个重要步骤,而另一个重要步骤就是学习使用标准库中为处理迭代器而提供的众多神奇的工具。这将是本系列中下一个教程的主题。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Big data and analytics, Cloud computing
ArticleID=1062419
ArticleTitle=在 Python 中按需处理数据,第 1 部分: Python 迭代器和生成器
publish-date=07182018