查看全部 Python3 基础教程
类机制
类提供了一种将数据和功能捆绑在一起的方式。创建一个新类将创建一种新的对象类型,从而允许创建该类型的新实例。可以给类实例附加属性来维护其状态,还可以用其类定义的方法来修改其状态。
与其他编程语言相比,Python 的类机制增加了具有最少新语法和语义的类,它融合了 C++ 和 Modula-3 中的类机制。Python 类提供所有面向对象编程的标准特性:类继承机制允许有多个基类,派生类可以覆盖基类的任何方法,派生类中的方法可以调用基类中的同名方法。对象可以包含任意数量和种类的数据。与模块一样,类具有 Python 的动态特性:它们在运行时被创建,且可以在创建后被进一步修改。
用 C++ 术语来说,通常类成员(包括数据成员)是公有的(除了以下要说的私有变量),所有的成员函数都是虚拟的。用 Modula-3 术语来说,从对象方法引用对象成员是没有简写方式的:方法函数在定义时要显示地将对象作为第一个参数,调用该方法时会隐示地提供该对象。用 Smalltalk 术语来说,类本身就是对象,这为导入和重命名提供了语义,与 C++ 和 Modula-3 不同,Smalltalk 内置类型可以用作用户扩展的基类,此外,与 C++ 类似,Smalltalk 大多数具有特殊语法的内置运算符(算术运算符、下标等)可以为了类实例的需要被重新定义。
由于缺乏关于类的通用术语,我将偶尔使用 Smalltalk 和 C++ 术语。我更希望用 Modula-3 术语,因为它的面向对象机制比 C++ 更接近 Python,不过我想没多少读者听说过它。
对象别名的好处
对象具有个体性,并且可以将多个命名(在多个作用域内)绑定到同一个对象。这在其他语言中被称为别名。乍看 Python 的这种特性通常并不欣赏,并且在处理不可变的基本类型(数字、字符串、元组)时可以安全地忽略它。然而,别名可能会对可变对象(列表、字典和大多数其他类型)的 Python 代码语义产生令人吃惊的影响。这通常会给程序带来好处,因为别名在某些方面表现得像指针。例如,传递一个对象代价很小,因为具体实现只是传递一个指针,如果函数修改了作为参数传递的对象,则调用者将看到该变化 —— 这就消除了像 Pascal 那样的两种不同参数的传递机制。
作用域和命名空间
在介绍类之前,先要说一下 Python 的作用域规则。类定义巧妙地运用了命名空间,要完全理解接下来的知识,就要先理解作用域和命名空间的工作原理。
命名空间是命名到对象的映射。目前大多数命名空间都是由 Python 字典实现的,但除了性能,这通常并不明显,而且将来可能会改变。命名空间的例子有:内置命名集合(包括像 abs()
这样的函数以及内置异常命名),模块中的全局命名,函数调用中的局部命名。某种意义上对象的属性集合也构成命名空间。关于命名空间有一点很重要:不同命名空间中的命名之间没有任何关系,例如两个不同的模块可能都会定义一个 maximize
函数而不会发生混淆 —— 用户必须以模块名为前缀来引用它们。
可以将跟在 .
后面的命名称为属性,例如在表达式 z.real
中,real
是对象 z
的一个属性。严格来说,引用模块中的命名是在引用其属性,在表达式 modname.funcname
中,modname
是模块对象并且 funcname
是它的一个属性。在这种情况下,模块的属性和模块中定义的全局命名之间恰好有一个简单的映射:它们共享相同的命名空间。
有一个例外,模块对象有一个隐秘的只读对象,叫做 __dict__,它返回用于实现模块命名空间的字典,
__dict__
是一个属性而非全局命名。显然,使用它违反了命名空间实现的抽象原则,应该仅限于调试中。
属性可以是只读或可写的,后一种情况下可以给属性赋值。模块属性是可写的:可以编写 modname.the_answer=42
。可写的属性也可以用 del 语句删除。例如,del modname.the_answer
将从对象 modname
中删除属性 the_answer
。
命名空间是在不同的时刻创建的,并且有不同的生存期。包含内置命名的命名空间是在 Python 解释器启动时创建的,并且永远不会被删除。模块的全局命名空间是在读取模块定义时创建的,通常,模块命名空间会一直存在直到解释器退出。由解释器直接调用执行的语句,不管是从脚本文件读入还是来自交互式输入,都被认为是 __main__ 模块的一部分,因此它们有自己的全局命名空间。(内置命名实际上也存在于一个叫 builtins 的模块中。)
调用函数时会创建该函数的局部命名空间,在函数返回或抛出一个未在函数内部处理的异常时会删除该命名空间。当然,每个递归调用都有自己的局部命名空间。
作用域是 Python 程序的文本区域,该区域的命名空间是可直接访问的。此处的“可直接访问”是指对命名的非限定引用会尝试在该命名空间中找到该命名。
虽然作用域是静态确定的,但它们是动态使用的。在执行过程中的任何时候,都有 3 或 4 个嵌套作用域的命名空间是可直接访问的:
- 首先搜索最内部的作用域,其包含局部命名;
- 其次从最近的封闭作用域开始搜索任何封闭函数的作用域,其包含非局部命名和非全局命名;
- 然后搜索包含当前模块全局命名的作用域;
- 最后搜索最外层的作用域,其包含内置命名。
如果某个命名被声明为 global
,则所有的引用和赋值会直接从包含了该模块全局命名的中间作用域中搜索。可以使用 nonlocal 语句重新绑定最内部作用域之外的变量,如果未声明为 nonlocal
,则这些变量为只读,如果对这样的变量进行写入,则只会在最内部作用域创建一个局部变量,而不会改变同名的外部变量。
通常从字面意义上讲,局部作用域引用当前函数中的命名。在函数之外,局部作用域与全局作用域引用相同的命名空间:模块命名空间。类定义会在局部作用域中放置另一个命名空间。
作用域是由程序文本确定的:在模块中定义的函数的全局作用域是该模块的命名空间,无论从哪里或以什么样的别名调用该函数都是如此。另一方面,命名的实际搜索过程是在运行时动态完成的 —— 然而,Python 语言正在向编译时的静态解析方向发展,所以不要依赖于动态命名解析。实际上,局部变量已经是静态确定的。
Python 有一个特别之处,如果 global 或 nonlocal 语句都无效,那么对命名的赋值总会去最内部的作用域。赋值不会复制数据,它们只是将命名绑定到对象。删除也是如此:语句 del x
只是从局部作用域引用的命名空间中删除 x
的绑定。实际上,所有引入新命名的操作都会使用局部作用域:特别是 import 语句和函数定义会将模块或函数名绑定到局部作用域。
global
语句用于表示存在于全局作用域中的特定变量并且应该在那里重新绑定;nonlocal
语句用于表示存在于某个封闭作用域中特定变量并且应该在那里重新绑定。
示例
下例展示如何引用不同的作用域和命名空间以及 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
注意
local
赋值操作并没有改变scope_test
的spam
绑定,而nonlocal
赋值操作改变了scope_test
的spam
绑定。global
赋值操作改变了模块级绑定,可以看到在global
赋值操作之前没有对spam
的绑定。
初识类
类引入了一点新的语法,三种新的对象类型,以及一些新的语义。
类定义语法
最简单的类定义形式:
class ClassName:
<statement-1>
.
.
.
<statement-N>
类定义像函数定义(def 语句)一样需要先执行才能生效。(可以将某个类定义放入 if 语句的某一分支或者某个函数的内部。)
实际应用中,类定义中的语句通常是函数定义,不过其它语句也可以,有时会很有用。类中的函数定义通常有一种特殊形式的参数列表,具体决定于方法的调用约定。
运行到某个类定义中时,会创建一个新的命名空间,并用作局部作用域。因此,对局部变量的所有赋值都在这个新的命名空间中。特别是函数定义将新函数的命名绑定于此。
类定义完成时(正常退出),就创建了一个类对象。它基本上是对类定义创建的命名空间内容进行一个包装。恢复原始局部作用域(在进入类定义之前的那个有效作用域),类对象在这里会绑定到类定义头部给定的类名上(例子中是 ClassName
)。
类对象
类对象支持两种操作:属性引用和实例化。
属性引用使用和 Python 中所有的属性引用一样的标准语法:obj.name
。类对象创建后,类命名空间中所有的命名都是有效属性名。所以如果类定义像这样:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
那么 MyClass.i
和 MyClass.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)
实例对象
实例对象只能理解属性引用操作,有两种有效的属性命名:数据属性和方法。
数据属性相当于 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.f
和 MyClass.f
不同,它是一个方法对象,不是一个函数对象。
方法对象
通常,方法在绑定之后立即调用:
x.f()
在 MyClass
的例子中,这会返回字符串 'hello world'
。然而,不是非得立接调用某个方法,x.f
是一个方法对象,它可以存储起来以后调用。例如:
xf = x.f
while True:
print(xf())
会不断地打印 hello world
。
以上在
x.f()
时并没有按该函数定义指定一个参数,而在 Python 中如果函数调用缺少参数是要抛出异常的。这就是方法的特别之处,即实例对象会作为第一个参数传给该函数。本例中,调用x.f()
相当于MyClass.f(x)
。通常,用含有 n 个参数的列表调用某个方法就相当于用一个特别参数列表调用相应的函数,该参数列表在第一个参数之前插入了该方法的实例对象。
了解方法的实现有助于理解其工作原理。引用实例的非数据属性时,会搜索该实例的类。如果这个命名是一个有效的函数对象类属性,则会将实例对象(其指针)和函数对象打包进一个抽象对象:这就是方法对象。当使用某个参数列表调用该方法对象时,则会从该实例对象和参数列表构造一个新的参数列表,并用此新的参数列表调用该函数对象。
类和实例变量
一般来说,实例变量是每个实例都唯一数据,类变量是类的所有实例共享的属性和方法:
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']
属性的一些说明
如果实例对象和类中出现相同的属性命名,那么优先从实例对象查找属性。
>>> 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
同名的数据属性会覆盖方法属性,为避免可能的命名冲突 —— 在大型程序中这可能会引起难以发现的 bug,最好以某种命名约定来避免冲突。可能的约定包括将方法名的首字母大写,给数据属性名加上小的唯一字符串前缀(可能只是一个下划线),或者给方法名使用动词而给数据属性名使用名词。
可以通过方法以及对象的普通用户(“clients”)来引用数据属性。换句话说,类不能用来实现纯抽象数据类型。事实上,Python 中没有什么办法可以强制隐藏数据,这都是基于约定的。(另一方面,Python 的实现是用 C 写的,如果有必要,可以用 C 来编写 Python 扩展,完全隐藏实现细节,控制对象的访问。)
Clients 应该小心使用数据属性,否则其标记数据属性的操作可能会破坏本来由方法维护的数据一致性。注意,只要能避免命名冲突,客户端可以在不影响方法有效性的情况下向实例对象添加自己的数据属性,这时使用命名约定可以省去很多麻烦。
从方法内部引用数据属性或其它方法没有什么速记方式,事实上这增加了方法的可读性:当粗略浏览一个方法时,会发现局部变量和实例变量没有机会混淆。
通常,方法的第一个参数称为 self
,这仅仅是一个约定:对 Python 而言,self
没有任何特殊含义,但是要注意,如果不遵守该约定,你的代码对其他 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
,g
和h
都是类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)
方法可以像引用普通函数那样引用全局命名。与方法关联的全局作用域是包含其定义的模块,类从不会用作全局作用域。尽管很少需要在方法中使用全局数据,但全局作用域仍有很多合法用途:其一就是导入到全局作用域内的函数和模块可以被方法,以及在其中定义的函数和类所使用。通常,包含该方法的类本身就是在这个全局作用域内定义的,下一节会了解为何一个方法要引用自己的类。
每个值都是一个对象,因此每个值都有一个类(也叫做它的类型),该类以 object.__class__
形式存储。
继承
当然,如果一种语言不支持继承,“类”就没有什么意义。派生类的定义如下所示:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
基类 BaseClassName
必须与派生类定义在一个作用域内。还允许使用其他任意表达式来代替基类名称,当基类定义在另一个模块中时这就有用了。
class DerivedClassName(modname.BaseClassName):
派生类定义的执行过程和基类是一样的。构造派生类对象时,会记住基类,这用于解析属性引用:如果在派生类中找不到请求的属性,则会继续搜索基类。如果基类本身派生自别的类,则会递归地应用该规则。
派生类的实例化没有什么特别之处:DerivedClassName()
会创建一个新的类实例。方法引用按如下规则解析:搜索相应的类属性,必要时沿基类链向下逐级搜索,如果找到了函数对象则这个方法引用就是有效的。
派生类可能会覆盖其基类的方法。因为方法调用同一对象中的其它方法时没有特权,所以基类的方法调用同一基类中的另一个方法时,可能最终调用的是派生类中的覆盖方法。
对 C++ 程序员来说,Python 中的所有方法实际上都是虚方法。
派生类中的覆盖方法实际上可能想要扩展,而不是简单地替换同名的基类方法。有一种简单的方法可以直接调用基类方法:只需调用 BaseClassName.methodname(self, arguments)
,有时这对 clients 也有用。注意,只有在全局作用域内可以通过基类名访问该基类时,这才有效。
Python 有两个支持继承的内置函数:
- 使用 isinstance() 来检查某个实例的类型:
isinstance(obj, int)
仅当obj.__class__
是 int 或某个派生自int
的类时为True
。 - 使用 issubclass() 来检查类继承:
issubclass(bool, int)
为True
因为 bool 为int
的子类。但是,issubclass(float, int)
为False
因为 float 不是int
的子类。
多重继承
Python 也支持多继承,其类定义形如下例:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
对于大多数情况,最简单的情形是,按照深度优先、向左到右,且不在层次结构中有重叠的同一类中搜索两次的规则搜索继承自父类的属性。因此,如果在 DerivedClassName
中找不到属性,则会在 Base1
中搜索该属性,然后(递归地)在 Base1
的基类中搜索,如果还是找不到,则会在 Base2
中搜索,以此类推。
实际情况会比这稍微复杂一些,方法解析顺序会动态变化,以配合调用 super()。这种方法在其他一些多重继承语言中被称为 call-next-method
,并且比单重继承语言中的 super
调用更强大。
方法解析顺序动态变化是必要的,因为所有多重继承的情况都存在一个或多个菱形关系(最底层的类通过多个路径至少可以访问一个父类)。例如,所有类都继承自 object,因此任何多重继承的情况都会提供不止一个路径可以到达 object
。为防止基类被多次访问,动态算法通过维护每个类中指定的左右顺序来将搜索顺序线性化,这样每个父级只调用一次,并且是单调的(这意味着类可以在不影响其父级优先顺序的情况下被继承)。综上所述,这些特性使得设计具有多重继承的可靠和可扩展的类成为可能。
详情可以参考 The Python 2.3 Method Resolution Order
私有变量
Python 中不存在只能从对象内部访问的“私有”实例变量。然而,大多数 Python 代码都遵循一个约定:一个以下划线为前缀的命名(如 _spam
)应被视为 API 的非公共部分(无论它是函数、方法还是数据成员)。它应被视为一个实施细节,且可能会变动而不另行通知。
因为有一个合理的类私有成员的使用场景,即为了避免命名与子类定义的命名冲突,所以 Python 对这种机制有个叫 name mangling 的简单支持。任何形如 __spam
的标识符(前面至少两个下划线,后面至多一个下划线)在文字层面将被替换为 _classname__spam
,其中 classname
是去掉了前下划线的当前类名。该 mangling 只要出现在类的定义中就可以,不用考虑该标识符的句法位置。
Name mangling 有利于子类重写父类的方法而不会破坏类内部的方法调用。例如:
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
。
注意 mangling 规则的设计主要用来避免意外,还是有访问和修改“私有”变量的可能,这点甚至能用在一些特殊情况中,比如调试。
注意,传给 exec()
或 eval()
的代码不会将调用类的类名视为当前类。这与 global
语句的效果类似,后者同样被与编译过的字节在一起的代码限制。同样的限制也适用于 getattr()
,setattr()
和 delattr()
,以及直接引用 __dict__
的时候。
类的一些说明
有时使用像 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__
是与该方法对应的函数对象。
迭代器
大多数容器对象都可以用 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
生成器
生成器是创建迭代器的简单且强大的工具。它们写起来像是常规函数,但使用 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.index
和 self.data
等使用实例变量的方法更容易编写,也更清晰。
除了会自动创建方法和保存程序状态外,当生成器终止时,还会自动抛出 StopIteration
异常。结合起来看,这些特性使得可以很容易地创建迭代器,只需编写一个常规函数。
生成器表达式
一些简单的生成器可以简洁地编码为表达式,使用的语法类似于列表生成式,但用的圆括号而不是方括号。这些表达式是为一个封闭函数立即使用生成器的情况而设计的。生成器表达式比完整的生成器定义更紧凑但不通用,且往往比等效的列表生成式更不占用内存。
>>> 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']