您当前位置:首页 ‣ 深入 Python 3 ‣
难度级别:♦♦♦♢♢
❝东方是东方,西方是西方,两者永不相交。❞
—— 鲁德亚德·吉卜林
迭代器是 Python 3 的“秘诀”。它们无处不在,支撑着所有内容,始终隐而不现。 推导式 只是 迭代器 的一种简单形式。生成器只是 迭代器 的一种简单形式。一个 yield
值的函数是构建迭代器的简洁高效的方式,无需显式构建迭代器。让我向您展示我的意思。
还记得 斐波那契生成器 吗?以下是用从头开始构建的迭代器实现它。
class Fib:
'''iterator that yields numbers in the Fibonacci sequence'''
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__()
方法此示例展示了使用 __init__
方法初始化 Fib
类。
class Fib:
'''iterator that yields numbers in the Fibonacci sequence''' ①
def __init__(self, max): ②
docstring
,就像模块和函数一样。__init__()
方法在创建类实例后立即被调用。称它为类的“构造函数”很诱人——但从技术上来说是不正确的——。它很诱人,因为它看起来像 C++ 构造函数(按照惯例,__init__()
方法是为类定义的第一个方法),行为也像(它是新建类实例中执行的第一段代码),甚至听起来也像。不正确,因为在调用 __init__()
方法时,对象已经构造完成,并且您已经拥有对该类的新实例的有效引用。每个类方法(包括 __init__()
方法)的第一个参数始终是对该类当前实例的引用。按照惯例,此参数名为 self。此参数在 C++ 或 Java 中充当保留字 this
的角色,但 self 不是 Python 中的保留字,只是一个命名约定。尽管如此,请不要将其称为除 self 之外的任何东西;这是一个非常强的约定。
在所有类方法中,self 指向调用该方法的实例。但在 __init__()
方法的特定情况下,调用该方法的实例也是新建的对象。虽然您在定义方法时需要显式指定 self,但在调用方法时不需要指定它;Python 会自动为您添加它。
⁂
在 Python 中实例化类很简单。要实例化一个类,只需像调用函数一样调用它,传递 __init__()
方法所需的参数。返回值将是新建的对象。
>>> import fibonacci2 >>> fib = fibonacci2.Fib(100) ① >>> fib ② <fibonacci2.Fib object at 0x00DB8810> >>> fib.__class__ ③ <class 'fibonacci2.Fib'> >>> fib.__doc__ ④ 'iterator that yields numbers in the Fibonacci sequence'
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__()
方法的。 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__()
方法在任何时候调用 iter(fib)
时都会被调用。(正如您稍后将看到的那样,for
循环会自动调用它,但您也可以手动调用它。)在执行迭代开始时的初始化(在本例中,重置 self.a
和 self.b
,我们的两个计数器)后,__iter__()
方法可以返回任何实现 __next__()
方法的对象。在本例中(以及大多数情况下),__iter__()
只返回 self,因为此类实现了它自己的 __next__()
方法。__next__()
方法在任何时候调用类实例的迭代器的 next()
时都会被调用。过会儿就会更清楚了。__next__()
方法引发 StopIteration
异常时,它向调用者发出信号,表明迭代已完成。与大多数异常不同,这不是错误;这是一种正常情况,仅仅意味着迭代器没有更多值要生成。如果调用者是 for
循环,它会注意到此 StopIteration
异常并优雅地退出循环。(换句话说,它会吞下异常。)这种小小的魔法实际上是在 for
循环中使用迭代器的关键。__next__()
方法只需 return
该值。不要在此处使用 yield
;这只是语法糖,仅适用于使用生成器时。这里您从头开始创建自己的迭代器;请使用 return
而不是 yield
。彻底搞糊涂了吗?很好。让我们看看如何调用此迭代器。
>>> 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
为什么,它完全一样!与调用 斐波那契生成器 一模一样(除了一个大写字母)。但这是怎么做到的呢?
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。这只会发生一次,在导入时。
让我们一次一步地分析该类。
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 ②
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
倒退…
def __next__(self):
.
.
.
line = self.pattern_file.readline() ①
if not line: ②
self.pattern_file.close()
raise 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 ②
.
.
.
把所有这些放在一起,以下是当
我们已经实现了复数化的涅槃。
☞这真的是涅槃吗?嗯,是也不是。在 `LazyRules` 示例中,需要考虑以下内容:模式文件在 `__init__()` 期间打开,并且一直保持打开状态,直到最后一个规则被到达。Python 最终会在退出时或最后一个 `LazyRules` 类实例被销毁后关闭文件,但仍然,这可能需要很长时间。如果此类是长时间运行的 Python 进程的一部分,Python 解释器可能永远不会退出,`LazyRules` 对象可能永远不会被销毁。
有一些方法可以解决这个问题。与其在 `__init__()` 期间打开文件,并在你逐行读取规则时保持打开状态,不如打开文件,读取所有规则,然后立即关闭文件。或者你可以打开文件,读取一个规则,使用 `tell()` 方法 保存文件位置,关闭文件,然后稍后重新打开它,并使用 `seek()` 方法 继续从你停止读取的地方开始读取。或者你可以不理会它,就像这个示例代码一样,直接将文件保持打开状态。编程是设计,而设计都是关于权衡和约束的。将文件保持打开状态太久可能是个问题;使你的代码更复杂也可能是个问题。哪个问题更大取决于你的开发团队、你的应用程序和你的运行时环境。
⁂
© 2001–11 Mark Pilgrim