Skip to main content

9. 类

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

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

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

9.1. 关于名称和对象的一个词

对象具有个性,并且多个名称(在多个范围中)可以绑定到同一个对象。这在其他语言中称为别名。这通常不是首先看一看Python,当处理不可变的基本类型(数字,字符串,元组)时可以安全地忽略。然而,别名对于涉及可变对象(例如列表,字典和大多数其他类型)的Python代码的语义可能有令人惊讶的影响。这通常用于程序的利益,因为别名在某些方面表现得像指针。例如,传递一个对象是很便宜的,因为只有一个指针由实现传递;并且如果函数修改作为参数传递的对象,则调用者将看到改变 - 这消除了对于如在Pascal中的两个不同的参数传递机制的需要。

9.2. Python范围和命名空间

在介绍类之前,我首先要告诉你一些关于Python的范围规则。类定义对命名空间使用一些简单的技巧,你需要知道范围和命名空间如何工作,以完全了解发生了什么。顺便说一下,关于这个主题的知识对任何高级Python程序员都很有用。

让我们从一些定义开始。

namespace 是从名称到对象的映射。大多数命名空间目前实现为Python字典,但通常不会以任何方式(性能除外)显着,并且它可能会在将来更改。命名空间的示例是:内置名称集(包含诸如 abs() 的函数和内置异常名称);模块中的全局名称;和函数调用中的本地名称。在某种意义上,对象的属性集合也形成一个命名空间。了解命名空间的重要事情是,在不同的命名空间中的名称之间绝对没有关系;例如,两个不同的模块可以都定义函数 maximize 而不会混淆 - 模块的用户必须用模块名称作为前缀。

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

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

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

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

scope 是Python程序的文本区域,其中命名空间是可直接访问的。 “直接可访问”在这里意味着对名称的非限定引用试图在命名空间中找到该名称。

虽然范围是静态确定的,但它们是动态使用的。在执行期间的任何时候,至少有三个嵌套的作用域,其命名空间可以直接访问:

  • 最先搜索的最内层包含本地名称

  • 从最近的封闭范围开始搜索的任何封闭函数的范围都包含非本地名称,但也包含非全局名称

  • 最后一个作用域包含当前模块的全局名称

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

如果一个名称被声明为全局的,那么所有的引用和赋值直接到包含模块的全局名称的中间作用域。要重新绑定在最内层范围之外发现的变量,可以使用 nonlocal 语句;如果没有声明为非局部变量,这些变量是只读的(尝试写入这样的变量将简单地创建一个 new 局部变量在最深的范围,留下相同命名的外部变量不变)。

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

重要的是要意识到范围是文本决定的:在模块中定义的函数的全局范围是该模块的命名空间,不管从什么别名,函数被调用。另一方面,实际搜索名称是在运行时动态完成的,但是,在“编译”时,语言定义正在向静态名称解析演进,因此不要依赖动态名称解析! (事实上,局部变量已经静态确定。)

Python的一个特殊的奇怪是 - 如果没有 global 语句生效 - 对名称的分配总是进入最深的范围。分配不复制数据—它们只是将名称绑定到对象。删除同样如此:语句 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" 的docstring。

instantiation 类使用函数符号。只是假装类对象是一个无参数的函数,返回一个新的类的实例。例如(假设上面的类):

x = MyClass()

创建该类的新 instance,并将此对象分配给本地变量 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

另一种实例属性引用是 method。方法是“属于”对象的函数。 (在Python中,术语方法对类实例不是唯一的:其他对象类型也可以有方法。例如,list对象有称为append,insert,remove,sort等方法,我们将使用术语方法专门用于表示类实例对象的方法,除非另有明确说明。

实例对象的有效方法名称取决于其类。根据定义,作为函数对象的类的所有属性定义其实例的相应方法。所以在我们的例子中,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'

关于名称和对象的一个词 中所讨论的,共享数据可能具有涉及 mutable 对象(例如列表和字典)的可能令人惊讶的效果。例如,以下代码中的 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. 随机备注

数据属性覆盖具有相同名称的方法属性;以避免意外的名称冲突,这可能导致在大型程序中难以找到的错误,使用某种最小化冲突几率的约定是明智的。可能的约定包括大写方法名称,用小唯一字符串(可能只是下划线)前缀数据属性名称,或者对数据属性使用方法和名词的动词。

数据属性可以由方法以及对象的普通用户(“客户端”)引用。换句话说,类不能用于实现纯抽象数据类型。事实上,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)

方法可以以与普通函数相同的方式引用全局名称。与方法相关联的全局范围是包含其定义的模块。 (一个类从不用作全局范围。)虽然很少会遇到在方法中使用全局数据的好理由,但是有很多合法的使用全局范围:一方面,导入到全局范围的函数和模块被方法使用,以及在其中定义的函数和类。通常,包含方法的类本身在此全局范围中定义,在下一节中,我们将找到一个方法想要引用其自己的类的一些好的原因。

每个值都是一个对象,因此具有 class (也称为 type)。它存储为 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() 检查实例的类型:只有 obj.__class__int 或从 int 派生的某些类,isinstance(obj, 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,比单继承语言中的超级调用更强大。

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

9.6. 私有变量

除了在对象内部不能访问的“私有”实例变量在Python中不存在。但是,大多数Python代码遵循的约定是:以下划线(例如 _spam)为前缀的名称应该被视为API的非公开部分(无论它是函数,方法还是数据成员) 。它应被视为实施细节,如有更改,恕不另行通知。

由于类私有成员有一个有效的用例(即避免名称与子类定义的名称冲突),因此对这种机制(称为 name mangling)的支持有限。任何形式 __spam (至少两个前导下划线,最多一个尾部下划线)的标识符在文本上被替换为 _classname__spam,其中 classname 是带有前导下划线的当前类名。这种修改不考虑标识符的句法位置,只要它发生在类的定义内。

名称整理有助于让子类重写方法,而不会打断intraclass方法调用。例如:

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)

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

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

9.7. 什物

有时,有一个类似于Pascal“record”或C“struct”的数据类型,绑定在一起的几个命名的数据项是有用的。一个空的类定义会很好:

class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
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
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
    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. 发电机

Generator 是一个用于创建迭代器的简单而强大的工具。它们写成常规函数,但是每当他们想返回数据时使用 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

>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}

>>> 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']

脚注

[1]

除了一件事。模块对象有一个称为 __dict__ 的秘密只读属性,它返回用于实现模块命名空间的字典;名称 __dict__ 是一个属性,但不是一个全局名称。显然,使用这种方法违反了命名空间实现的抽象,应该局限于事后调试器之类的东西。