9. 类

未匹配的标注
本文档最新版为 3.8,旧版本可能放弃维护,推荐阅读最新版!

类就是一组数据和函数的集合。创建了一个新的类意味着创建了一个全新的对象 类型,也就允许我们创建新的此类型的 实例。每个类实例都带有属性以维护其状态。同样也有方法(在它自己的类中定义)来修改这些状态。

相比其他编程语言,使用 Python 的类机制创建类是最方便简洁的。Python 的类机制结合了 C++ 和 Modua-3 的机制,同时也符合所有面向对象编程的标准:类继承机制允许有多个基类,一个派生的类可以覆盖基类的任意方法,每个方法也可以调用基类的同名方法。每个对象都可以包含任意数量和类型的数据。与模块一样,类也具有 Python 动态的特性:即时创建,创建后仍可修改。

在 C++ 的概念中,常规的类成员都是 公共(public) 的(例外的情况请看 Private Variables 这一节)成员函数都是 虚(virtual) 的。 到了 Modula-3 中,我们没有从其自身的方法中引用对象成员的快速写法:方法函数声明时要在第一参数中标明对象,这样就会在调用时隐式的调用。在 Smalltalk 中,类本身就是对象。同时还有导入和重命名语法。但不同于 C++ 和 Modula-3,它的内置类型可以直接作为基类由用户进行扩展。同样,它也有类似 C++ 的地方,大部分有特殊语法的内置操作符(算术运算符,下标等等)都可以被类实例重定义。

(由于目前并没有普遍接受的术语来讨论类,我会在接下来的文章中偶然提到几个 Smalltak 和 C++ 中的术语。同样我也会用一些 Modula-3 的术语,因为它的面向对象概念与 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 将会删除掉 modnamethe_answer 属性。

命名空间会在不同时刻被创建,也会拥有不同的命名空间。命名空间中包含着在 Python 解释器启动之初创建的内置名称,并且永远不会被删除。模块中的全局命名空间也会在模块被读入时创建,一般情况下也会持续到解释器退出。声明的执行由上层解释器调用,不管是从文件中读入还是交互式的,模块中包含最多的是一个叫 __main__ 的东西,每个模块都有自己的全局命名空间(实际上内置名称也在模块中存在,它们被称为 builtins.) )。

函数的本地命名空间在函数被调用时创建,函数返回或抛出异常时但没在函数内处理时删除。(实际上,忘记处理可能是描述实际所发生的了什么的更好的方式..)当然,递归调用每一次都有自己的本地命名空间。

scope (作用域) 是一段 Python 程序的文本区域,处于其中的命名空间是可直接访问的。「可直接访问」在这里的意思是非限定性引用的某名称会尝试在此命名空间中查找。

尽管作用域一般都是静态的,不过也常常被动态的用。在任何执行的时候,每段代码都至少有3个嵌套的作用域可直接访问。

  • 最内层的作用域,会被首先搜索,里面放的是本地名称。
  • 任何处于函数内的作用域,会从在最接近它的作用域中开始寻找,这层的命名空间中放的是非本地但也非全局的名称。
  • 倒数第二层作用域是包含着当前模块的全局名称。
  • 最外层作用域(最后搜索的一层)是包含内置名称的命名空间。

如果某名称是在全局进行的声明,那么所有的引用和分配都会直接导向中间的这层包含模块的全局名称的作用域中。要想让最内层的作用域重新绑定一个在外层出现过的变量,我们可以用 nonlocal 声明来完成;如果不声明 nonlocal (非本地),这些变量则都是只读的(任何尝试写入这种变量的行为都将会创建一个 全新 的本地变量,不会对最外层的那个有丝毫影响。)

通常情况下,本地作用域引用着当前函数的本地名称。外层的函数引用的是和全局作用域一样的命名空间:模块的命名空间。类定义放置在本地作用域的另一个命名空间中。

意识到作用域取决于文本是很重要的:某个模块中所定义的函数的全局作用域是它所在的模块的命名空间,不管这函数来自什么地方或以什么别名被调用。换句话说,实际的名称搜索是在动态的情况下完成的,也就是运行时 --- 但,语言定义的发展是朝着静态命名去的,在 「编译」阶段完成,所以不要试图依赖任何动态的命名!(实际上,本地变量已经是静态定义的了。)

Python 中也有皮一下的地方 -- 如果不用 global 声明,那么所分配的变量总是在它所处位置的最内层。分配不会复制数据 -- 它们只是把名字绑定到对象上。对删除来讲也是一样: del x 声明会把 x 从本地作用域所引用的命名空间中移除绑定。实际上,所有引入新名称的操作都会使用本地作用域:尤其是 import 声明和绑定在模块中的函数定义或者在本地作用域的函数名称。

global 声明被用在要指定某个特殊的变量要在全局作用域中存活且应该在这重新被绑定的情况下;nonlocal 声明则是用在指示某变量存在于某封闭的作用域且应该在这被重新绑定的情况下。

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

用个小例子来演示下如何引用不同的作用域和命名空间,以及 global 和 nonlocal 是如何影响变量绑定的:

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 则更改的是模块层面的绑定。

不知道你有没有注意到,我们在 global 之前是没有绑定 spam 的。

9.3. 初窥类对象

类的话需要介绍一点小小的新语法,三种新的对象类型和一些新的语义。

9.3.1. 类定义语法

类定义的形式很简单像这样既可:

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

类的定义与函数定义(def statements)差不多,在它们生效前我们需要预先执行这些定义(你也可以在  if 分支或函数内部声明类)。

在实践中,类定义内的声明通常是函数定义,不过也有其他的声明,而且还挺有用 -- 我们之后再谈这个。在类中定义的函数通常有一个特有的参数列表,指代是作为方法调用的 --- 同样我们稍后再解释。

进入到类定义后,会创建一个新的命名空间作为本地作用域 --- 也因此,所有的本地变量的指定都会进到这个新的作用域里。尤其是定义函数所绑定的是此函数的名字。

类定义正常结束时,一个新的 类对象 就被创建出来了。这是类定义在命名空间中最基本的一层包装;我们在下一节中详细讨论这个。原始的本地作用域(在进入类定义前生效的那个)会被重新安装,然后将类名字(就是上例中的 ClassName)绑定到这个类对象上。

9.3.2. 类对象

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

Attribute references (属性引用) 使用的是 Python 中标准的属性引用语法: obj.name。有效的属性名都会在此类创建时被塞入的命名空间中。所以,如果一个类定义看起来像这样:

class MyClass:
    """简单的例子"""
    i = 12345

    def f(self):
        return 'hello world'

MyClass.iMyClass.f 都是有效的属性引用,分别返回的是一个整数和一个函数对象。类属性同样是可分配的,所以你可以更改 MyClass.i 的值。__doc__ 同样也是一个有效属性,返回的是此类的文档字符串: "简单的例子"

类的 实例化 类似函数的形式。把它假装成一个无参数且返回的是此类实例的函数就行。看代码(用的上面那个类):

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. 实例对象

那么..我们要用这个实例对象干什么呢?最基本的操作时属性引用。我们现在有两种有效的属性名:数据属性和方法。

data attributes(数据属性) 等同于 Smalltalk 中的「实例变量」,以及 C++ 中的 「数据成员」。数据属性不需要提前声明;就像本地变量一样,它们会在第一次分配时传播到已有的命名空间中。举个例子,假设我们已经创建了 MyClass 的实例 x,下面的代码会打印出 16且不留下痕迹:

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

另一种实例属性引用是 method (方法)。一个方法也就是一个 「属于」某个对象的函数。(在 Python 中,方法一词并不被类实例独占:其他对象属性也同样具有方法。比如,列表对象也有如 append, insert, remove, sort 的方法。不过,接下来的讨论中我们所说的方法只指代类实例对象中的方法,除非特别指明。)

实例对象的有效方法名依赖于它的类。基于定义,所有是函数对象的类属性定义都会等同于它所实例化后的方法。所以在我们的例子中,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'         # 类变量会在所有实例里共享

    def __init__(self, name):
        self.name = name    # 每个实例都有独立的实例变量

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # 所有 dog 都会共享
'canine'
>>> e.kind                  # 同上
'canine'
>>> d.name                  # d 所独有的
'Fido'
>>> e.name                  # e 所独有的
'Buddy'

A Word About Names and Objects 的讨论中,共享的数据可能在调用 mutable 可变对象(比如列表和字典)时有意料之外的效果。举个例子, 下面写的 tricks 列表就不应该作为一个类变量存在,因为同一个列表会在所有的 Dog 实例中共享:

class Dog:

    tricks = []             # 不要作为类变量误用。

    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                # 我们不希望它在所有 dog 中共享。
['roll over', 'play dead']

正确的设计应该是用实例变量代替:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # 每只 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. 补充说明

数据属性会覆盖同名的方法属性;为了避免命名冲突(冲突的话在大型程序中往往会引起很难查找的 bug),用一些大家都遵守的约定来最小化冲突的机会是非常明智的。一般有大写方法名字,使用独特的短字符串来给数据属性加上前缀(也可以是仅仅一个下划线),或者使用动词命名方法,而使用名词命名数据属性。

数据属性不光可以被此对象的用户(「客户」)一方使用,我们在方法内同样可以使用。换句话说,类不能用于实现纯粹的抽象数据类型。实际上,在 Python 中强制数据隐藏起来也是不可能的 --- 它们只是在约定。(换..换句话说, Python 中实现于 C 的部分可以做到完全的隐藏实现细节,也可以控制一个对象的访问;这一点可以用在用 C 写 Python 的扩展上)。

客户一方(就是创建了实例后再用)也应该小心地使用数据属性 --- 因为有可能弄乱方法们维护的数据属性一致性。不过客户一方也可以添加自己的数据属性进去,只要避免影响到方法的有效性就行,也就是说避免命名冲突 --- 再说一遍!避免命名冲突很!重!要!

Python 中并无在方法内快捷访问数据熟悉的途径(方法的也没有!)。因为这样可以提高可读性:这样就可以快速弄清楚本地变量和实例变量。

通常我们把方法的第一个参数命名为 self. 这只是一个约定: self 这个名字对 Python 来讲并无特殊含义。不过要注意,如果不遵守的话~,其他的 Python 程序猿可能会不知道你写的啥呦~,而且还可依据此来写一个 class browser 的程序

任何作为类属性定义的函数对象都会作为实例或类的方法。把相关的函数定义在类的文本域内并不是必须的:指定一个函数对象到类的本地变量中同样是 Ok 的:

# 定义在类外了...
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 必须被定义在一个包含派生类 DerivedClassName 定义的作用域下。相较于直接使用基类名,任何其它表达式也是可以被填入的。这个特性经常被用到,比如,当基类被定义在其它模块中时:

class DerivedClassName(modname.BaseClassName):

派生类定义时的执行流程和基类相同。当一个类对象被创建,它会记录它的基类。这将被用于解析对象的属性:如果一个需要的属性不存在于当前类中,紧接着就会去基类中寻找。如果该基类也是从其他类派生出来的,那么相同的过程也会递归地被应用到这些类中。

实例化派生类也没有什么特别的: DerivedClassName() 就会创建类的一个新的实例。方法引用则按如下的方式被解析:首先在当前类中搜索对应的属性,然后沿着继承链往下搜索,如果找到了一个函数对象,那么这个方法引用就是可用的。

派生类可以重写基类的方法。因为方法在调用同一对象其它方法的时候没有什么特权,所以当派生类的实例调用某个基类的方法后,该基类的方法可能会再次调用派生类覆写的另一个基类方法。(对于 C++ 程序员而言, Python 中所有的方法都是 virtual 函数。)

派生类中重写的方法一般用于扩展同名的基类方法,而非简单的替换。 Python 中有一种简单的直接调用基类方法的方案:调用 BaseClassName.methodname(self, arguments) 即可。这在某些情景下也是有用的。(注意这个方法只有在基类 BaseClassName 在全局作用域下可以访问才能使用。)

Python 提供了两个判断继承关系的内建函数:

  • 使用 isinstance() 检查一个实例的类型:当且仅当 obj.__class__int 或其它从 int 派生的类时, isinstance(obj, int) 才会返回 True
  • 使用 issubclass() 检查类之间的继承关系:因为 boolint 的一个子类,所以 issubclass(bool, int) 返回 True 。然而,因为 float 不是 int 的派生类,所以 issubclass(float, int) 返回 False

9.5.1. 多重继承

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

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

对于多数目的,在最简单的情况下,你可以认为搜索父类中继承的属性是深度优先,从左到右,而不是在继承结构中重叠的同一个类中搜索两次。因此,如果一个属性在 DerivedClassName 中没有找到,则在 Base1 中查找,再在 Base1 的基类中(递归地)查找,如果未能找到,则在 Base2 中查找,以此类推。

事实上,这个过程要稍稍更复杂一些;方法解析顺序是动态变化的,以支持合作调用 super() 。这种方法在其他多继承语言中被称为调用下一方法,比单继承语言中的 super 调用更加强大。

动态排序是必要的,因为所有多重继承的情况都表现出一个或多个菱形关系(其中至少有一个父类可以通过最底层的多个路径访问)。例如,所有的类都继承自 object ,所以任何情况的多重继承都提供了不止一条的路径到达 object 。 为了避免基类被多次访问,动态算法保证在每个类中进行从左到右特定顺序的线性搜索,因此每个父类只被调用一次,并且这个方法是单调的(意味着类可以被子类化而不影响其的优先顺序)。 总之,这些特性使得设计具有多重继承的可靠的且可扩展的类成为可能。 更多细节请参考 www.python.org/download/releases/2....

9.6. 私有变量

只能从对象内部访问的『私有』实例变量,在 Python 中不存在。然而,在大多数 Python 代码中存在一个这样的约定:以一个下划线开头的命名(例如 _spam )会被处理为 API 的非公开部分(无论它是一个函数、方法或数据成员)。它会被视为一个实现细节,无需公开。

因为有一个正当的类私有成员用途(即避免子类里定义的命名与之冲突),Python 提供了对这种结构的有限支持,称为 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)

需要注意的是编码规则设计为尽可能的避免冲突,被认作为私有的变量仍然有可能被访问或修改。在特定的场合它也是有用的,比如调试的时候。

要注意的是代码传入 exec(), eval() 时不考虑所调用的类的类名,视其为当前类,这类似于 global 语句的效应,已经按字节编译的部分也有同样的限制。这也同样作用于 getattr(), setattr() 和 delattr(),像直接引用 __dict__ 一样。

9.7. 零碎知识点

有时候,有一个类似于 Pascal 「记录」或者 C 「结构体」的数据类型是非常有用的,它能够将一些命名数据项捆绑在一起:

class Employee:
    pass

john = Employee()  # 创建一个新的 employee 记录

# 给记录的各个字段赋值
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='')

这种形式的访问很清晰,简洁,方便。其背后是迭代器在起作用,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:
    """从后向前的迭代器"""
    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 代替 return 返回数据。 每次 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))                 # 平方之和
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # 乘积
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']

脚注

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~