我们已经讨论了 Python 的许多内置代码和习惯用法,乍一看,似乎与面向对象的原则背道而驰,但实际上提供了对真实对象的访问。在本章中,我们将讨论for循环是如何围绕一组面向对象的原则进行轻量级包装的,它看起来是如此结构化。我们还将看到该语法的各种扩展,它们可以自动创建更多类型的对象。我们将讨论以下主题:
- 什么是设计模式
- 迭代器协议是最强大的设计模式之一
- 列表、集合和词典理解
- 生成器和协同程序
当工程师和建筑师决定建造桥梁、塔或建筑物时,他们会遵循一定的原则来确保结构的完整性。桥梁有各种可能的设计(例如,悬索和悬臂),但如果工程师不使用其中一种标准设计,并且没有出色的新设计,那么他/她设计的桥梁很可能会倒塌。
设计模式是将正确设计的结构的相同形式定义引入软件工程的一种尝试。有许多不同的设计模式来解决不同的一般问题。设计模式通常解决开发人员在某些特定情况下面临的特定常见问题。然后,设计模式是关于该问题的理想解决方案的建议,即面向对象设计。
然而,了解设计模式并选择在我们的软件中使用它并不能保证我们正在创建一个正确的解决方案。1907 年,魁北克大桥(迄今为止,世界上最长的悬臂桥)在完工前倒塌,因为设计它的工程师严重低估了建造它所用的钢材的重量。类似地,在软件开发中,我们可能会错误地选择或应用设计模式,并创建在正常操作情况下或在压力超过其原始设计极限时崩溃的软件。
任何一种设计模式都会提出一组以特定方式交互的对象来解决一般问题。程序员的工作是识别他们何时面临此类问题的特定版本,然后根据他们的精确需求选择和调整通用设计。
在本章中,我们将介绍迭代器设计模式。该模式功能强大且无处不在,因此 Python 开发人员提供了多种语法来访问该模式下的面向对象原则。在接下来的两章中,我们将介绍其他设计模式。它们中的一些具有语言支持,而另一些则没有,但它们中没有一个像迭代器模式那样本质上是 Python 程序员日常生活的一部分。
在典型的设计模式术语中,迭代器是一个具有next()方法和done()方法的对象;如果序列中没有剩余项,则返回True。在没有内置迭代器支持的编程语言中,迭代器的循环如下:
while not iterator.done():
item = iterator.next()
# do something with the item 在 Python 中,迭代是一个特殊的特性,因此该方法得到一个特殊的名称,__next__。可使用next(iterator)内置按钮访问此方法。Python 的迭代器协议不是一个done方法,而是引发StopIteration来通知循环它已经完成。最后,我们有了更具可读性的foriteminiterator语法来实际访问迭代器中的项,而不是在while循环中乱搞。让我们更详细地看看这些。
collections.abc模块中的Iterator抽象基类用 Python 定义迭代器协议。如前所述,它必须有一个__next__方法,for循环(和其他支持迭代的特性)可以调用该方法从序列中获取新元素。此外,每个迭代器还必须实现Iterable接口。任何提供__iter__方法的类都是可移植的。该方法必须返回一个Iterator实例,该实例将覆盖该类中的所有元素。
这听起来可能有点让人困惑,所以请看一下下面的示例,但请注意,这是一种非常详细的解决此问题的方法。它清楚地解释了迭代和讨论中的两个协议,但我们将在本章后面讨论几种更具可读性的方法来获得这种效果:
class CapitalIterable:
def __init__(self, string):
self.string = string
def __iter__(self):
return CapitalIterator(self.string)
class CapitalIterator:
def __init__(self, string):
self.words = [w.capitalize() for w in string.split()]
self.index = 0
def __next__(self):
if self.index == len(self.words):
raise StopIteration()
word = self.words[self.index]
self.index += 1
return word
def __iter__(self):
return self 本例定义了一个CapitalIterable类,其任务是循环字符串中的每个单词,并以大写的第一个字母输出它们。iterable 的大部分工作都传递给了CapitalIterator实现。与此迭代器交互的规范方式如下:
>>> iterable = CapitalIterable('the quick brown fox jumps over the lazy dog')
>>> iterator = iter(iterable)
>>> while True:
... try:
... print(next(iterator))
... except StopIteration:
... break
...
The
Quick
Brown
Fox
Jumps
Over
The
Lazy
Dog 本例首先构造一个 iterable 并从中检索迭代器。这种区别可能需要解释;iterable 是一个包含可以循环的元素的对象。通常,这些元素可以循环多次,甚至可能在同一时间或在重叠的代码中循环。另一方面,迭代器表示该 iterable 中的特定位置;有些物品已经被消费,有些还没有。两个不同的迭代器可能位于单词列表中的不同位置,但任何一个迭代器只能标记一个位置。
每次在迭代器上调用next()时,它都会按顺序从 iterable 返回另一个标记。最终,迭代器将被耗尽(不会有更多的元素返回),在这种情况下,Stopiteration被引发,我们中断循环。
当然,我们已经知道了从 iterable 构造迭代器的一种简单得多的语法:
>>> for i in iterable:
... print(i)
...
The
Quick
Brown
Fox
Jumps
Over
The
Lazy
Dog 正如您所看到的,for语句,尽管看起来不太面向对象,但实际上是一些明显面向对象设计原则的捷径。在我们讨论理解时,请记住这一点,因为它们似乎也是面向对象工具的对立物。然而,它们使用与for循环完全相同的迭代协议,只是另一种快捷方式。
理解是简单但强大的语法,它允许我们在一行代码中转换或过滤一个 iterable 对象。结果对象可以是一个完全正常的列表、集合或字典,也可以是一个生成器表达式,它可以高效地使用,同时一次只在内存中保留一个元素。
列表理解是 Python 中最强大的工具之一,因此人们倾向于将其视为高级工具。他们不是。事实上,我冒昧地把前面的例子胡乱乱扔,假设你能理解它们。虽然高级程序员确实经常使用理解,但这并不是因为他们是高级的。这是因为它们很琐碎,可以处理软件开发中最常见的一些操作。
让我们看看其中一个常见的操作;即,将项目列表转换为相关项目列表。具体来说,假设我们只是从文件中读取字符串列表,现在我们想将其转换为整数列表。我们知道列表中的每个项目都是一个整数,我们想对这些数字进行一些活动(比如,计算平均值)。这里有一个简单的方法:
input_strings = ["1", "5", "28", "131", "3"]
output_integers = []
for num in input_strings:
output_integers.append(int(num)) 这很好,只需要三行代码。如果你不习惯理解,你甚至不会认为它看起来很丑!现在,使用列表了解相同的代码:
input_strings = ["1", "5", "28", "131", "3"]
output_integers = [int(num) for num in input_strings] 我们只剩下一行,重要的是为了性能,我们已经为列表中的每个项目删除了一个append方法调用。总的来说,即使您不习惯理解语法,也很容易判断发生了什么。
和往常一样,方括号表示我们正在创建一个列表。在这个列表中有一个for循环,它迭代输入序列中的每个项目。唯一可能让人困惑的是列表的开始括号和for循环开始之间发生了什么。此处发生的任何情况都将应用于输入列表中的每个项。有问题的项由循环中的num变量引用。因此,它为每个元素调用int函数,并将生成的整数存储在新列表中。
这就是基本列表理解的全部内容。理解是高度优化的 C 代码;当在大量项目上循环时,列表理解比for循环快得多。如果仅仅可读性还不能成为尽可能多地使用它们的令人信服的理由,那么速度应该是。
将一个项目列表转换为一个相关列表并不是我们使用列表理解所能做的唯一事情。我们也可以通过在理解中添加if语句来排除某些值。看看:
output_integers = [int(num) for num in input_strings if len(num) < 3]这个例子和上一个例子的不同之处在于if len(num) < 3部分。此额外代码不包括任何超过两个字符的字符串。if语句应用于函数之前的每个元素**,因此它在测试字符串的长度。因为我们的输入字符串本质上都是整数,所以它排除了超过 99 的任何数字。**
列表理解用于将输入值映射到输出值,在此过程中应用过滤器以包括或排除满足特定条件的任何值。
任何 iterable 都可以作为列表的输入。换言之,我们可以在for循环中包装的任何东西也可以放在理解中。例如,文本文件是可编辑的;在文件的迭代器上对__next__的每次调用都将返回文件的一行。我们可以使用zip函数将第一行为标题行的制表符分隔文件加载到字典中:
import sys
filename = sys.argv[1]
with open(filename) as file:
header = file.readline().strip().split("\t")
contacts = [
dict(
zip(header, line.strip().split("\t")))
for line in file
]
for contact in contacts:
print("email: {email} -- {last}, {first}".format(**contact))这一次,我添加了一些空白以使其更具可读性(列表理解不需要将放在一行中)。此示例从压缩的标题创建字典列表,并为文件中的每一行拆分行。
呃,什么??如果代码或解释没有意义,不要担心;这令人困惑。一个列表理解是在这里做大量的工作,代码很难理解、阅读,最终也很难维护。这个例子表明,列表理解并不总是最好的解决方案;大多数程序员都同意for循环比这个版本更具可读性。
Remember: the tools we are provided with should not be abused! Always pick the right tool for the job, which is always to write maintainable code.
理解并不局限于列表。我们也可以使用类似的语法和大括号来创建集合和字典。让我们从布景开始。创建集合的一种方法是在set()构造函数中包装一个列表,将其转换为集合。但是,当我们可以直接创建一个集合时,为什么要在一个被丢弃的中间列表上浪费内存呢?
下面是一个使用命名元组对 author/title/genre 三元组进行建模的示例,然后检索一组以特定类型编写的所有作者:
from collections import namedtuple
Book = namedtuple("Book", "author title genre")
books = [
Book("Pratchett", "Nightwatch", "fantasy"),
Book("Pratchett", "Thief Of Time", "fantasy"),
Book("Le Guin", "The Dispossessed", "scifi"),
Book("Le Guin", "A Wizard Of Earthsea", "fantasy"),
Book("Turner", "The Thief", "fantasy"),
Book("Phillips", "Preston Diamond", "western"),
Book("Phillips", "Twice Upon A Time", "scifi"),
]
fantasy_authors = {b.author for b in books if b.genre == "fantasy"}与演示数据设置相比,突出显示的集合较短!当然,如果我们使用列表理解,Terry Pratchett 会被列出两次。事实上,集合的性质删除了重复项,我们最终得到以下结果:
>>> fantasy_authors
{'Turner', 'Pratchett', 'Le Guin'} 仍然使用大括号,我们可以引入冒号来创建字典。这将使用键:值对将序列转换为字典。例如,如果我们知道书名,在字典中快速查找作者或体裁可能会很有用。我们可以使用字典理解将标题映射到books对象:
fantasy_titles = {b.title: b for b in books if b.genre == "fantasy"}现在,我们有了一本字典,可以使用正常语法按书名查找书籍。
总之,理解不是高级 Python,也不是应该避免的非面向对象工具。它们只是从现有序列创建列表、集合或字典的更简洁和优化的语法。
有时,我们希望处理一个新序列,而不将新列表、集合或字典拉入系统内存。如果我们只是一次循环一个项目,而实际上并不关心创建一个完整的容器(如列表或字典),那么创建该容器就是浪费内存。当一次处理一个项目时,我们只需要内存中随时可用的当前对象。但是,当我们创建一个容器时,所有的对象都必须存储在该容器中,然后才能开始处理它们。
例如,考虑一个处理日志文件的程序。非常简单的日志可能包含以下格式的信息:
Jan 26, 2015 11:25:25 DEBUG This is a debugging message. Jan 26, 2015 11:25:36 INFO This is an information method. Jan 26, 2015 11:25:46 WARNING This is a warning. It could be serious. Jan 26, 2015 11:25:52 WARNING Another warning sent. Jan 26, 2015 11:25:59 INFO Here's some information. Jan 26, 2015 11:26:13 DEBUG Debug messages are only useful if you want to figure something out. Jan 26, 2015 11:26:32 INFO Information is usually harmless, but helpful. Jan 26, 2015 11:26:40 WARNING Warnings should be heeded. Jan 26, 2015 11:26:54 WARNING Watch for warnings. 流行的 web 服务器、数据库或电子邮件服务器的日志文件可能包含许多千兆字节的数据(我曾经不得不从一个不正常的系统中清除近 2 TB 的日志)。如果我们想处理日志中的每一行,我们不能使用列表理解;它将创建一个包含文件中每一行的列表。这可能不适合 RAM,可能会使计算机崩溃,具体取决于操作系统。
如果我们在日志文件上使用for循环,我们可以一次处理一行,然后将下一行读入内存。如果我们可以使用理解语法来获得同样的效果,那不是很好吗?
这就是生成器表达式的用武之地。它们使用与理解相同的语法,但不创建最终的容器对象。要创建生成器表达式,请将理解内容包装在()中,而不是[]或{}。
下面的代码以前面给出的格式解析日志文件,并输出一个只包含WARNING行的新日志文件:
import sys
inname = sys.argv[1]
outname = sys.argv[2]
with open(inname) as infile:
with open(outname, "w") as outfile:
warnings = (l for l in infile if 'WARNING' in l)
for l in warnings:
outfile.write(l) 该程序获取命令行上的两个文件名,使用生成器表达式过滤掉警告(在本例中,它使用if语法,不修改该行),然后将警告输出到另一个文件。如果在示例文件上运行它,输出如下所示:
Jan 26, 2015 11:25:46 WARNING This is a warning. It could be serious.
Jan 26, 2015 11:25:52 WARNING Another warning sent.
Jan 26, 2015 11:26:40 WARNING Warnings should be heeded.
Jan 26, 2015 11:26:54 WARNING Watch for warnings. 当然,对于如此短的输入文件,我们可以安全地使用列表理解,但是如果文件有数百万行长,生成器表达式将对内存和速度产生巨大影响。
Wrapping a for expression in parenthesis creates a generator expression, not a tuple.
生成器表达式通常是函数调用中最有用的。例如,我们可以对生成器表达式而不是列表调用sum、min或max,因为这些函数一次处理一个对象。我们只对聚合结果感兴趣,而不是任何中间容器。
通常,在这四个选项中,应尽可能使用生成器表达式。如果我们实际上不需要列表、集合或字典,只需要过滤或转换序列中的项,那么生成器表达式将是最有效的。如果我们需要知道一个列表的长度,或者对结果进行排序,删除重复项,或者创建一个字典,我们就必须使用理解语法。
生成器表达式实际上也是一种理解;它们将更高级的生成器语法压缩到一行中(这次它确实更高级!)。更强大的生成器语法看起来比我们见过的任何东西都更不面向对象,但我们将再次发现,它是创建一种对象的简单语法快捷方式。
让我们进一步了解一下日志文件示例。如果我们想从输出文件中删除WARNING列(因为它是多余的:这个文件只包含警告),我们有几个不同可读性级别的选项。我们可以使用生成器表达式执行此操作:
import sys
# generator expression
inname, outname = sys.argv[1:3]
with open(inname) as infile:
with open(outname, "w") as outfile:
warnings = (
l.replace("\tWARNING", "") for l in infile if "WARNING" in l
)
for l in warnings:
outfile.write(l)这是完全可读的,尽管我不想让表达式比这复杂得多。我们也可以使用正常的for循环:
with open(inname) as infile:
with open(outname, "w") as outfile:
for l in infile:
if "WARNING" in l:
outfile.write(l.replace("\tWARNING", ""))这显然是可以维护的,但是在这么少的行中有这么多级别的缩进有点难看。更令人担忧的是,如果我们不想打印行,我们还必须复制循环和条件代码。
现在让我们考虑一个真正的面向对象的解决方案,没有任何捷径:
class WarningFilter:
def __init__(self, insequence):
self.insequence = insequence
def __iter__(self):
return self
def __next__(self):
l = self.insequence.readline()
while l and "WARNING" not in l:
l = self.insequence.readline()
if not l:
raise StopIteration
return l.replace("\tWARNING", "")
with open(inname) as infile:
with open(outname, "w") as outfile:
filter = WarningFilter(infile)
for l in filter:
outfile.write(l)毫无疑问:这是如此丑陋和难以阅读,以至于你甚至可能无法说出发生了什么。我们创建了一个以文件对象作为输入的对象,并提供了一个类似于任何迭代器的__next__方法。
此__next__方法从文件中读取行,如果它们不是WARNING行,则丢弃它们。当我们遇到一个WARNING行时,我们修改并返回它。然后我们的for循环再次调用__next__来处理后续的WARNING行。当我们的行数用完时,我们启动StopIteration来告诉循环我们已经完成了迭代。与其他例子相比,它相当丑陋,但它也很强大;现在我们手中有一门课,我们可以用它做任何我们想做的事情。
有了这样的背景,我们终于看到了真正的发电机在运行。下一个示例的作用与上一个示例的作用完全相同:它使用一个__next__方法创建一个对象,在输入不足时引发StopIteration:
def warnings_filter(insequence):
for l in insequence:
if "WARNING" in l:
yield l.replace("\tWARNING", "")
with open(inname) as infile:
with open(outname, "w") as outfile:
filter = warnings_filter(infile)
for l in filter:
outfile.write(l)好的,这很容易理解,也许。。。至少它很短。但这里到底发生了什么?这毫无意义。那么yield到底是什么?
事实上,yield是发电机的关键。当 Python 在一个函数中看到yield时,它会接受该函数并将其封装到一个对象中,这与我们前面的示例中的对象没有什么不同。把yield语句想象成与return语句相似的语句;它退出函数并返回一行。但是,与return不同的是,当再次调用该函数时(通过next()调用),它将从yield语句后的行中断处开始,而不是在函数的开头。
在本例中,yield语句之后没有第行,因此它跳转到for循环的下一次迭代。由于yield语句位于if语句中,因此它只生成包含WARNING的行。
虽然看起来这只是一个在各行上循环的函数,但实际上它正在创建一种特殊类型的对象,一个生成器对象:
>>> print(warnings_filter([]))
<generator object warnings_filter at 0xb728c6bc> 我将一个空列表传递到函数中作为迭代器。该函数所做的只是创建并返回一个生成器对象。该对象上有__iter__和__next__方法,就像我们在上一个示例中创建的一样。(您可以调用其上的dir内置函数进行确认。)每当调用__next__时,生成器都会运行该函数,直到找到yield语句。然后它返回来自yield的值,下次调用__next__时,它将从停止的地方开始。
生成器的这种使用并没有那么先进,但是如果您没有意识到函数正在创建一个对象,那么它可能看起来很神奇。这个例子很简单,但是你可以通过在一个函数中多次调用yield来获得非常强大的效果;在每个回路上,发电机只需在最近的yield处拾取并继续下一个回路。
通常,当我们构建一个生成器函数时,我们最终会遇到这样的情况:我们希望从另一个 iterable 对象生成数据,可能是在生成器内部构造的列表理解或生成器表达式,也可能是传递到函数中的一些外部项。通过在 iterable 上循环并单独生成每个项目,这始终是可能的。然而,在 Python 版本 3.3 中,Python 开发人员引入了一种新语法,使其更加优雅。
让我们稍微调整一下生成器示例,以便它不接受一系列行,而是接受一个文件名。这通常是不赞成的,因为它将对象与特定的范例联系起来。如果可能,我们应该将迭代器作为输入进行操作;这样,无论日志行来自文件、内存还是 web,都可以使用相同的函数。
此版本的代码说明,生成器可以在从另一个 iterable(在本例中为生成器表达式)生成信息之前进行一些基本设置:
def warnings_filter(infilename):
with open(infilename) as infile:
yield from (
l.replace("\tWARNING", "") for l in infile if "WARNING" in l
)
filter = warnings_filter(inname)
with open(outname, "w") as outfile:
for l in filter:
outfile.write(l)此代码将上一示例中的for循环组合到生成器表达式中。请注意,这种转换没有任何帮助;上一个带有for循环的示例更具可读性。
所以,让我们考虑一个比它的替代品更可读的例子。构造从多个其他生成器生成数据的生成器非常有用。例如,itertools.chain函数按顺序从 iterables 生成数据,直到它们全部用完。这可以非常容易地使用 OLE T1 语法,所以让我们考虑一个经典的计算机科学问题:走一般的树。
通用树数据结构的一个常见实现是计算机的文件系统。让我们在 Unix 文件系统中对几个文件夹和文件进行建模,以便我们可以使用yield from有效地对它们进行遍历:
class File:
def __init__(self, name):
self.name = name
class Folder(File):
def __init__(self, name):
super().__init__(name)
self.children = []
root = Folder("")
etc = Folder("etc")
root.children.append(etc)
etc.children.append(File("passwd"))
etc.children.append(File("groups"))
httpd = Folder("httpd")
etc.children.append(httpd)
httpd.children.append(File("http.conf"))
var = Folder("var")
root.children.append(var)
log = Folder("log")
var.children.append(log)
log.children.append(File("messages"))
log.children.append(File("kernel"))这个设置代码看起来需要做很多工作,但是在一个真正的文件系统中,它将更加复杂。我们必须从硬盘读取数据并将其组织到树中。然而,一旦进入内存,输出文件系统中每个文件的代码就相当优雅:
def walk(file):
if isinstance(file, Folder):
yield file.name + "/"
for f in file.children:
yield from walk(f)
else:
yield file.name如果这段代码遇到一个目录,它会递归地要求walk()生成一个从属于每个子目录的所有文件的列表,然后生成所有这些数据加上它自己的文件名。在遇到普通文件的简单情况下,它只生成该名称。
另一方面,在不使用生成器的情况下解决上述问题非常棘手,这是一个常见的面试问题。如果你这样回答,你要准备好让你的面试官对你如此轻松的回答既印象深刻又有些恼火。他们可能会要求你准确解释发生了什么。当然,有了本章学到的原则,你不会有任何问题。祝你好运
在编写链式生成器时,yield from语法是一个有用的快捷方式。它被添加到语言中的另一个原因是为了支持协同程序。然而,它已经不再经常使用了,因为它的用法已经被async和await语法所取代。我们将在下一节中看到这两个方面的示例。
协同程序是非常强大的构造,经常与生成器混淆。许多作者不恰当地将协程描述为带有一点额外语法的生成器。这是一个容易犯的错误,因为早在 Python2.5 中,当引入协同路由时,它们被表示为*,我们在生成器语法*中添加了send方法。事实上,这种差别要微妙得多,在你看过几个例子后会更有意义。
协同程序很难理解。在asyncio模块之外,它们在野外并不经常使用。您完全可以跳过这一部分,在 Python 中愉快地开发多年,而不会遇到协同程序。有两个库广泛使用协同路由(主要用于并发或异步编程),但它们通常是这样编写的,这样您就可以在不了解协同路由工作原理的情况下使用协同路由!所以,如果你在这一部分迷路了,不要绝望。
如果我没有吓跑你,我们开始吧!下面是一个最简单的协同程序;它允许我们保持可通过任意值增加的运行计数:
def tally():
score = 0
while True:
increment = yield score
score += increment 这段代码看起来像是不可能起作用的黑魔法,所以在逐行描述之前,让我们先证明它起作用。这个简单的对象可以被棒球队的计分应用程序使用。每支球队都可以进行单独的计分,他们的得分可以随着每半局结束时累积的得分而增加。看看这个互动环节:
>>> white_sox = tally()
>>> blue_jays = tally()
>>> next(white_sox)
0
>>> next(blue_jays)
0
>>> white_sox.send(3)
3
>>> blue_jays.send(2)
2
>>> white_sox.send(2)
5
>>> blue_jays.send(4)
6 首先,我们构建两个tally对象,每个团队一个。是的,它们看起来像函数,但与上一节中的生成器对象一样,函数中有一个yield语句,这一事实告诉 Python 要花大量精力将简单函数转化为对象。
然后,我们对每个 coroutine 对象调用next()。这与在任何生成器上调用 next 的操作相同,也就是说,它执行每行代码,直到遇到一个yield语句,在该点返回值,然后暂停直到下一个next()调用。
到目前为止,没有什么新鲜事。但回顾一下我们合作计划中的yield声明:
increment = yield score 与生成器不同,这个yield函数看起来应该返回一个值并将其分配给变量。事实上,这正是正在发生的事情。协同程序仍在yield语句处暂停,并等待通过另一个对next()的调用再次激活。
除非我们不打电话给next()。正如您在交互式会话中看到的,我们调用了一个名为send()的方法。send()方法的功能与next()完全相同,只是除了将生成器推进到下一个yield语句之外,它还允许您从生成器外部传入一个值。该值是分配给yield语句左侧的值。
对许多人来说,真正令人困惑的是发生的顺序:
yield发生,发电机暂停send()从功能外部发生,发电机唤醒- 发送的值被分配到
yield语句的左侧 - 生成器继续处理,直到遇到另一条
yield语句
因此,在这个特定的示例中,在构造协程并通过对next()的单个调用将其推进到yield语句之后,对send()的每个后续调用都会将一个值传递到协程中。我们将此值添加到其分数中。然后我们返回到while循环的顶部,继续处理,直到找到yield语句。yield语句返回一个值,该值成为我们最近调用send的返回值。不要错过:就像next()一样,send()方法不仅向生成器提交一个值,它还返回即将到来的yield语句中的值。这就是我们如何定义生成器和协同程序之间的区别:生成器只生成值,而协同程序也可以使用它们。
The behavior and syntax of next(i), i.__next__(), and i.send(value) are rather unintuitive and frustrating. The first is a normal function, the second is a special method, and the last is a normal method. But all three do the same thing: advance the generator until it yields a value and pause. Further, the next() function and associated method can be replicated by calling i.send(None). There is value to having two different method names here, since it helps the reader of our code easily see whether they are interacting with a coroutine or a generator. I just find the fact that in one case it's a function call and in the other it's a normal method somewhat irritating.
当然,前面的示例可以很容易地使用两个整数变量进行编码,并调用它们x += increment。让我们看第二个例子,协同程序实际上为我们节省了一些代码。这个例子是我在 Facebook 工作时必须解决的一个问题的简化版(出于教学原因)。
Linux 内核日志包含的行看起来几乎是,但不是全部,与此不同:
unrelated log messages
sd 0:0:0:0 Attached Disk Drive
unrelated log messages
sd 0:0:0:0 (SERIAL=ZZ12345)
unrelated log messages
sd 0:0:0:0 [sda] Options
unrelated log messages
XFS ERROR [sda]
unrelated log messages
sd 2:0:0:1 Attached Disk Drive
unrelated log messages
sd 2:0:0:1 (SERIAL=ZZ67890)
unrelated log messages
sd 2:0:0:1 [sdb] Options
unrelated log messages
sd 3:0:1:8 Attached Disk Drive
unrelated log messages
sd 3:0:1:8 (SERIAL=WW11111)
unrelated log messages
sd 3:0:1:8 [sdc] Options
unrelated log messages
XFS ERROR [sdc]
unrelated log messages 有一大堆分散的内核日志消息,其中一些与硬盘有关。硬盘消息可能与其他消息混杂在一起,但它们以可预测的格式和顺序出现。对于每个驱动器,具有已知序列号的特定驱动器与总线标识符相关联(例如0:0:0:0。块设备标识符(如sda)也与该总线相关联。最后,如果驱动器有一个损坏的文件系统,它可能会因 XFS 错误而失败。
现在,给定前面的日志文件,我们需要解决的问题是如何获取任何有 XFS 错误的驱动器的序列号。数据中心技术人员稍后可能会使用此序列号来识别和更换驱动器。
我们知道我们可以使用正则表达式来识别单独的行,但是我们必须在循环行时更改正则表达式,因为我们将根据之前发现的内容查找不同的内容。另一个困难的地方是,如果我们发现一个错误字符串,关于哪个总线包含该字符串以及序列号的信息已经被处理。这可以通过以相反顺序迭代文件的行来轻松解决。
在查看此示例之前,请注意,基于协同路由的解决方案所需的代码量非常少:
import re
def match_regex(filename, regex):
with open(filename) as file:
lines = file.readlines()
for line in reversed(lines):
match = re.match(regex, line)
if match:
regex = yield match.groups()[0]
def get_serials(filename):
ERROR_RE = "XFS ERROR (\[sd[a-z]\])"
matcher = match_regex(filename, ERROR_RE)
device = next(matcher)
while True:
try:
bus = matcher.send(
"(sd \S+) {}.*".format(re.escape(device))
)
serial = matcher.send("{} \(SERIAL=([^)]*)\)".format(bus))
yield serial
device = matcher.send(ERROR_RE)
except StopIteration:
matcher.close()
return
for serial_number in get_serials("EXAMPLE_LOG.log"):
print(serial_number)此代码将作业整齐地划分为两个单独的任务。第一个任务是循环所有行,并吐出与给定正则表达式匹配的任何行。第二个任务是与第一个任务交互,并指导它在任何给定时间搜索什么样的正则表达式。
先看match_regex合作项目。记住,它在构造时不执行任何代码;相反,它只是创建一个协同路由对象。一旦构建完成,协同程序之外的人最终将调用next()来启动代码运行。然后存储两个变量filename和regex的状态。然后它读取文件中的所有行,并反向迭代它们。将每一行与传入的正则表达式进行比较,直到找到匹配项。当找到匹配项时,协程从正则表达式中产生第一个组并等待。
在将来的某个时候,其他代码将发送一个新的正则表达式来搜索。注意,协程从不关心它试图匹配的正则表达式;它只是在行上循环,并将它们与正则表达式进行比较。其他人有责任决定提供什么样的正则表达式。
在本例中,其他人是get_serials生成器。它不关心文件中的行;事实上,它甚至没有意识到它们。它要做的第一件事是从match_regex协程构造函数创建一个matcher对象,给它一个要搜索的默认正则表达式。它将协程推进到它的第一个yield并存储它返回的值。然后它进入一个循环,指示matcher对象根据存储的设备 ID 搜索总线 ID,然后根据该总线 ID 搜索序列号。
在指示匹配器找到另一个设备 ID 并重复该循环之前,它会将该序列号空闲地传递给外部for循环。
基本上,协同程序的任务是搜索文件中的下一个重要行,而生成器(get_serial,使用yield语法而不赋值)的任务是确定哪一行是重要的。生成器具有有关此特定问题的信息,例如文件中将显示哪些订单行。
另一方面,协同程序可以插入任何需要在文件中搜索给定正则表达式的问题。
正常发电机通过提升StopIteration从内部发出退出信号。如果我们将多个生成器链接在一起(例如,通过从一个生成器内部迭代另一个生成器),那么StopIteration异常将向外传播。最终,它将命中一个for循环,该循环将看到异常并知道是时候退出该循环了。
即使它们使用类似的语法,协同程序通常也不会遵循迭代机制。通常将数据推入其中(使用send),而不是在遇到异常之前将数据拉入其中。执行推送的实体通常是负责告知协同程序何时完成的实体。它通过调用相关协同路由上的close()方法来实现这一点。
调用时,close()方法将在协程等待发送值时引发GeneratorExit异常。通常情况下,将其yield语句封装在try中是一个很好的策略。。。finally块,以便执行任何清理任务(如关闭关联文件或套接字)。
如果我们需要在协同程序中引发异常,我们可以以类似的方式使用throw()方法。它接受带有可选的value和traceback参数的异常类型。当我们在一个协同程序中遇到异常,并希望在保持回溯的同时在相邻的协同程序中发生异常时,后者非常有用。
The previous example could be written without coroutines and would be about equally readable. The truth is, correctly managing all the state between coroutines is pretty difficult, especially when you take things like context managers and exceptions into account. Luckily, the Python standard library contains a package called asyncio that can manage all of this for you. In general, I recommend you avoid using bare coroutines unless you are specifically coding for asyncio. The logging example could almost be considered an anti-pattern; a design pattern that should be avoided rather than embraced.
我们已经看到协同路由在起作用,所以现在让我们回到讨论它们与生成器的关系。在 Python 中,就像经常发生的情况一样,这种区别非常模糊。事实上,所有协同程序都是生成器对象,作者经常交替使用这两个术语。有时,他们将协同路由描述为生成器的子集(只有从 yield 返回值的生成器才被视为协同路由)。在 Python 中,这在技术上是正确的,正如我们在前面的部分中所看到的。
然而,在理论计算机科学的更大范围内,协程被认为是更一般的原则,而生成器是一种特定类型的协程。此外,正规函数是协程的另一个独特子集。
协同程序是一个例程,它可以在一个或多个点上传入数据,并在一个或多个点上导出数据。在 Python 中,数据传入和传出的点是yield语句。
函数或子例程是最简单的协同例程类型。当函数返回时,可以在一点传入数据,在另一点传出数据。虽然一个函数可以有多个return语句,但对于该函数的任何给定调用,只能调用其中一个语句。
最后,生成器是一种协同程序,可以在一个点上传入数据,但可以在多个点上传出数据。在 Python 中,数据将在一个yield语句中传递出去,但您不能将数据传递回去。如果您调用send,数据将被悄悄地丢弃。
因此,在理论上,生成器是协程的类型,函数是协程的类型,有些协程既不是函数也不是生成器。这很简单,是吗?那么,为什么在 Python 中感觉更复杂呢?
在 Python 中,生成器和协同程序都是使用一种语法构造的,看起来就像我们在构造一个函数。但结果对象根本不是一个函数;这是一种完全不同的物体。当然,函数也是对象。但它们有不同的界面;函数是可调用和返回值,生成器使用next()拉出数据,协同程序使用send推入数据。
There is an alternate syntax for coroutines using the async and await keywords. The syntax makes it clearer that the code is a coroutine and further breaks the deceiving symmetry between coroutines and generators.
如今 Python 最受欢迎的领域之一是数据科学。为了纪念这个事实,让我们实现一个基本的机器学习算法。
机器学习是一个庞大的主题,但总体思路是利用从过去数据中获得的知识对未来数据进行预测或分类。这种算法的使用比比皆是,数据科学家正在寻找每天应用机器学习的新方法。一些重要的机器学习应用包括计算机视觉(如图像分类或面部识别)、产品推荐、识别垃圾邮件和自动驾驶汽车。
为了不离题到机器学习的整本书中,我们将看一个更简单的问题:给定 RGB 颜色定义,人类将识别该颜色的名称是什么?
在标准 RGB 颜色空间中有超过 1600 万种颜色,人类只为其中的一小部分命名。虽然有数千个名称(有些非常可笑;只要去任何一家汽车经销商或油漆店就可以了),但让我们构建一个分类器,尝试将 RGB 空间划分为基本颜色:
- 红色
- 紫色
- 蓝色
- 绿色
- 黄的
- 橙色
- 灰色
- 粉红色
(在我的测试中,我将白色和黑色分为灰色,棕色分为橙色。)
我们首先需要一个数据集来训练我们的算法。在一个制作系统中,你可能会从一个颜色列表网站上抓取颜色,或者调查数千人。相反,我创建了一个简单的应用程序来呈现随机颜色,并要求用户从前面的八个选项中选择一个进行分类。我使用 Python 附带的用户界面工具包tkinter实现了它。我不打算详细介绍这个脚本的功能,但为了完整起见,这里是完整的脚本(它有点长,所以您可能希望从 Packt 的 GitHub 存储库中提取它,并提供本书的示例,而不是键入它):
import random
import tkinter as tk
import csv
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(sticky="news")
master.columnconfigure(0, weight=1)
master.rowconfigure(0, weight=1)
self.create_widgets()
self.file = csv.writer(open("colors.csv", "a"))
def create_color_button(self, label, column, row):
button = tk.Button(
self, command=lambda: self.click_color(label), text=label
)
button.grid(column=column, row=row, sticky="news")
def random_color(self):
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
return f"#{r:02x}{g:02x}{b:02x}"
def create_widgets(self):
self.color_box = tk.Label(
self, bg=self.random_color(), width="30", height="15"
)
self.color_box.grid(
column=0, columnspan=2, row=0, sticky="news"
)
self.create_color_button("Red", 0, 1)
self.create_color_button("Purple", 1, 1)
self.create_color_button("Blue", 0, 2)
self.create_color_button("Green", 1, 2)
self.create_color_button("Yellow", 0, 3)
self.create_color_button("Orange", 1, 3)
self.create_color_button("Pink", 0, 4)
self.create_color_button("Grey", 1, 4)
self.quit = tk.Button(
self, text="Quit", command=root.destroy, bg="#ffaabb"
)
self.quit.grid(column=0, row=5, columnspan=2, sticky="news")
def click_color(self, label):
self.file.writerow([label, self.color_box["bg"]])
self.color_box["bg"] = self.random_color()
root = tk.Tk()
app = Application(master=root)
app.mainloop()You can easily add more buttons for other colors if you like. You may get tripped up on the layout; the second and third argument to create_color_button represent the row and column of a two column grid that the button goes in. Once you have all your colors in place, you will want to move the Quit button to the last row.
出于本案例研究的目的,了解此应用程序的重要内容是输出。创建一个名为colors.csv的C****omma 分隔值(CSV文件)。该文件包含两个 CSV:用户指定给颜色的标签和颜色的十六进制 RGB 值。下面是一个例子:
Green,#6edd13
Purple,#814faf
Yellow,#c7c26d
Orange,#61442c
Green,#67f496
Purple,#c757d5
Blue,#106a98
Pink,#d40491
.
.
.
Blue,#a4bdfa
Green,#30882f
Pink,#f47aad
Green,#83ddb2
Grey,#baaec9
Grey,#8aa28d
Blue,#533eda在我感到厌烦之前,我做了 250 多个数据点,并决定是时候开始在我的数据集上进行机器学习了。如果您愿意使用,我的数据点附带了本章的示例(从来没有人告诉我我是色盲,所以这应该是合理的)。
我们将实现一种更简单的机器学习算法,称为k-最近邻。该算法依赖于数据集中点之间的某种距离计算(在我们的例子中,我们可以使用毕达哥拉斯定理的三维版本)。给定一个新的数据点,它会找到一定数量的数据点(称为k,即k中的k在通过该距离计算进行测量时最接近它的数据点。然后它以某种方式组合这些数据点(平均值可能适用于线性计算;对于我们的分类问题,我们将使用模式),并返回结果。
我们不会详细讨论算法的功能;相反,我们将重点介绍一些可以将迭代器模式或迭代器协议应用于此问题的方法。
现在,让我们编写一个程序,按顺序执行以下步骤:
- 从文件中加载示例数据并从中构建模型。
- 生成 100 种随机颜色。
- 对每种颜色进行分类,并以与输入相同的格式将其输出到文件中。
第一步是一个相当简单的生成器,用于加载 CSV 数据并将其转换为符合我们需要的格式:
import csv
dataset_filename = "colors.csv"
def load_colors(filename):
with open(filename) as dataset_file:
lines = csv.reader(dataset_file)
for line in lines:
label, hex_color = line
yield (hex_to_rgb(hex_color), label)我们以前没有见过csv.reader函数。它在文件中的行上返回一个迭代器。迭代器返回的每个值都是字符串列表,以逗号分隔。因此,Green,#6edd13行返回为["Green", "#6edd13"]。
然后,load_colors生成器使用该迭代器,每次一行,并生成 RGB 值的元组以及标签。生成器以这种方式链接是很常见的,一个迭代器调用另一个迭代器,以此类推。您可能想查看 Python 标准库中的itertools模块,了解等待您的大量现成生成器。
本例中的 RGB 值是 0 到 255 之间的整数元组。从十六进制到 RGB 的转换有点棘手,因此我们将其拉入一个单独的函数:
def hex_to_rgb(hex_color):
return tuple(int(hex_color[i : i + 2], 16) for i in range(1, 6, 2))这个生成器表达式正在做大量的工作。它接受一个字符串,如"#12abfe"作为输入,并返回一个元组,如(18, 171, 254)。让我们从后到前把它分解。
range呼叫将返回号码[1, 3, 5]。这些表示十六进制字符串中三个颜色通道的索引。索引0被跳过,因为它代表我们不关心的字符"#"。对于这三个数字中的每一个,它都会提取介于i和i+2之间的两个字符串。对于前面的示例字符串,这将是12、ab和fe。然后将该字符串值转换为整数。作为第二个参数传递给int函数的16告诉函数使用 base-16(十六进制)而不是常用的 base-10(十进制)进行转换。
考虑到生成器表达式的读取难度,您认为它应该以不同的格式表示吗?例如,它可以创建为多个生成器表达式的序列,也可以使用yield语句展开为普通生成器函数。你喜欢哪一种?
在这种情况下,我可以放心地相信函数名来解释这行丑陋的代码在做什么。
现在我们已经加载了训练数据(手动分类的颜色),我们需要一些新的数据来测试算法的工作情况。我们可以通过生成 100 种随机颜色来实现这一点,每种颜色由三个介于 0 和 255 之间的随机数组成。
有很多方法可以做到这一点:
- 具有嵌套生成器表达式的列表理解:
[tuple(randint(0,255) for c in range(3)) for r in range(100)] - 基本生成函数
- 实现
__iter__和__next__协议的类 - 将数据推送到一个协同路由管道中
- 甚至只是一个基本的
for循环
生成器版本似乎最具可读性,因此让我们将该函数添加到程序中:
from random import randint
def generate_colors(count=100):
for i in range(count):
yield (randint(0, 255), randint(0, 255), randint(0, 255))注意我们如何参数化要生成的颜色数。现在,我们可以在将来的其他颜色生成任务中重用此功能。
现在,在我们进行分类步骤之前,我们需要一个函数来计算两种颜色之间的距离。因为可以将颜色看作是三维的(例如,红色、绿色和蓝色可以映射到x、y和z轴),所以让我们使用一些基本数学:
def color_distance(color1, color2):
channels = zip(color1, color2)
sum_distance_squared = 0
for c1, c2 in channels:
sum_distance_squared += (c1 - c2) ** 2
return sum_distance_squared这是一个非常基本的函数;看起来它甚至没有使用迭代器协议。没有yield功能,没有理解。然而,有一个for循环,对zip函数的调用也在进行一些真正的迭代(如果您不熟悉,zip生成元组,每个元组包含来自每个输入迭代器的一个元素)。
这个距离计算是毕达哥拉斯定理的三维版本,你可能记得从学校:a2+b2=c2。因为我们使用的是三维,我猜它实际上应该是a2+b2+c2=d2。从技术上讲,距离是*a2+b2+c2*的平方根,但不需要进行有点昂贵的sqrt计算,因为平方距离彼此的相对大小都相同。
现在我们已经有了一些管道,让我们来执行实际的 k-最近邻实现。这个例程可以被认为是消耗并结合了我们已经看到的两个生成器(load_colors和generate_colors:
def nearest_neighbors(model_colors, target_colors, num_neighbors=5):
model_colors = list(model_colors)
for target in target_colors:
distances = sorted(
((color_distance(c[0], target), c) for c in model_colors)
)
yield target, distances[:5]我们首先将model_colors生成器转换为列表,因为它必须被多次使用,每个target_colors一次。如果我们不这样做,我们将不得不重复从源文件加载颜色,这将执行大量不必要的磁盘读取。
这个决定的缺点是整个列表必须一次性存储在内存中。如果我们有一个不适合内存的海量数据集,那么实际上每次都需要从磁盘重新加载生成器(尽管在这种情况下,我们实际上会研究不同的机器学习算法)。
nearest_neighbors生成器在每个目标颜色(一个三元组,如(255, 14, 168))上循环,并在生成器表达式中对其调用color_distance函数。围绕生成器表达式的sorted调用然后根据结果的第一个元素(即距离)对结果进行排序。这是一段复杂的代码,根本不是面向对象的。您可能希望将其分解为一个正常的for循环,以确保您理解生成器表达式的作用。
yield语句没有那么复杂。对于target_colors生成器中的每一个 RGB 三元组,它都会生成目标,以及num_neighbors(即k 最近的中的k的列表理解。顺便说一句,许多数学家和数据科学家都有使用难以理解的单字母变量名)最接近的颜色的可怕倾向。
列表理解中每个元素的内容都是来自model_colors生成器的一个元素;也就是说,三个 RGB 值的元组的元组和手动为该颜色输入的字符串名称。所以,有一个元素可能是这样的:((104, 195, 77), 'Green')。当我看到这样的嵌套元组时,我想到的第一件事是,这不是正确的数据结构。RGB 颜色可能应该表示为一个命名元组,这两个属性可能应该放在一个数据类上。
现在,我们可以在链中添加另一个生成器,以确定我们应该为该目标颜色指定的名称:
from collections import Counter
def name_colors(model_colors, target_colors, num_neighbors=5):
for target, near in nearest_neighbors(
model_colors, target_colors, num_neighbors=5
):
print(target, near)
name_guess = Counter(n[1] for n in near).most_common()[0][0]
yield target, name_guess此生成器正在将nearest_neighbors返回的元组解包到三元组目标和五个最近的数据点中。它使用Counter查找返回的颜色中最常出现的名称。Counter构造函数中还有另一个生成器表达式;这一个从每个数据点提取第二个元素(颜色名称)。然后它产生一个元组 RGB 值和猜测的名称。返回值的示例为(91, 158, 250) Blue。
我们可以编写一个函数,接受name_colors生成器的输出并将其写入 CSV 文件,RGB 颜色表示为十六进制值:
def write_results(colors, filename="output.csv"):
with open(filename, "w") as file:
writer = csv.writer(file)
for (r, g, b), name in colors:
writer.writerow([name, f"#{r:02x}{g:02x}{b:02x}"])这是一个函数,不是生成器。它在for循环中消耗发电机,但没有产生任何结果。它构造一个 CSV 编写器,并为每个目标颜色输出名称行、十六进制值(例如,Purple,#7f5f95)对。这里唯一可能让人困惑的是格式字符串的内容。与r、g和b通道一起使用的:02x修饰符将数字输出为零填充的两位十六进制数字。
现在,我们所要做的就是将这些不同的生成器和管道连接在一起,并通过单个函数调用启动流程:
def process_colors(dataset_filename="colors.csv"):
model_colors = load_colors(dataset_filename)
colors = name_colors(model_colors, generate_colors(), 5)
write_results(colors)
if __name__ == "__main__":
process_colors()因此,这个函数与我们定义的几乎所有其他函数不同,它是一个完全正常的函数,没有任何yield语句或for循环。它根本不做任何迭代。
然而,它确实建造了三台发电机。你能看到这三个吗
load_colors返回一个发电机generate_colors返回一个发电机name_guess返回一个发电机
name_guess发电机消耗前两台发电机。然后,它又被write_results函数消耗。
我写了第二个 Tkinter 应用程序来检查算法的准确性。它与第一个应用程序类似,只是它渲染每种颜色以及与该颜色关联的标签。如果标签与颜色匹配,则必须手动单击“是”或“否”。对于我的示例数据,我得到了大约 95%的准确率。这可以通过实施以下措施加以改进:
- 添加更多颜色名称
- 通过手动分类更多颜色添加更多训练数据
- 调整
num_neighbors的值 - 使用更先进的机器学习算法
下面是输出检查应用程序的代码,不过我建议下载示例代码。这将是乏味的键入:
import tkinter as tk
import csv
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.grid(sticky="news")
master.columnconfigure(0, weight=1)
master.rowconfigure(0, weight=1)
self.csv_reader = csv.reader(open("output.csv"))
self.create_widgets()
self.total_count = 0
self.right_count = 0
def next_color(self):
return next(self.csv_reader)
def mk_grid(self, widget, column, row, columnspan=1):
widget.grid(
column=column, row=row, columnspan=columnspan, sticky="news"
)
def create_widgets(self):
color_text, color_bg = self.next_color()
self.color_box = tk.Label(
self, bg=color_bg, width="30", height="15"
)
self.mk_grid(self.color_box, 0, 0, 2)
self.color_label = tk.Label(self, text=color_text, height="3")
self.mk_grid(self.color_label, 0, 1, 2)
self.no_button = tk.Button(
self, command=self.count_next, text="No"
)
self.mk_grid(self.no_button, 0, 2)
self.yes_button = tk.Button(
self, command=self.count_yes, text="Yes"
)
self.mk_grid(self.yes_button, 1, 2)
self.percent_accurate = tk.Label(self, height="3", text="0%")
self.mk_grid(self.percent_accurate, 0, 3, 2)
self.quit = tk.Button(
self, text="Quit", command=root.destroy, bg="#ffaabb"
)
self.mk_grid(self.quit, 0, 4, 2)
def count_yes(self):
self.right_count += 1
self.count_next()
def count_next(self):
self.total_count += 1
percentage = self.right_count / self.total_count
self.percent_accurate["text"] = f"{percentage:.0%}"
try:
color_text, color_bg = self.next_color()
except StopIteration:
color_text = "DONE"
color_bg = "#ffffff"
self.color_box["text"] = "DONE"
self.yes_button["state"] = tk.DISABLED
self.no_button["state"] = tk.DISABLED
self.color_label["text"] = color_text
self.color_box["bg"] = color_bg
root = tk.Tk()
app = Application(master=root)
app.mainloop()您可能想知道,这些与面向对象编程有什么关系?这段代码中甚至没有一个类!。在某些方面,你是对的;生成器通常不被认为是面向对象的。但是,创建它们的函数返回对象;事实上,您可以将这些函数视为构造函数。构造对象具有适当的__next__()方法。基本上,生成器语法是一种特定类型对象的语法快捷方式,如果没有它,创建该对象将非常冗长。
如果您在日常编码中不经常使用理解,那么您应该做的第一件事就是搜索一些现有代码并找到一些for循环。查看是否可以将其中任何一个简单地转换为生成器表达式或列表、集合或字典。
测试列表理解比for循环快的说法。这可以通过内置的timeit模块完成。使用timeit.timeit功能的帮助文档了解如何使用它。基本上,编写两个做同样事情的函数,一个使用列表理解,另一个使用for循环迭代数千项。将每个函数传递到timeit.timeit,并比较结果。如果你喜欢冒险,也可以比较生成器和生成器表达式。使用timeit测试代码可能会让人上瘾,因此请记住,除非代码被执行了大量次,例如在一个巨大的输入列表或文件上,否则代码不需要超高速。
玩发电机的功能。从需要多个值的基本迭代器开始(数学序列是典型的例子;如果想不出更好的方法,Fibonacci 序列就会被过度使用)。尝试一些更高级的生成器,它们可以执行一些操作,例如获取多个输入列表并以某种方式生成合并它们的值。生成器也可以用于文件;你能写一个简单的生成器来显示两个文件中相同的行吗?
协同路由滥用迭代器协议,但实际上没有实现迭代器模式。您能否构建从日志文件获取序列号的非协同程序版本的代码?采用面向对象的方法,以便可以在类上存储额外的状态。如果您可以创建一个对象来替换现有的协同路由,那么您将学到很多关于协同路由的知识。
本章的案例研究中有很多奇数元组,这些奇数元组很难跟踪。看看是否可以用更多面向对象的解决方案替换这些返回值。另外,尝试将一些共享数据的函数(例如,model_colors和target_colors)移动到一个类中。这将减少必须传递到大多数生成器的参数数量,因为它们可以在self上查找它们。
在本章中,我们了解到设计模式是为常见编程问题提供最佳实践解决方案的有用抽象。我们介绍了我们的第一个设计模式,迭代器,以及 Python 为了自己的邪恶目的而使用和滥用该模式的多种方式。最初的迭代器模式是非常面向对象的,但编码起来也相当难看和冗长。然而,Python 的内置语法将丑陋之处抽象了出来,为这些面向对象的构造留下了一个干净的接口。
理解和生成器表达式可以在一行中将容器构造与迭代结合起来。生成器对象可以使用yield语法构造。协同程序在外部看起来像发电机,但用途却大不相同。
在接下来的两章中,我们将介绍更多的设计模式。