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 个嵌套作用域,其命名空间可以直接访问。

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

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

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

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

如果一个名称被声明为全局的,那么所有引用和赋值都会直接到包含模块全局名称的倒数第二个作用域。要重新绑定在最内层作用域之外找到的变量,可以使用 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

请注意,local 赋值(这是默认值)没有改变 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()

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 实例的方法——hg 完全等效。请注意,这种做法通常只会让程序的读者感到困惑。

方法可以通过使用 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']

脚注