[译]The Python Tutorial#Classes

写在前面

本篇文章是《The Python Tutorial》(3.6.1),第九章,的译文。

9. Classes

与其他编程语言相比,Python的类机制定义类时,最小化了新的语法和语义的引入。Python类机制是C++Modula-3的混合体。Python类支持所有面向对象编程的特性:

类继承机制允许多继承,子类可以覆盖其父类们的任何方法,方法可以使用相同的名字调用父类中的方法。对象可以包含任意数量和类型的数据。

跟模块相似,Python类也具有Python的动态性质:

类在运行时被创建,在创建之后可被修改

用在C++的术语来说,在Python中,通常类成员(包括数据成员)是public(公有)的(除了见下文的Private Variables),并且所有成员方法是virtual(虚拟)的。跟Modual-3一样,从对象的方法中引用其成员没有直接的方法:方法成员声明时带有一个显式的表示对象自身的参数,这个参数在参数列表第一位,并且在调用时隐式提供。与Smalltalk语言类似,类自身也是对象。这为importing(导入)和renaming(重命名)提供了语义支持。与C++Modula-3不同的是,built-in(内建)类型可以被程序员用作父类来扩展。当然,与C++类似,大多数带有特殊语义的內建运算符(比如算数运算符,下标运算符)可以被重定义为类实例。

(由于缺乏关于类的普遍接受的通用术语,作者会偶尔使用C++Smalltalk的相关项。由于Modula-3的面相对象语义与Python更接近,作者会使用Modula-3的一些东西,但是恐怕少数读者听说过。)

9.1. A Word About Names and Objects

对象具有其特性,不同作用域的各种名字或者相同作用域的不同名字都可以绑定到同一个对象上。在其他语言中这被称作别名。这种特性在初识Python时不易理解,而且在处理不可变基本类型(如数字,字符串,元组)时可以被安全的忽视。然而,当涉及到可变对象时,比如列表,字典和其他大多数类型,别名可能会对Python的语义产生意想不到的影响。由于别名在某些方面就像指针一样,通常在一些地方会使程序受益。比如,传递一个对象很容易,因为在实现上只是传递了一个指针;如果一个函数修改了参数对象,修改对调用者可见(译注:可变对象)——这消除了在Pascal中需要两个不同参数的传参机制的需求。

9.2. Python Scopes and Namespaces

在介绍类之前,先介绍Python作用域的相关规则。类定义和命名空间之间有一些巧妙的联系,理解了作用域以及命名空间的机制之后才能完全理解类的一些行为。此外,这个主题的知识对高级Python程序员也是有帮助的。

我们以一些定义开始:

namespace(命名空间)是从名字到对象的映射。目前大多数的命名空间都是由Python的字典实现的,但是实现方式通常都是不重要的,并且将来或许会改变命名空间的实现方式。命名空间的一些例子:built-in名字的集合(包括如abs()的函数以及built-in的异常名字);模块中的global(全局)名字;函数调用中的local局部名字等。在某种意义上,对象的属性集合同样构成了一个命名空间。关于命名空间比较重要的特征是:不同命名空间中的名字绝对没有任何联系;比如,两个不同模块可以定义同名函数maximize,不会有任何冲突——模块的使用者必须使用模块名字为前缀来调用它们。

顺便说一下,我使用attribute(属性)这个词来描述任何跟在.后面的名字,例如,在表达式z.real中,real是对象z的属性。严格来说,在模块中引用名字是属性的引用:在表达式modname.funcnamemodname是一个模块对象,funcname是模块对象的一个属性。因此,模块的属性和定义在模块中的全局名字之间有直接的映射关系:它们共享同一个命名空间[1]

属性也许是只读或者可写的。在后面的例子中,对属性赋值是可行的。模块的属性是可写的:可以写出modname.the_answer = 42这样的语句。可写属性也可以使用del语句来删除。例如,del modname.the_answer会从名字为modname的对象中移除the_answer属性。

命名空间在不同的时刻被创建,并且具有不同的生命周期。包含built-in(内建)名字的命名空间Python解释器启动时创建,并且不会被删除。模块的全局命名空间在模块定义读入时创建;通常来说,模块的命名空间也是在解释器退出时销毁。在解释器最高层调用执行的语句,无论是从脚本文件读入还是交互输入的,被认为是一个叫做__main__模块的一部分,因此这些语句有自己的全局命名空间(实际上内建名字也在一个模块中,这个模块名字叫做builtins)

函数的局部namespace在函数调用时创建,函数返回或者抛出未在函数中处理的异常时销毁。(实际上,忽略掉函数的namespace能更好理解函数调用实际发生的事情) 当然,每一次递归调用都有自己的局部namespace

scope(作用域)是一个可以直接访问命名空间的Python程序文本区域。这里的"直接访问"的意思是对一个名字的无限定引用会尝试在命名空间中查找这个名字 (译注:限定引用是通过对象.属性的方式,无限定引用是直接写名字引用的方式)

虽然定义作用域是静态的,但是被动态的使用。在程序执行的任何时间内,至少有三个命名空间可以被直接访问的嵌套的作用域:

  • 首先搜索,包含局部名字的最内层作用域
  • 根据嵌套层次从内到外搜索,包含非局部也非全局名字的任意封闭函数的作用域
  • 倒数第二次被搜索,包含当前模块全局名字的作用域
  • 最后被搜索,包含内建名字的最外层作用域

(译注:这就是所谓LEGB - local, enclosing, global, buit-in搜索规则,这四个指的是作用域而非命名空间)

如果名字是全局的,那么对名字所有的引用和赋值操作都会到包含这个模块全局名字的作用域搜索。若要重新绑定在最内层作用域之外变量,可以使用nonlocal语句;如果该变量未声明为nonlocal,那么变量只读(对变量写操作会在最内层作用域创建一个新的局部变量,外部的变量不会有任何改变)。

通常,局部作用域引用函数的局部名字(译注:局部作用域引用函数的命名空间)。在函数之外,局部作用域和全局作用域引用相同的命名空间:模块命名空间。然而,类定义将命名空间关联到局部作用域中。
(译注:类的定义创建一个命名空间,并把它关联到局部作用域上,这一点非常重要)

作用域源于程序文本上的意义:无论函数从哪儿或者以何种别名被调用,定义在模块中的函数的全局作用域是该模块的命名空间,这一点非常重要!另一方面,实际的名字搜索是在动态执行的,在运行时确定;然而,语言定义不断朝着静态名字解决方案发展,未来可能在编译时确定。因此不要依赖名字的动态搜索。(实际上,局部变量已经静态确定了。)

Python有一个特别的地方:如果没有使用global语句声明变量,那么对于其的赋值语句会影响最内层作用域(译注:没用使用global语句声明该名字是全局名字时,在局部作用域对其进行赋值操作,会在局部作用域对应的命名空间创建一个同名的变量。) 赋值操作从不拷贝数据,只是简单的将名字绑定到对象上。对于删除操作也一样:语句del x移除局部作用域命名空间x的绑定关系。实际上,所有引入新的名字的操作都是用的是局部作用域:特别地,import语句以及函数定义也将模块或者函数名字绑定到局部作用域。(译注:这就是使用导入其他模块后,不能直接引用导入模块成员的原因,而要使用模块.属性的方式)

可以使用global语句来表明特定的变量属于全局作用域,并且应该重新绑定;nonlocal语句表明特定的变量是enclosing作用域,并且应该重新绑定。

9.2.1. Scopes and Namespaces Example

以示例说明了如何引用不同作用域命名空间,以及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. A First Look at Classes

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

9.3.1. Class Definition Syntax

最简单的类定义如下:

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

类定义与函数定义(def语句)相似,必须在生效之前执行。(可以将类定义放在if的分支或者函数中来证明这个问题。)

在实践中,类中的语句通常都是函数定义,但是其他语句也是允许的,有时候也是有用的——我们后面介绍。类中的函数定义通常有一个特殊形式的参数列表,参数受方法约定调用的影响——当然,这些后面会解释。

类定义开始后,会创建一个作为局部作用域的新命名空间——因此,所有局部变量的赋值操作都在这个命名空间中。特别地,函数定义在这个命名空间中绑定了新的名字。
类定义完成后,创建了一个类对象。这个类对象基本上就是对类定义创建的命名空间内容的包装;我们在接下来的章节中会深入学习。类定义完成后,原始的局部作用域(类定义进入之前生效的局部作用域)得到恢复,类对象绑定到了类定义头部的名字(示例中是ClassName)

(译注:
在一个模块中定义类,从书写下 class ClassName:开始,类定义开始,此时创建一个新的命名空间作为局部作用域,注意作用域只是程序文本上的意义。接下来的所有赋值语句,函数定义都绑定在这个新的命名空间。按照Python一切皆对象的哲学,类也不例外。类定义结束后,一个类对象被创建,这个对象被绑定到值为类名的名字上,即有了类名->类对象这一个映射关系,这个映射关系绑定在原始的命名空间中。类对象是对类定义时创建的那个命名空间的包装。类定义结束后,原来的局部作用域就恢复了。
)

9.3.2. Class Objects

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

对象属性引用 使用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. Instance Objects

现在我们可以使用实例对象做什么?实例对象接受的唯一操作就是属性引用。有两种类型的有效属性名字,数据属性和方法(译注:这里的方法与函数是不一样的)

数据属性相当于Smalltalk中的"实例变量",以及C++中的"数据成员"。数据属性无需声明;像局部变量一样,它们在第一次赋值时就会绑定到相应对象。例如,如果x是上面创建的MyClass的实例,下面的代码段会打印16,不会抛出异常:

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

另一种实例属性引用是 方法。方法是“属于”对象的函数。(Python中,术语方法不是类实例独有的:其他对象类型也可以有方法。例如,list对象有append, insert, remove, sort等方法。然而,在接下来的讨论中,我们使用术语方法特指类实例的方法,除非另有明确说明。)

实例对象有效的方法名取决于它的类。按照定义,类中所有的函数对象对应类实例的方法。因此在例子中,x.f是有效的方法引用,因为MyClass.f是一个函数,但是x.i不是有效方法引用,因为MyClass.i不是函数。但是x.fMyClass.f是不一样的,它是一个方法对象,而不是函数对象。(译注:Python中一切皆对象,有对象就有对象对应的类,方法和函数是对应不同的类)

9.3.4. Method Objects

通常,方法通过写在其绑定名字右边的方式调用:

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个参数的方法,等同于重新构造参数列表调用方法对应的函数,新的参数列表在原来的参数列表的第一位插入方法所属实例对象。

如果你仍然不理解方法是如何工作的,了解其实现原理也许能澄清原因。引用非数据属性的实例属性时,会搜索它对应的类。如果名字是一个有效的函数对象,Python会将实例对象连同函数对象打包到一个抽象的对象中并且依据这个对象创建方法对象:这就是被调用的方法对象。当使用参数列表调用方法对象时,会使用实例对象以及原有参数列表构建新的参数列表,并且使用新的参数列表调用函数对象。

9.3.5. Class and Instance Variables

类变量以及实例变量
通常来说,实例变量是对于每个实例都独有的数据,而类变量是该类所有实例共享的属性和方法:

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'

正如在A Word About Names and Objects中讨论的那样,涉及到如list列表和dict字典之类的可变对象时,共享的数据或许有一些意想不到的惊人影响。例如,下面代码中的trick列表不应该作为类变量使用,因为所有的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. Random Remarks

数据属性会覆盖同名方法属性;为避免意外的名字冲突,这些冲突或许会造成大型程序中极难排除的Bug,使用一些约定来减少冲突的几率是非常明智的。可能的约定包括大写方法名,给数据属性的名字建一个短小的唯一字符串(或许只是一个下划线)前缀,或者使用动词作为方法名,名词作为数据属性名。

数据属性可能被方法和对象的普通用户引用。换句话说,类不能用来实现纯粹的抽象数据类型。实际上,Python并没有强制数据隐藏的机制——一切都基于约定(另一方面,用C写的Python实现了完全隐藏实现细节并且在需要的情况下控制对象的访问权限;C写的Python扩展可以是使用这些特性)

客户应该谨慎地使用数据属性——客户在实例对象中加入自己的数据属性时,或许会把由方法维持的不变量搞糟。值得注意的是,只有避免了名字冲突,客户或许会在实例对象中加入自己的数据成员,而不影响方法的有效性。再次,命名约定可以解决许多头疼的问题。

在方法中没有通过名字直接引用数据属性(或者其他方法)的途径。(译注:直接途径是指只通过名字引用,如nameself.name不是直接引用。) 我认为这实际上增加了方法的可读性:当阅读方法时,不会混淆局部变量和实例变量。

通常,方法的第一个参数名叫self。这仅仅是一个约定:名字selfPython绝没有任何特殊含义。然而值得注意的是,若不遵循约定,你的代码对于其他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

现在f, gh都是指向函数对象,都是类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)

与使用普通函数一样,方法也可以以相同的方式引用全局名字。与方法关联的全局作用域是包含方法定义的模块。(类从来不会作为全局作用域使用。) 虽然在方法中使用全局数据不是一个明智的选择,但是全局作用域确实有许多合理的使用场景:首先,导入到全局作用域的函数和模块可以被方法,函数和定义在其中的类使用。通常,包含该方法的类也会自定义在这个全局作用域中,在后面的章节我们会介绍方法为什么会需要引用自己的类。

9.5. Inheritance

这一特性的语言,如果不支持继承,那这个特性就没有什么意义了。派生类定义的语法如下:

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

名字BaseClassName必须与派生类定义在同一个作用域中。在基类类名的位置,还可以使用其他表达式。当基类定义在其他模块中时,这一点非常有用:

class DerivedClassName(modname.BaseClassName):

派生类定义的执行过程与基类一样。派生类对象构造完成时,基类也被记住了。这被用来解析属性引用:如果被请求的属性在该类中没有找到,搜索继续在基类寻找。当基类本身派生于其他类时,会递归应用这个规则。

派生类的实例化并没有什么特殊之处:DrivedClassName()创建了一个新的类实例。方法引用按照如下解析:搜索对应类属性,必要时沿着类继承链向下搜索,如果找到对应函数对象,那这个方法引用就是有效的。

派生类可以重写其基类的方法。因为方法在调用相同对象的其他方法时没有特权,基类的一个方法调用基类的另一个方法时,可能最终会调用其派生类中被重写的方法。(对于C++程序员来说,Python中所有的方法实质上都是的。)

派生类中重写基类方法时,可能会扩展基类方法而不是简单的替换掉基类的同名方法。一个简单的方式可以直接调用基类方法:使用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. Multiple Inheritance

Python也支持多继承。多继承的类定义如下:

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

在大多数情况下,在最简单的情况中,可以认为搜索从父类继承的属性是深度优先,从左到右的,而不是继承有重叠时在同一个类中搜索两次。因此,如果一个属性在DerivedClassName中没有找到,接下来会在Base1中搜索,递归的在Base1的基类中搜索,如果还没有找到,会在Base2中搜索,以此类推。

实际上的搜索方式会比上述稍微复杂一些;为了支持super(),方法的解析顺序会动态变化。在其他一些多继承语言中,这种方式被称为call-next-method,这种方式比单继承语言中的super调用更加强大。

因为所有的多继承都存在一个或者多个菱形关系(至少一个父类可以通过从最底层类开始的多条路径达到),动态排序是必须的。比如,所有的类都继承自object,因此任何多继承都存在不止一条可达object的路径。为避免基类被重复访问,动态算法线性化了搜索顺序,这个算法维持在每个类中从左到右的顺序,每个父类只调用一次,并且是单调的(这意味着一个类被继承时,不会影响其父类的优先顺序。) 这些特性综合起来,使得设计出可靠且可扩展的多继承成为可能。更多详细信息,请参考:https://www.python.org/download/releases/2.3/mro/

9.6. Private Variables

Python中不存在只能在对象内部访问,而不能在外部访问的“私有”实例变量。然而,大多数Python代码都遵循了这样一个约定:以一个下划线为前缀的名字(如_spam)应该被当做是API非公有的部分(无论是函数,方法还是数据成员)。这个约定是实现细节,更改后不会通知。

由于类的私有成员有一个有效的用例(即为避免基类名字和子类名字的冲突),Python只对这个机制做了有限的支持,叫做名字改编。任意形如__spam(至少两个前置下划线,至多一个后置下划线)的标识符被替换为文本_classname__spamclassname是当前类的名字加上一个前置下划线。不论标识符的句法位置在哪儿,只要这种标识符出现在类定义中,形如__spam的名字都会被改编。

名字改编使得子类可以重写父类方法,而不会影响父类中的方法内部调用。例如:

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)

需要注意的是名字改编规则是为了避免意外而设计的:访问或者改变被认为是私有属性的值是可行的。甚至这在一些特定的环境下是可行的,比如在debugger时。

需要注意传递给exec()或者eval()的代码并不为认为用来调用类的类名是当前类;类似于global语句影响,字节编译的代码也受同样的限制。这同样作用于getattr(), setattr() 以及delattr()以及直接引用__dict__

9.7. Odds and Ends

有时候Pascal的“record”以及C中的“struct”是有用的,这两种结构将一些数据项绑定在一起。在Python中空的类定义也可以优雅做到这一点:

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

在某一段需要一个特定的抽象数据类型的代码段,通常可以传入一个模拟该数据类型方法的类来代替它。例如,如果有一个格式化文件对象数据的函数,可以新建一个有能从数据缓冲读取数据的read()readline()方法的对象来代替文件对象,把新的类实例作为参数传递到这个函数即可。(译注:由于Python是动态语言,实现多态的方法不如Java等静态语言严格)

实例方法对象也拥有属性:m.__self__指向持有这个方法m()的实例对象,m.__func__指向对应于方法的函数对象。

9.8. Iterators

到现在你也许注意到了大多数基本对象可以使用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()__方法抛出一个指示for循环终止的StopIteration异常。可以使用内建函数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. Generator Expressions

使用类似于列表推导式的语法,将中括号[]换成括号(),可以简洁的写出一些简单生成器。这些表达式是为恰好使用生成器的封闭函数的情形设计的。比起完成的生成器定义,生成器表达式更加紧凑,更加容易记忆,语法与列表推导式近似,但不多变。

示例:

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

Footnotes

[1] 除了一点,模块对象有一个叫做__dict__的私密属性,这个属性返回用来实现模块命名空间的字典;名字__dict__是属性而不是全局名字。显而易见,这违反了名字空间的抽象原则,应该严格地限制在调试使用中。

推荐阅读更多精彩内容