9. Classes

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

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

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

(缺乏普遍接受的术语来谈论课堂,我偶尔会使用SimultTalk和C++术语。我将使用Muldia-3术语,因为它的面向对象语义比Python更接近C++,但我希望很少有读者能听到它。

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

对象具有个性,多个名称(在多个作用域中)可以绑定到同一对象。这在其他语言中称为别名。第一眼看到python时通常不了解这一点,在处理不可变的基本类型(数字、字符串、元组)时可以安全地忽略这一点。但是,别名可能对涉及可变对象(如列表、字典和大多数其他类型)的Python代码的语义产生令人惊讶的影响。这通常用于程序的好处,因为别名在某些方面的行为类似于指针。例如,传递一个对象很便宜,因为实现只传递一个指针;如果一个函数修改作为参数传递的对象,调用者会看到变化---这就消除了对两种不同的参数传递机制的需要,就像在pascal中一样。

9.2. python范围和名称空间

在介绍类之前,我首先要告诉您一些关于Python作用域规则的事情。类定义对名称空间起到了一些巧妙的作用,您需要知道范围和名称空间是如何工作的,才能完全理解正在发生的事情。顺便说一句,关于这个主题的知识对于任何高级的Python程序员都是有用的。

让我们从一些定义开始。

A 命名空间 是从名称到对象的映射。大多数名称空间目前都是作为Python字典实现的,但这通常在任何方面都不明显(除了性能),而且将来可能会发生变化。名称空间的例子有:一组内置名称(包含诸如 abs() 和内置异常名称);模块中的全局名称;函数调用中的本地名称。从某种意义上说,对象的一组属性也构成了一个名称空间。了解名称空间的重要一点是,不同名称空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义一个函数 maximize 没有混淆---模块的用户必须在其前面加上模块名。

顺便说一下,我用这个词 属性 对于点后面的任何名称——例如,在表达式中 z.realreal 是对象的属性 z . 严格来说,对模块中名称的引用是属性引用:在表达式中 modname.funcnamemodname 是模块对象,并且 funcname 是它的一个属性。在这种情况下,模块属性和模块中定义的全局名称之间正好有一个简单的映射:它们共享同一个命名空间! 1

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

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

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

A 范围 是一个python程序的文本区域,其中可以直接访问命名空间。”这里的“直接访问”意味着对名称的非限定引用试图在名称空间中查找名称。

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

  • 首先搜索的最里面的作用域包含本地名称

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

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

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

如果一个名称被声明为全局名称,那么所有引用和分配将直接转到包含模块全局名称的中间作用域。要重新绑定在最内部范围之外找到的变量,请 nonlocal 可以使用语句;如果未声明为非本地变量,则这些变量是只读的(尝试写入此类变量只会创建一个 new 最内部作用域中的局部变量,保持同名外部变量不变)。

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

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

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_test 绑定 spam . 这个 nonlocal 工作分配已更改 scope_test 绑定 spamglobal 分配更改了模块级绑定。

您还可以看到以前没有针对的绑定 spam 之前 global 指派。

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__ 也是一个有效属性,返回属于该类的docstring: "A simple example class" .

等级 实例化 使用函数表示法。假设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. 实例对象

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

数据属性 对应SimultSalk中的“实例变量”和C++中的“数据成员”。数据属性不需要声明;与局部变量一样,它们在第一次被分配给时就出现了。例如,如果 x 是的实例 MyClass 上面创建的,下面的代码将打印该值 16 ,不留痕迹:

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

另一种实例属性引用是 方法 . 方法是“属于”对象的函数。(在python中,术语method对于类实例不是唯一的:其他对象类型也可以有方法。例如,列表对象具有名为append、insert、remove、sort等的方法。但是,在下面的讨论中,我们将专门使用术语method来表示类实例对象的方法,除非另有明确说明。)

实例对象的有效方法名取决于其类。根据定义,作为函数对象的类的所有属性定义其实例的相应方法。在我们的例子中, x.f 是有效的方法引用,因为 MyClass.f 是一个函数,但是 x.i 不是,因为 MyClass.i 不是。但是 x.f 不是同一件事 MyClass.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 列表和字典等对象。例如, 技巧 以下代码中的列表不应用作类变量,因为只有一个列表将由所有人共享 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)

方法引用全局名称的方式与普通函数相同。与方法关联的全局范围是包含其定义的模块。(类从不用作全局作用域。)虽然在方法中很少遇到使用全局数据的好理由,但全局作用域有许多合法用途:一方面,导入到全局作用域的函数和模块可以由方法使用,也可以由方法中定义的函数和类使用。通常,包含方法的类本身就是在这个全局范围内定义的,在下一节中,我们将找到一些好的理由来解释为什么一个方法要引用它自己的类。

每个值都是一个对象,因此 classes (也称为ITS 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() 要检查实例的类型: isinstance(obj, int)True 只有 obj.__class__int 或者从 int .

  • 使用 issubclass() 检查类继承: issubclass(bool, int)True 自从 bool 是的子类 int .然而, 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方法,它比单继承语言中的超级调用更强大。

动态排序是必要的,因为所有多重继承的情况都显示一个或多个菱形关系(其中至少一个父类可以通过最底层的类的多个路径访问)。例如,所有类都继承自 object ,因此,任何多个继承的情况都提供了多个要访问的路径 object . 为了防止基类被多次访问,动态算法将搜索顺序线性化,以保持每个类中指定的从左到右的顺序,只调用每个父类一次,并且是单调的(也就是说,可以在不影响父类优先顺序的情况下对类进行子类化)。综上所述,这些属性使得设计具有多个继承的可靠的可扩展类成为可能。有关详细信息,请参阅https://www.python.org/download/releases/2.3/mro/。

9.6. 私有变量

“private”实例变量只能从对象内部访问,但在python中不存在。但是,大多数python代码后面都有一个约定:以下划线作为前缀的名称(例如 _spam )应该被视为API的非公共部分(无论它是函数、方法还是数据成员)。应将其视为实施细节,如有更改,恕不另行通知。

由于类私有成员有一个有效的用例(即避免名称与由子类定义的名称冲突),因此对这种机制的支持有限,称为 name mangling .表单的任何标识符 __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__updateMapping 类和 _MappingSubclass__updateMappingSubclass 分别为类。

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

注意代码传递给 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 <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. 生成器

Generators 是创建迭代器的简单而强大的工具。它们像常规函数一样编写,但使用 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']

脚注

1

除了一件事。模块对象具有一个称为 __dict__ 返回用于实现模块命名空间的字典;名称 __dict__ 是属性,但不是全局名称。显然,使用它违反了名称空间实现的抽象,应该限制在事后调试程序之类的东西上。