9.

类提供了一种将数据和功能捆绑在一起的方式。创建一个新类就创建了一种新的对象 类型,从而允许创建该类型的新 实例。每个类实例都可以有附加的属性来维护其状态。类实例还可以有方法(由其类定义)来修改其状态。

与其他编程语言相比,Python 的类机制以最少的新语法和语义增加了类。它是 C++ 和 Modula-3 中类机制的混合体。Python 类提供了面向对象编程的所有标准特性:类继承机制允许多个基类,派生类可以覆盖其基类或多个基类的任何方法,并且方法可以调用具有相同名称的基类方法。对象可以包含任意数量和种类的数据。与模块一样,类也具有 Python 的动态特性:它们在运行时创建,并且在创建后可以进一步修改。

在 C++ 术语中,通常类成员(包括数据成员)是 公共的(除了参见下面的 私有变量),所有成员函数都是 虚拟的。与 Modula-3 一样,从方法中引用对象的成员没有简写:方法函数声明时有一个显式的第一个参数代表对象,该参数由调用隐式提供。与 Smalltalk 一样,类本身就是对象。这为导入和重命名提供了语义。与 C++ 和 Modula-3 不同,内置类型可以作为用户扩展的基类。此外,与 C++ 一样,大多数具有特殊语法的内置运算符(算术运算符、下标等)可以为类实例重新定义。

(由于缺乏普遍接受的术语来谈论类,我将偶尔使用 Smalltalk 和 C++ 术语。我将使用 Modula-3 术语,因为它的面向对象语义比 C++ 更接近 Python,但我预计很少有读者听说过它。)

9.1. 关于名称和对象

对象具有个体性,多个名称(在多个作用域中)可以绑定到同一个对象。这在其他语言中称为别名。初次接触 Python 时,通常不会注意到这一点,在处理不可变的基本类型(数字、字符串、元组)时可以安全地忽略。然而,别名可能对涉及可变对象(如列表、字典和大多数其他类型)的 Python 代码的语义产生令人惊讶的影响。这通常对程序有利,因为别名在某些方面表现得像指针。例如,传递对象成本很低,因为实现只传递一个指针;如果函数修改了作为参数传递的对象,调用者将看到这种改变——这消除了像 Pascal 中那样需要两种不同参数传递机制的必要性。

9.2. Python 作用域和命名空间

在介绍类之前,我必须先告诉您一些关于 Python 作用域规则的知识。类定义在命名空间方面做了一些巧妙的技巧,您需要了解作用域和命名空间的工作原理才能完全理解正在发生的事情。顺便说一下,了解这个主题对于任何高级 Python 程序员都很有用。

让我们从一些定义开始。

命名空间 是从名称到对象的映射。大多数命名空间目前都实现为 Python 字典,但这通常在任何方面都无法注意到(除了性能),并且将来可能会改变。命名空间的例子有:内置名称集(包含像 abs() 这样的函数和内置异常名称);模块中的全局名称;以及函数调用中的局部名称。从某种意义上说,对象的属性集也形成一个命名空间。关于命名空间需要了解的重要一点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义一个函数 maximize 而不会造成混淆——模块的用户必须在其前面加上模块名称。

顺便说一下,我将 属性 用于点后面的任何名称——例如,在表达式 z.real 中,real 是对象 z 的一个属性。严格来说,对模块中名称的引用是属性引用:在表达式 modname.funcname 中,modname 是一个模块对象,funcname 是它的一个属性。在这种情况下,模块的属性和模块中定义的全局名称之间碰巧存在直接映射:它们共享相同的命名空间![1]

属性可以是只读的也可以是可写的。在后一种情况下,可以对属性进行赋值。模块属性是可写的:你可以写 modname.the_answer = 42。可写属性也可以用 del 语句删除。例如,del modname.the_answer 将从名为 modname 的对象中删除属性 the_answer

命名空间在不同时刻创建,并具有不同的生命周期。包含内置名称的命名空间在 Python 解释器启动时创建,并且从不删除。模块的全局命名空间在读取模块定义时创建;通常,模块命名空间也持续到解释器退出。解释器顶层调用执行的语句,无论是从脚本文件读取还是交互式执行,都被认为是名为 __main__ 的模块的一部分,因此它们拥有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块叫做 builtins。)

函数的本地命名空间在函数被调用时创建,并在函数返回或引发未在函数内部处理的异常时删除。(实际上,“忘记”是描述实际发生情况的更好方式。)当然,递归调用各有自己的本地命名空间。

作用域 是 Python 程序中可以直接访问命名空间的文本区域。“直接可访问”在这里意味着对名称的不合格引用会尝试在命名空间中查找该名称。

虽然作用域是静态确定的,但它们是动态使用的。在执行的任何时候,都有 3 或 4 个嵌套作用域的命名空间可以直接访问

  • 最内层作用域,首先被搜索,包含局部名称

  • 任何 enclosing 函数的作用域,从最近的 enclosing 作用域开始搜索,包含非局部但也不是全局的名称

  • 倒数第二个作用域包含当前模块的全局名称

  • 最外层作用域(最后搜索)是包含内置名称的命名空间

如果一个名称被声明为全局的,那么所有的引用和赋值都会直接指向包含模块全局名称的倒数第二个作用域。为了重新绑定在最内层作用域之外找到的变量,可以使用 nonlocal 语句;如果没有声明为非局部变量,这些变量是只读的(尝试写入这样的变量只会创建最内层作用域中的一个 局部变量,而使同名的外部变量保持不变)。

通常,局部作用域引用(文本上)当前函数的局部名称。在函数外部,局部作用域引用与全局作用域相同的命名空间:模块的命名空间。类定义会在局部作用域中放置另一个命名空间。

重要的是要认识到作用域是文本决定的:在模块中定义的函数的全局作用域是该模块的命名空间,无论函数是从何处或以何别名调用。另一方面,名称的实际搜索是在运行时动态完成的——但是,语言定义正在朝着在“编译”时进行静态名称解析的方向发展,所以不要依赖动态名称解析!(事实上,局部变量已经静态确定了。)

Python 的一个特殊之处在于——如果没有 globalnonlocal 语句生效——对名称的赋值总是进入最内层作用域。赋值不复制数据——它们只是将名称绑定到对象。删除也是如此:语句 del x 从局部作用域引用的命名空间中删除 x 的绑定。事实上,所有引入新名称的操作都使用局部作用域:特别是,import 语句和函数定义在局部作用域中绑定模块或函数名称。

global 语句可以用来指示特定的变量存在于全局作用域中,并且应该在那里重新绑定;nonlocal 语句指示特定的变量存在于封闭作用域中,并且应该在那里重新绑定。

9.2.1. 作用域和命名空间示例

这是一个示例,演示如何引用不同的作用域和命名空间,以及 globalnonlocal 如何影响变量绑定

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

示例代码的输出是

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

注意 局部 赋值(默认)如何没有改变 scope_testspam 的绑定。nonlocal 赋值改变了 scope_testspam 的绑定,而 global 赋值改变了模块级别的绑定。

您还可以看到,在 global 赋值之前,没有 spam 的先前绑定。

9.3. 初识类

类引入了一些新的语法,三种新的对象类型,以及一些新的语义。

9.3.1. 类定义语法

最简单的类定义形式如下所示

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

类定义,如同函数定义(def 语句),必须在它们生效之前执行。(你可以想象将类定义放在 if 语句的一个分支中,或在一个函数内部。)

实际上,类定义中的语句通常是函数定义,但也允许其他语句,并且有时很有用——我们稍后会再讨论这个问题。类内部的函数定义通常具有一种特殊形式的参数列表,由方法的调用约定决定——同样,这将在后面解释。

当进入类定义时,会创建一个新的命名空间,并用作局部作用域——因此,所有对局部变量的赋值都进入这个新的命名空间。特别是,函数定义在这里绑定新函数的名称。

当类定义正常退出(通过结束)时,会创建一个 类对象。这基本上是围绕类定义创建的命名空间内容的包装;我们将在下一节中了解更多关于类对象的信息。原始的局部作用域(在进入类定义之前生效的那个)被恢复,并且类对象在这里绑定到类定义头中给出的类名(示例中的 ClassName)。

9.3.2. 类对象

类对象支持两种操作:属性引用和实例化。

属性引用 使用 Python 中所有属性引用的标准语法:obj.name。有效的属性名称是类对象创建时类命名空间中的所有名称。因此,如果类定义如下所示

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

那么 MyClass.iMyClass.f 是有效的属性引用,分别返回一个整数和一个函数对象。类属性也可以被赋值,因此你可以通过赋值改变 MyClass.i 的值。__doc__ 也是一个有效属性,返回属于该类的文档字符串:"A simple example class"

实例化 使用函数符号。只需假装类对象是一个无参数函数,它返回该类的一个新实例。例如(假设上述类)

x = MyClass()

创建该类的一个新 实例 并将此对象赋值给局部变量 x

实例化操作(“调用”一个类对象)创建一个空对象。许多类喜欢创建具有特定初始状态的自定义实例对象。因此,一个类可以定义一个名为 __init__() 的特殊方法,像这样

def __init__(self):
    self.data = []

当一个类定义了 __init__() 方法时,类实例化会自动为新创建的类实例调用 __init__()。因此在这个例子中,可以通过以下方式获得一个新的、已初始化的实例:

x = MyClass()

当然,__init__() 方法可以有参数以增加灵活性。在这种情况下,传递给类实例化操作符的参数会传递给 __init__()。例如,

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. 实例对象

现在我们可以用实例对象做什么呢?实例对象唯一理解的操作是属性引用。有两种有效的属性名称:数据属性和方法。

数据属性 对应于 Smalltalk 中的“实例变量”,以及 C++ 中的“数据成员”。数据属性不需要声明;像局部变量一样,它们在第一次被赋值时就存在了。例如,如果 x 是上面创建的 MyClass 的实例,那么以下代码将打印值 16,不留痕迹

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

另一种实例属性引用是 方法。方法是“属于”一个对象的函数。

实例对象的有效方法名称取决于其类。根据定义,类的所有函数对象属性都定义了其实例的相应方法。因此在我们的示例中,x.f 是一个有效的方法引用,因为 MyClass.f 是一个函数,而 x.i 不是,因为 MyClass.i 不是。但是 x.fMyClass.f 不同——它是一个 方法对象,而不是一个函数对象。

9.3.4. 方法对象

通常,方法在绑定后立即调用

x.f()

如果 x = MyClass(),如上所述,这将返回字符串 'hello world'。然而,不必立即调用方法:x.f 是一个方法对象,可以存储起来稍后调用。例如

xf = x.f
while True:
    print(xf())

将继续打印 hello world 直到永远。

方法被调用时究竟发生了什么?您可能已经注意到,x.f() 在上面被调用时没有参数,即使函数 f() 的定义指定了一个参数。参数去哪儿了?当然,当一个需要参数的函数在没有参数的情况下被调用时,Python 会引发异常——即使该参数实际上没有被使用……

实际上,您可能已经猜到答案了:方法的特殊之处在于实例对象作为函数的第一个参数传递。在我们的示例中,调用 x.f() 完全等同于 MyClass.f(x)。通常,使用 n 个参数列表调用方法等同于使用通过在第一个参数之前插入方法的实例对象而创建的参数列表调用相应的函数。

通常,方法的工作方式如下。当引用实例的非数据属性时,会搜索实例的类。如果名称表示一个有效的类属性,并且它是一个函数对象,则实例对象和函数对象的引用会被打包成一个方法对象。当方法对象与参数列表一起调用时,会根据实例对象和参数列表构建一个新的参数列表,并使用这个新的参数列表调用函数对象。

9.3.5. 类变量和实例变量

一般来说,实例变量用于每个实例独有的数据,而类变量用于类的所有实例共享的属性和方法

class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

关于名称和对象 中所讨论的,共享数据对于涉及 可变 对象(如列表和字典)的情况可能会产生令人惊讶的影响。例如,以下代码中的 tricks 列表不应作为类变量使用,因为所有 Dog 实例都将共享一个列表

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

正确的类设计应该改用实例变量

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4. 随笔

如果实例和类中出现相同的属性名称,则属性查找会优先考虑实例

>>> class Warehouse:
...    purpose = 'storage'
...    region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

数据属性既可以被方法引用,也可以被对象的普通用户(“客户端”)引用。换句话说,类不能用于实现纯抽象数据类型。实际上,Python 中没有任何东西可以强制执行数据隐藏——它完全基于约定。(另一方面,用 C 语言编写的 Python 实现可以完全隐藏实现细节并控制对对象的访问;这可以被用 C 语言编写的 Python 扩展所使用。)

客户在使用数据属性时应小心谨慎——客户可能会通过践踏其数据属性来破坏方法所维护的不变量。请注意,客户可以在不影响方法有效性的情况下,向实例对象添加自己的数据属性,只要避免名称冲突即可——同样,命名约定可以在这里省去很多麻烦。

在方法内部,没有引用数据属性(或其他方法!)的简写形式。我发现这实际上提高了方法的代码可读性:在浏览方法时,不会混淆局部变量和实例变量。

通常,方法的第一个参数被称为 self。这不过是一个约定:名称 self 对 Python 而言绝对没有特殊含义。但是请注意,如果不遵循此约定,您的代码对其他 Python 程序员而言可能可读性较差,而且也可以想象会有依赖于此约定的 类浏览器 程序。

任何作为类属性的函数对象都定义了该类实例的一个方法。函数定义不必在文本上包含在类定义中:将函数对象赋值给类中的局部变量也是可以的。例如

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

现在 fgh 都是类 C 的属性,它们引用函数对象,因此它们都是 C 实例的方法——h 完全等同于 g。请注意,这种做法通常只会使程序的读者感到困惑。

方法可以通过使用 self 参数的方法属性来调用其他方法

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

方法可以像普通函数一样引用全局名称。与方法关联的全局作用域是包含其定义的模块。(类从不用作全局作用域。)虽然很少遇到在方法中使用全局数据的好理由,但全局作用域有许多合法用途:例如,导入到全局作用域的函数和模块可以被方法使用,以及在其中定义的函数和类。通常,包含该方法的类本身是在此全局作用域中定义的,在下一节中,我们将找到一些方法想要引用其自身类的充分理由。

每个值都是一个对象,因此它有一个 (也称为其 类型)。它存储为 object.__class__

9.5. 继承

当然,如果不支持继承,语言特性就不配被称为“类”。派生类的定义语法如下所示

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

名称 BaseClassName 必须在派生类定义所在作用域可访问的命名空间中定义。除了基类名称,也允许其他任意表达式。例如,当基类在另一个模块中定义时,这可能很有用

class DerivedClassName(modname.BaseClassName):

派生类的定义执行与基类相同。当构造类对象时,基类会被记住。这用于解析属性引用:如果在类中未找到请求的属性,则搜索将继续在基类中查找。如果基类本身派生自其他类,则此规则递归应用。

派生类的实例化没有什么特别之处:DerivedClassName() 创建一个该类的新实例。方法解析如下:搜索相应的类属性,必要时沿着基类链向下,如果找到一个函数对象,则该方法引用是有效的。

派生类可以覆盖其基类的方法。因为方法在调用同一对象的其他方法时没有特殊特权,所以基类中调用在同一基类中定义的另一个方法的方法可能会最终调用覆盖它的派生类的方法。(对于 C++ 程序员:Python 中的所有方法都实际上是 virtual。)

派生类中的覆盖方法实际上可能想要扩展而不是简单地替换同名的基类方法。有一种简单的方法可以直接调用基类方法:只需调用 BaseClassName.methodname(self, arguments)。这对于客户端偶尔也很有用。(请注意,这仅在基类在全局作用域中可作为 BaseClassName 访问时才有效。)

Python 有两个与继承一起使用的内置函数

  • 使用 isinstance() 检查实例的类型:isinstance(obj, int) 将只在 obj.__class__int 或从 int 派生出的某个类时才为 True

  • 使用 issubclass() 检查类继承:issubclass(bool, int)True,因为 boolint 的子类。但是,issubclass(float, int)False,因为 float 不是 int 的子类。

9.5.1. 多重继承

Python 也支持一种多重继承形式。具有多个基类的类定义如下所示

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

在最简单的情况下,对于大多数目的,您可以将从父类继承属性的搜索视为深度优先、从左到右,在层次结构有重叠的同一类中不再搜索两次。因此,如果 DerivedClassName 中未找到属性,则会在 Base1 中搜索,然后(递归地)在 Base1 的基类中搜索,如果仍未找到,则在 Base2 中搜索,依此类推。

事实上,这比那稍微复杂一些;方法解析顺序动态变化,以支持对 super() 的协作调用。这种方法在其他一些多重继承语言中被称为 call-next-method,并且比单继承语言中的 super 调用更强大。

动态排序是必要的,因为所有多重继承的情况都表现出一个或多个菱形关系(其中至少一个父类可以通过多条路径从最底层的类访问)。例如,所有类都继承自 object,因此任何多重继承的情况都提供了多条路径来达到 object。为了防止基类被多次访问,动态算法以一种保持每个类中指定的从左到右排序、只调用每个父类一次并且是单调的方式线性化搜索顺序(这意味着一个类可以被子类化而不影响其父类的优先顺序)。总而言之,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。有关更多详细信息,请参阅 Python 2.3 方法解析顺序

9.6. 私有变量

Python 中不存在除了对象内部之外无法访问的“私有”实例变量。但是,大多数 Python 代码都遵循一个约定:前缀为下划线的名称(例如 _spam)应被视为 API 的非公共部分(无论是函数、方法还是数据成员)。它应被视为实现细节,并且可能在不通知的情况下更改。

由于存在对类私有成员的有效用例(即避免名称与子类定义的名称冲突),因此对此机制提供了有限的支持,称为 名称修饰。任何形式为 __spam(至少两个前导下划线,最多一个尾随下划线)的标识符都会在文本上替换为 _classname__spam,其中 classname 是当前类名,并去掉了前导下划线。只要标识符出现在类定义中,此修饰就不考虑其语法位置。

参见

有关详细信息和特殊情况,请参见 私有名称修饰规范

名称修饰有助于子类覆盖方法而不破坏类内方法调用。例如

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

即使 MappingSubclass 引入一个 __update 标识符,上述示例仍然有效,因为它在 Mapping 类中被替换为 _Mapping__update,在 MappingSubclass 类中被替换为 _MappingSubclass__update

请注意,修饰规则主要是为了避免意外;仍然可以访问或修改被认为是私有的变量。这在特殊情况下甚至可能很有用,例如在调试器中。

请注意,传递给 exec()eval() 的代码不会将调用类的类名视为当前类;这类似于 global 语句的效果,其效果同样仅限于一起字节编译的代码。同样的限制适用于 getattr()setattr()delattr(),以及直接引用 __dict__ 时。

9.7. 杂项

有时,拥有类似于 Pascal 的“记录”或 C 的“结构”的数据类型很有用,它将一些命名数据项捆绑在一起。惯用的方法是为此目的使用 dataclasses

from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    dept: str
    salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000

一段期望特定抽象数据类型的 Python 代码通常可以传入一个模拟该数据类型方法的类。例如,如果你有一个函数可以格式化来自文件对象的一些数据,你可以定义一个具有 read()readline() 方法的类,这些方法从字符串缓冲区获取数据,并将其作为参数传入。

实例方法对象 也有属性:m.__self__ 是方法 m() 所在的实例对象,m.__func__ 是与方法对应的 函数对象

9.8. 迭代器

现在你可能已经注意到,大多数容器对象都可以使用 for 语句进行循环

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

这种访问方式清晰、简洁且方便。迭代器的使用贯穿并统一了 Python。在幕后,for 语句在容器对象上调用 iter()。该函数返回一个迭代器对象,该对象定义了方法 __next__(),该方法一次访问容器中的一个元素。当没有更多元素时,__next__() 引发 StopIteration 异常,告诉 for 循环终止。您可以使用内置函数 next() 调用 __next__() 方法;这个例子展示了这一切是如何工作的

>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

在了解了迭代器协议的机制之后,将迭代器行为添加到您的类中就很容易了。定义一个 __iter__() 方法,它返回一个带有 __next__() 方法的对象。如果类定义了 __next__(),那么 __iter__() 可以直接返回 self

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

9.9. 生成器

生成器 是创建迭代器的一个简单而强大的工具。它们像普通函数一样编写,但在需要返回数据时使用 yield 语句。每次调用 next() 时,生成器都会从上次离开的地方恢复(它会记住所有数据值以及最后执行的语句)。一个示例表明,生成器可以非常容易地创建

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

使用生成器可以完成的所有事情,也可以使用上一节中描述的基于类的迭代器来完成。生成器如此紧凑的原因是 __iter__()__next__() 方法是自动创建的。

另一个关键特性是局部变量和执行状态在调用之间会自动保存。这使得函数更容易编写,并且比使用实例变量(如 self.indexself.data)的方法更清晰。

除了自动方法创建和保存程序状态之外,当生成器终止时,它们会自动引发 StopIteration。这些特性结合在一起,使得创建迭代器变得非常容易,无需比编写普通函数更多的努力。

9.10. 生成器表达式

一些简单的生成器可以使用类似于列表推导但使用圆括号而不是方括号的语法简洁地编码为表达式。这些表达式设计用于生成器立即被封闭函数使用的情况。生成器表达式比完整的生成器定义更紧凑,但功能较少,并且往往比等效的列表推导更节省内存。

示例:

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> unique_words = set(word for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

脚注