我们很幸运地生活在市场力量将内存,磁盘甚至CPU的价格推至以前难以想象的低点的时候。 但是,与此同时,大数据,人工智能和认知计算等蓬勃发展的应用程序正以令人眼花rate乱的速度推动我们对这些资源的需求。 具有讽刺意味的是,在计算资源丰富的时代,对于开发人员来说,了解如何降低其消耗量以保持竞争力越来越重要。
Python保持如此流行的编程语言近二十年的主要原因是它非常易于学习。 在一个小时内,您可以了解如何轻松地操作列表和词典。 坏消息是,用天真的方法解决列表和词典的许多问题会使您Swift陷入尝试扩展应用程序的麻烦,因为如果不加小心,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
。
现在是时候通过仔细研究范围等迭代器来加深对Python的了解。 在Python解释器中输入以下内容:
r = range(10)
现在,您已经初始化了范围迭代器,仅此而已。 继续询问它的第一个价值。 您可以使用内置的next
函数向迭代器询问Python中的值。
>>> 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!")
它显示消息“这就是所有人!” for
语句使用StopIteration
异常来确定何时退出循环。
其他可迭代
范围只是可以转换为迭代器的一种对象。 以下解释器会话演示了如何将各种标准类型解释为迭代器。
>>> 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语句,或者仅是该函数的最后缩进行,这与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是一个天气模拟器,它打印了一系列晴天或雨天,并附带了一些注释。
如果考虑到天气,通常会在晴天之后是另一个晴天,而在雨天之后通常还会是另一个雨天。 您可以通过随机选择第二天的天气来模拟此情况,但是天气保持不变的可能性更高。 关于天气的一个极有可能发生变化的单词是易变的,并且在此生成器函数中有一个参数,波动率,应在0到1之间。此参数越小,天气从一天到一天保持不变的可能性就越大。天。 在此清单中,波动率设置为0.2,这意味着5个转换中的平均4个应保持不变。
该清单具有附加功能,如果连续超过三个晴天,或者连续三个以上雨天,则会发布一些评论。
清单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!
当然,这是基于随机性,每次天气保持不变,就有五分之四的机会,因此您可以轻松获得:
$ 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!
花一些时间自己动手,首先为volatility
和days
传递不同的值,然后调整生成器函数代码本身。 实验是确保您真正了解发电机工作原理的最佳方法。
我希望这个更有趣的示例可以通过发电机的某些功能激发您的想象力。 您可以肯定地在没有生成器的情况下编写先前的代码,但是这种方法不仅更具表达力,而且通常效率更高,而且还具有能够以其他有趣的方式重用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加仑的牛奶,但是您不想在地下室中建立所有牛奶的存储设施。 相反,由于需要更多牛奶,您一次去商店购买一加仑。 一直使用生成器而不是构建列表有点像使用杂货店而不是自己构建仓库。
也有字典表达式,但是它们不在本教程的讨论范围之内。 请注意,您可以轻松地将生成器表达式转换为列表,有时这可以是一次消耗所有生成器的方式,但是如果您不这样做,它也会通过创建需要大量内存的列表来破坏使用生成器的目的。小心。
>>> 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的GCD是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]
链接生成器的这种模式是强大而高效的。 在前面的示例中,第一行定义了一个generator对象,但是没有完成任何工作。 第二行定义了第二个生成器对象,该对象引用第一个生成器对象,但是这些对象均未完成任何工作。 直到请求完整的迭代(在这种情况下, list
构造函数)才完成所有工作。 这种根据需要进行迭代的工作的想法称为惰性评估,它是使用生成器精心设计的代码的标志之一。 但是,当使用此类生成器和生成器表达式链时,请记住必须实际触发迭代。 在这种情况下,它就是list
功能。 也可能是for
循环。 设置各种生成器然后忘记触发迭代器是一个容易犯的错误,在这种情况下,您最终不得不为代码为什么不执行任何操作而抓狂。
懒惰的价值
在本教程中,您学习了迭代器的基础知识,以及迭代器,生成器函数和表达式的最有趣的来源。
您当然可以在没有任何生成器的情况下编写本教程中的所有代码,但是学习使用生成器将为思考对一系列开发的任何概念或数据进行操作提供了更灵活有效的方法。 重复前面的类比,有意义的是一次按需从商店一次购买一加仑牛奶,而不是自己建立一个有一年供应量的仓库。 尽管开发人员将等效方法称为“懒惰评估”,但是懒惰更多地是在获取所需内容的时间上。 每隔一天去杂货店似乎并不那么懒。 同样,有时编写代码以使用生成器可能会花费更多的工作,甚至可能会有些弯腰,但好处是可扩展的处理能力。
学习迭代器和生成器是精通Python的重要一步,而另一步则是学习标准库中提供的许多令人惊奇的工具来处理迭代器。 这将是本系列下一个教程的主题。
翻译自: https://www.ibm.com/developerworks/library/ba-on-demand-data-python-1/index.html