您当前位置:首页 深入 Python 3

难度级别:♦♦♦♢♢

& 迭代器

东方是东方,西方是西方,两者永不相交。
—— 鲁德亚德·吉卜林

 

深入探究

迭代器是 Python 3 的“秘诀”。它们无处不在,支撑着所有内容,始终隐而不现。 推导式 只是 迭代器 的一种简单形式。生成器只是 迭代器 的一种简单形式。一个 yield 值的函数是构建迭代器的简洁高效的方式,无需显式构建迭代器。让我向您展示我的意思。

还记得 斐波那契生成器 吗?以下是用从头开始构建的迭代器实现它。

[下载 fibonacci2.py]

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           
  1. 此类的名称是 PapayaWhip,它没有从任何其他类继承。类名通常以大写字母开头,例如 EachWordLikeThis,但这只是一个约定,不是强制要求。
  2. 您可能已经猜到了,类中的所有内容都缩进,就像函数、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):                                      
  1. 类也可以(并且应该)拥有 docstring,就像模块和函数一样。
  2. __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'
  1. 您正在创建一个 Fib 类(在 fibonacci2 模块中定义)的实例,并将新建的实例分配给变量 fib。您正在传递一个参数 100,它将最终作为 Fib__init__() 方法中的 max 参数。
  2. fib 现在是 Fib 类的实例。
  3. 每个类实例都有一个内置属性 __class__,它是该对象的类。Java 程序员可能熟悉 Class 类,它包含 getName()getSuperclass() 等方法,用于获取有关对象的元数据信息。在 Python 中,这种元数据可以通过属性访问,但概念是一样的。
  4. 您可以像访问函数或模块一样访问该实例的 docstring。该类的所有实例共享相同的 docstring

在 Python 中,只需像调用函数一样调用类,即可创建一个类的实例。与 C++ 或 Java 中的显式 new 运算符不同。

实例变量

接下来我们看下一行代码。

class Fib:
    def __init__(self, max):
        self.max = max        
  1. 什么是 self.max?它是一个实例变量。它与 max 完全分离,max 是作为参数传递给 __init__() 方法的。 self.max 对该实例来说是“全局的”。这意味着您可以从其他方法访问它。
class Fib:
    def __init__(self, max):
        self.max = max        
    .
    .
    .
    def __next__(self):
        fib = self.a
        if fib > self.max:    
  1. self.max__init__() 方法中定义…
  2. …并在 __next__() 方法中引用。

实例变量特定于类的单个实例。例如,如果您使用不同的最大值创建两个 Fib 实例,它们将分别记住自己的值。

>>> import fibonacci2
>>> fib1 = fibonacci2.Fib(100)
>>> fib2 = fibonacci2.Fib(200)
>>> fib1.max
100
>>> fib2.max
200

斐波那契迭代器

现在您已准备好学习如何构建迭代器。迭代器只是一个定义了 __iter__() 方法的类。

[下载 fibonacci2.py]

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                                
  1. 要从头开始构建迭代器,Fib 需要是一个类,而不是一个函数。
  2. “调用”Fib(max) 实际上是创建该类的实例,并使用 max 调用其 __init__() 方法。__init__() 方法将最大值保存为实例变量,以便其他方法可以稍后引用它。
  3. __iter__() 方法在任何时候调用 iter(fib) 时都会被调用。(正如您稍后将看到的那样,for 循环会自动调用它,但您也可以手动调用它。)在执行迭代开始时的初始化(在本例中,重置 self.aself.b,我们的两个计数器)后,__iter__() 方法可以返回任何实现 __next__() 方法的对象。在本例中(以及大多数情况下),__iter__() 只返回 self,因为此类实现了它自己的 __next__() 方法。
  4. __next__() 方法在任何时候调用类实例的迭代器的 next() 时都会被调用。过会儿就会更清楚了。
  5. __next__() 方法引发 StopIteration 异常时,它向调用者发出信号,表明迭代已完成。与大多数异常不同,这不是错误;这是一种正常情况,仅仅意味着迭代器没有更多值要生成。如果调用者是 for 循环,它会注意到此 StopIteration 异常并优雅地退出循环。(换句话说,它会吞下异常。)这种小小的魔法实际上是在 for 循环中使用迭代器的关键。
  6. 要输出下一个值,迭代器的 __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 循环中包含一些魔法。以下是发生的事情

复数规则迭代器

现在是最后阶段了。让我们将 复数规则生成器 重写为迭代器。

[下载 plural6.py]

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 = []                                                  
  1. 当我们实例化 LazyRules 类时,打开模式文件,但不要读取任何内容。(这将在稍后进行。)
  2. 打开模式文件后,初始化缓存。稍后(在 __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'
  1. 该类的每个实例都继承了 rules_filename 属性,其值由类定义。
  2. 在一个实例中更改属性的值不会影响其他实例…
  3. …也不会更改类属性。您可以使用特殊属性 __class__ 访问类本身(而不是单个实例的属性),从而访问类属性。
  4. 如果您更改类属性,所有仍在继承该值的实例(如这里的 r1)都会受到影响。
  5. 已覆盖该属性的实例(如这里的 r2)不会受到影响。

现在让我们回到我们的节目。

    def __iter__(self):       
        self.cache_index = 0
        return self           
  1. 每次有人(比如,一个 `for` 循环)调用 `iter(rules)` 时,都会调用 `__iter__()` 方法。
  2. 每个 `__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
  1. 每次有人(比如,一个 `for` 循环)调用 `next(rules)` 时,都会调用 `__next__()` 方法。只有从最后开始逐步后退,这个方法才有意义。所以我们就这样做。
  2. 这个函数的最后部分应该很熟悉,至少。`build_match_and_apply_functions()` 函数没有改变;它和以前一样。
  3. 唯一的区别是,在返回匹配和应用函数(存储在元组 `funcs` 中)之前,我们将它们保存到 `self.cache` 中。

倒退…

    def __next__(self):
        .
        .
        .
        line = self.pattern_file.readline()  
        if not line:                         
            self.pattern_file.close()
            raise StopIteration              
        .
        .
        .
  1. 这里有一些高级的文件技巧。`readline()` 方法(注意:是单数形式,不是复数形式的 `readlines()`)从一个打开的文件中读取正好一行。具体来说,是下一行。(文件对象也是迭代器!迭代器无处不在……
  2. 如果 `readline()` 方法可以读取一行,`line` 将不是空字符串。即使文件包含一个空行,`line` 也会变成一个单字符字符串 `'\n'`(一个回车符)。如果 `line` 确实是空字符串,这意味着文件中没有更多行可读。
  3. 当我们到达文件末尾时,我们应该关闭文件并引发魔法 `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                         
        .
        .
        .
  1. `self.cache` 将是一个列表,包含我们用来匹配和应用单个规则的函数。(至少应该听起来很熟悉!) `self.cache_index` 用于跟踪我们应该返回的下一个缓存项。如果我们还没有用完缓存( 如果 `self.cache` 的长度大于 `self.cache_index`),那么我们命中了缓存!万岁!我们可以从缓存中返回匹配和应用函数,而不是从头开始构建它们。
  2. 另一方面,如果我们没有从缓存中获取到命中,并且 文件对象已被关闭(这可能发生,在方法中更下方,就像你在之前的代码片段中看到的),那么我们无能为力。如果文件已关闭,这意味着我们已用完它——我们已经读完模式文件中的每一行,并且已经构建并缓存了每个模式的匹配和应用函数。文件用完了;缓存用完了;我也用完了。等等,什么?坚持住,我们快完成了。

把所有这些放在一起,以下是当

我们已经实现了复数化的涅槃。

  1. 启动成本最小。在 `import` 期间,唯一发生的事情是实例化一个类并打开一个文件(但不从文件中读取)。
  2. 性能最大化。之前的示例会在每次你想要对一个词进行复数化时,遍历整个文件并动态构建函数。这个版本会在函数构建后立即缓存它们,并且在最坏的情况下,它只会遍历模式文件一次,无论你对多少个词进行复数化。
  3. 代码和数据的分离。所有模式都存储在一个单独的文件中。代码是代码,数据是数据,两者永不相交。

这真的是涅槃吗?嗯,是也不是。在 `LazyRules` 示例中,需要考虑以下内容:模式文件在 `__init__()` 期间打开,并且一直保持打开状态,直到最后一个规则被到达。Python 最终会在退出时或最后一个 `LazyRules` 类实例被销毁后关闭文件,但仍然,这可能需要很长时间。如果此类是长时间运行的 Python 进程的一部分,Python 解释器可能永远不会退出,`LazyRules` 对象可能永远不会被销毁。

有一些方法可以解决这个问题。与其在 `__init__()` 期间打开文件,并在你逐行读取规则时保持打开状态,不如打开文件,读取所有规则,然后立即关闭文件。或者你可以打开文件,读取一个规则,使用 `tell()` 方法 保存文件位置,关闭文件,然后稍后重新打开它,并使用 `seek()` 方法 继续从你停止读取的地方开始读取。或者你可以不理会它,就像这个示例代码一样,直接将文件保持打开状态。编程是设计,而设计都是关于权衡和约束的。将文件保持打开状态太久可能是个问题;使你的代码更复杂也可能是个问题。哪个问题更大取决于你的开发团队、你的应用程序和你的运行时环境。

进一步阅读

© 2001–11 Mark Pilgrim