当前位置: 首页‣ 深入 Python 3 ‣
难度级别: ♦♦♦♢♢
❝ 东是东,西是西,东西不相及 ❞
— 拉迪亚德·吉卜林
生成器是一类特殊 迭代器。 一个产生值的函数 yield
是一种产生一个迭代器却不需要构建迭代器的精密小巧的方法。 我会告诉你我是什么意思。
记得 菲波拉稀生成器吗? 这里是一个从无到有的迭代器:
class Fib:
'''生成菲波拉稀数列的迭代器'''
def __init__(self, max):
self.max = max
def __iter__(self):
self.a = 0
self.b = 1
return self
def __next__(self):
fib = self.a
if fib > self.max:
raise StopIteration
self.a, self.b = self.b, self.a + self.b
return fib
让我们一行一行来分析。
class Fib:
类(class)?什么是类?
⁂
Python 是完全面向对象的:你可以定义自己的类,从你自己或系统自带的类继承,并生成实例。
在Python里定义一个类非常简单。就像函数一样, 没有分开的接口定义。 只需定义类就开始编码。 Python类以保留字 class
开始, 后面跟类名。 技术上来说,只需要这么多就够了,因为一个类不是必须继承其他类。
class PapayaWhip: ①
pass ②
PapayaWhip
, 没有从其他类继承。 类名通常是大写字母分隔, 如EachWordLikeThis
, 但这只是个习惯,并非必须。
if
语句, for
循环, 或其他代码块。第一行非缩进代码表示到了类外。
PapayaWhip
类没有定义任何方法和属性, 但依据句法,应该在定义中有东西,这就是 pass
语句。 这是Python 保留字,意思是“继续,这里看不到任何东西”。 这是一个什么都不做的语句,是一个很好的占位符,如果你的函数和类什么都不想做(删空函数或类)。
☞Python中的
pass
就像Java 或 C中的空大括号对 ({}
) 。
很多类继承自其他类, 但这个类没有。 很多类有方法,这个类也没有。 Python 类不是必须有东西,除了一个名字。 特别是C++ 程序员发现 Python 类没有显式的构造和析构函数会觉得很古怪。 尽管不是必须, Python 类 可以 具有类似构造函数的东西: __init__()
方法。
__init__()
方法本示例展示 Fib
类使用 __init__
方法。
class Fib:
'''生成菲波拉稀数列的迭代器''' ①
def __init__(self, max): ②
docstring
, 与模块和方法一样。
__init__()
方法被立即调用。很容易将其——但技术上来说不正确——称为该类的“构造函数” 。 很容易,因为它看起来很像 C++ 的构造函数(按约定,__init__()
是类中第一个被定义的方法),行为一致(是类的新实例中第一片被执行的代码), 看起来完全一样。 错了, 因为__init__()
方法调用时,对象已经创建了,你已经有了一个合法类对象的引用。
每个方法的第一个参数,包括 __init__()
方法,永远指向当前的类对象。 习惯上,该参数叫 self。 该参数和C++或Java中 this
角色一样, 但 self 不是 Python的保留字, 仅仅是个命名习惯。 虽然如此,请不要取别的名字,只用 self; 这是一个很强的命名习惯。
在 __init__()
方法中, self 指向新创建的对象; 在其他类对象中, 它指向方法所属的实例。尽管需在定义方法时显式指定self ,调用方法时并 不 必须明确指定。 Python 会自动添加。
⁂
Python 中实例化类很直接。 实例化类时就像调用函数一样简单,将 __init__()
方法需要的参数传入。 返回值就是新创建的对象。
>>> import fibonacci2
>>> fib = fibonacci2.Fib(100) ①
>>> fib ②
<fibonacci2.Fib object at 0x00DB8810>
>>> fib.__class__ ③
<class 'fibonacci2.Fib'>
>>> fib.__doc__ ④
'生成菲波拉稀数列的迭代器
'
Fib
类的实例(在fibonacci2
模块中定义) 将新创建的实例赋给变量fib。 你传入一个参数 100
, 这是Fib
的__init__()
方法作为max参数传入的结束值。
Fib
的实例。
__class__
, 它是该对象的类。 Java 程序员可能熟悉 Class
类, 包含方法如 getName()
和 getSuperclass()
获取对象相关元数据。 Python里面, 这类元数据由属性提供,但思想一致。
docstring
,就像函数或模块中的一样。 类的所有实例共享一份 docstring
。
☞Python里面, 和调用函数一样简单的调用一个类来创建该类的新实例。 与C++ 或 Java不一样,没有显式的
new
操作符。
⁂
继续下一行:
class Fib:
def __init__(self, max):
self.max = max ①
__init__()
方法的 max完全是两回事。 self.max 是实例内 “全局” 的。 这意味着可以在其他方法中访问它。
class Fib:
def __init__(self, max):
self.max = max ①
.
.
.
def __next__(self):
fib = self.a
if fib > self.max: ②
__init__()
方法中定义……
__next__()
方法中引用。
实例变量特定于某个类的实例。 例如, 如果你创建 Fib
的两个具有不同最大值的实例, 每个实例会记住自己的值。
>>> import fibonacci2 >>> fib1 = fibonacci2.Fib(100) >>> fib2 = fibonacci2.Fib(200) >>> fib1.max 100 >>> fib2.max 200
⁂
现在 你已经准备学习如何创建一个迭代器了。 迭代器就是一个定义了 __iter__()
方法的类。
class Fib: ①
def __init__(self, max): ②
self.max = max
def __iter__(self): ③
self.a = 0
self.b = 1
return self
def __next__(self): ④
fib = self.a
if fib > self.max:
raise StopIteration ⑤
self.a, self.b = self.b, self.a + self.b
return fib ⑥
fib
应是一个类,而不是一个函数。
Fib(max)
会创建该类一个真实的实例,并以max做为参数调用__init__()
方法。 __init__()
方法以实例变量保存最大值,以便随后的其他方法可以引用。
iter(fib)
的时候,__iter__()
就会被调用。(正如你等下会看到的, for
循环会自动调用它, 你也可以自己手动调用。) 在完成迭代器初始化后,(在本例中, 重置我们两个计数器 self.a
和 self.b
), __iter__()
方法能返回任何实现了 __next__()
方法的对象。 在本例(甚至大多数例子)中, __iter__()
仅简单返回 self, 因为该类实现了自己的 __next__()
方法。
next()
方法时,__next__()
会自动调用。 随后会有更多理解。
__next__()
方法抛出 StopIteration
异常, 这是给调用者表示迭代用完了的信号。 和大多数异常不同, 这不是错误;它是正常情况,仅表示迭代器没有值可产生了。 如果调用者是 for
循环, 它会注意到该 StopIteration
异常并优雅的退出。 (换句话说,它会吞掉该异常。) 这点神奇之处就是使用 for
的关键。
__next__()
方法简单 return
该值。 不要使用 yield
; 该语法上的小甜头仅用于你使用生成器的时候。 这里你从无到有创建迭代器,使用 return
代替。
完全晕了? 太好了。 让我们看如何调用该迭代器:
>>> from fibonacci2 import Fib >>> for n in Fib(1000): ... print(n, end=' ') 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
为什么?完全一模一样! 一字节一字节的与你调用 Fibonacci-as-a-generator (模块第一个字母大写)相同。但怎么做到的?
for
循环内有魔力。下面是究竟发生了什么:
for
循环调用 Fib(1000)
。 这返回Fib
类的实例。 叫它 fib_inst。
for
循环调用 iter(fib_inst)
, 它返回迭代器。 叫它 fib_iter。 本例中, fib_iter == fib_inst, 因为 __iter__()
方法返回 self,但for
循环不知道(也不关心)那些。
for
循环调用 next(fib_iter)
, 它又调用 fib_iter
对象的 __next__()
方法,产生下一个菲波拉稀计算并返回值。 for
拿到该值并赋给 n, 然后执行n值的 for
循环体。
for
循环如何知道什么时候结束?很高兴你问到。 当next(fib_iter)
抛出 StopIteration
异常时, for
循环将吞下该异常并优雅退出。 (其他异常将传过并如常抛出。) 在哪里你见过 StopIteration
异常? 当然在 __next__()
方法。
⁂
现在到曲终的时候了。我们重写 复数规则生成器 为迭代器。
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8')
self.cache = []
def __iter__(self):
self.cache_index = 0
return self
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1]
if self.pattern_file.closed:
raise StopIteration
line = self.pattern_file.readline()
if not line:
self.pattern_file.close()
raise StopIteration
pattern, search, replace = line.split(None, 3)
funcs = build_match_and_apply_functions(
pattern, search, replace)
self.cache.append(funcs)
return funcs
rules = LazyRules()
因此这是一个实现了 __iter__()
和 __next__()
的类。所以它可以 被用作迭代器。然后,你实例化它并将其赋给 rules 。这只发生一次,在import的时候。
让我们一口一口来吃:
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8') ①
self.cache = [] ②
LazyRules
类时, 打开模式文件,但不读取任何东西。 (随后再进行)
__next__()
方法中) 。
我们继续之前,让我们近观 rules_filename。它没在 __iter__()
方法中定义。事实上,它没在任何方法中定义。它定义于类级别。它是 类变量, 尽管访问时和实例变量一样 (self.rules_filename), LazyRules
类的所有实例共享该变量。
>>> import plural6 >>> r1 = plural6.LazyRules() >>> r2 = plural6.LazyRules() >>> r1.rules_filename ① 'plural6-rules.txt' >>> r2.rules_filename 'plural6-rules.txt' >>> r2.rules_filename = 'r2-override.txt' ② >>> r2.rules_filename 'r2-override.txt' >>> r1.rules_filename 'plural6-rules.txt' >>> r2.__class__.rules_filename ③ 'plural6-rules.txt' >>> r2.__class__.rules_filename = 'papayawhip.txt' ④ >>> r1.rules_filename 'papayawhip.txt' >>> r2.rules_filename ⑤ 'r2-overridetxt'
__class__
属性来访问类属性(于此相对的是单独实例的属性)。
现在回到我们的演示:
def __iter__(self): ①
self.cache_index = 0
return self ②
for
循环——调用 iter(rules)
的时候,__iter__()
方法都会被调用。
__iter__()
方法都需要做的就是必须返回一个迭代器。 在本例中,返回 self,意味着该类定义了__next__()
方法,由它来关注整个迭代过程中的返回值。
def __next__(self): ①
.
.
.
pattern, search, replace = line.split(None, 3)
funcs = build_match_and_apply_functions( ②
pattern, search, replace)
self.cache.append(funcs) ③
return funcs
for
循环——调用 __next__()
方法, next(rules)
都跟着被调用。 该方法仅在我们从后往前移动时比较好体会。所以我们就这么做。
build_match_and_apply_functions()
函数还没修改;与它从前一样。
self.cache
。
从后往前移动……
def __next__(self):
.
.
.
line = self.pattern_file.readline() ①
if not line: ②
self.pattern_file.close()
raise StopIteration ③
.
.
.
readline()
方法 (注意:是单数,不是复数 readlines()
) 从一个打开的文件中精确读取一行,即下一行。(文件对象同样也是迭代器! 它自始至终是迭代器……)
readline()
可以读, line 就不会是空字符串。 甚至文件包含一个空行, line 将会是一个字符的字符串 '\n'
(回车换行符)。 如果 line 是真的空字符串, 就意味着文件已经没有行可读了。
StopIteration
异常。 记住,开门见山的说是因为我们需要为下一条规则找到一个匹配和应用功能。下一条规则从文件的下一行获取…… 但已经没有下一行了! 所以,我们没有规则返回。 迭代器结束。 (♫ 派对结束 ♫)
由后往前直到 __next__()
方法的开始……
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1] ①
if self.pattern_file.closed:
raise StopIteration ②
.
.
.
self.cache
将是一个我们匹配并应用单独规则的功能列表。 (至少那个应该看起来熟悉!) self.cache_index
记录我们下一步返回的缓存条目。 如果我们还没有耗尽缓存 (举例 如果 self.cache
的长度大于 self.cache_index
),那么我们就会命中一条缓存! 哇! 我们可以从缓存中返回匹配和应用功能而不是从无到有创建。
放到一起,发生了什么事? 当:
LazyRules
类的一个单一实例, 叫 rules, 它打开模式文件但并没有读取。
plural()
函数来让一个不同的单词变复数。 plural()
函数中的for
循环会调用iter(rules)
,这会重置缓存索引但不会重置打开的文件对象。
for
循环会从rules中索要一个值,该值会调用其__next__()
方法。然而这一次, 缓存已经被装入了一个匹配和应用功能对, 与模式文件中第一行模式一致。 由于对前一个单词做复数变换时已经被创建和缓存,它们被从缓存中返回。 缓存索引递增,打开的文件无需访问。
for
循环再次运转并从 rules请求一个值。 这会再次调用 __next__()
方法。 这一次, 缓存被用完了——它仅有一个条目,而我们被请求第二个——于是 __next__()
方法继续。 从打开的文件中读取下一行,从模式中创建匹配和应用功能,并缓存之。
readline()
命令的地方。现在,缓存已经有更多条目了,并且再次从头开始来将一个新单词变复数,在读取模式文件下一行之前,缓存中的每一个条目都将被尝试。
我们已经到达复数变换的极乐世界。
import
时发生的唯一的事就是实例化一个单一的类并打开一个文件(但并不读取)。
☞这真的是极乐世界? 嗯,是或不是。 这里有一些
LazyRules
示例需要细想的地方: 模式文件被打开(在__init__()
中),并持续打开直到读取最后一个规则。 当Python退出或最后一个LazyRules
类的实例销毁,Python 会最终关闭文件,但是那仍然可能会是一个很长的时间。如果该类是一个“长时间运行”的Python进程的一部分,Python可能从不退出,LazyRules
对象就可能一直不会释放。这种情况有解决办法。 不要在
__init__()
中打开文件并让其在一行一行读取规则时一直打开,你可以打开文件,读取所有规则,并立即关闭文件。或你可以打开文件,读取一条规则,用tell()
方法保存文件位置,关闭文件,后面再次打开它,使用seek()
方法 继续从你离开的地方读取。 或者你不需担心这些就让文件打开,如同本示例所做。 编程即是设计, 而设计牵扯到所有的权衡和限制。让一个文件一直打开太长时间可能是问题;让你代码太复杂也可能是问题。哪一个是更大的问题,依赖于你的开发团队,你的应用,和你的运行环境。
⁂
© 2001–9 Mark Pilgrim