9. 类¶
同别的编程语言相比, Python 的类机制中增加了少量新的语法和语义. 它是 C++ 的类机制和 Modula-3 的类机制的混合体. Python 类提供了面向对象编程的所有基本特征: 允许多继承的类继承机制, 派生类可以重写它父类的任何方法, 一个方法可以调用父类中重名的方法. 对象可以包含任意数量和类型的数据成员. 作为模块, 类也拥有 Python 的动态特征: 他们可以被动态创建, 并且可以在创建之后被修改.
从C++术语上讲, Python 类的成员 (包括数据成员) 通常都是 public 的 (例外见下 私有变量), 并且所有的成员函数都是 virtual 的. 和 Modula-3 中一样, Python 中没有关联对象成员和方法的隐式表达: 所有方法函数在声明时显式地将第一个参数表示为对象, 这个参数的值在方法被调用时隐式赋值. 同 Smalltalk 类似, Python 类本身就是对象. 这就提供了导入和重命名的语义. 与 C++ 和 Modula-3 不同的是, Python 的内置类型可以被当做基类来让使用者扩展. 另外, 像 C++ 一样, 大多数有特殊语法的内置操作符(算数运算符, 下标操作符等等) 在类的实例中都可以重定义.
(由于在谈论类的时候缺乏公认的术语, 我会偶尔使用 Smalltalk 和 C++ 的术语. 我更愿意用 Modula-3的术语, 因为它面向对象的语义比C++更贴近Python, 但是我估计没有读者听过这个说法.)
9.1. 关于名称和对象的讨论¶
对象都具有个别性, 多个名称(在多个作用域中) 可以被绑定到同一个对象上. 这就是其他语言中所谓的别名. 通常第一次接触 Python 可能不会意识到这一点, 而且在处理不变的基本类型(数值, 字符串, 元组)时这一点可能会被安全的忽略. 但是, 在涉及到可变对象如 lists, dictionaries, 以及大多数其他类型时, 别名可能会在 Python 代码的语义上起到惊人的效果. 别名通常对编程有益处, 因为别名在某些方面表现得像指针. 比如, 由于在实现的时候传递的是指针, 所以传递一个对象的开销很小; 又比如将对象作为参数传递给一个函数来对它进行修改, 调用者将会看到对象的变化 — 这就消除了像Pascal 语言中的两个不同参数之间的传递机制的必要.
9.2. Python 的作用域和命名空间¶
在介绍类之前, 我必须先告诉你一些关于 Python 作用域规则的事. 类定义用命名空间玩了一些巧妙的把戏, 而你为了完全理解发生了什么就必须知道命名空间和作用域是怎么工作的. 顺便说一下, 这一主题的知识对任何高级 Python 程序员都是有用的.
让我们从定义开始.
命名空间 是从名称到对象的映射. 大多数命名空间现在的实现就如同 Python 的字典, 但通常这一点并不明显(除了在性能上), 而且它有可能在将来发生改变.
顺便说一下, 我用了 属性 这个词来称呼任何点后面跟的名称 — 比如,
在表达式 z.real
中, real
就是对象 z
的属性. 更直接的说,
对模块中名称的引用就是属性引用: 在表达式 modname.funcname
中, modname
是模块对象而 funcname
是它的一个属性.
在这种情况下模块的属性和它里面所定义的全局名称之间就刚好有一个直接的映射关系:
他们共享同一个命名空间! [1]
属性可以是只读的或可写的. 在后一种情况下, 给属性赋值才是可能的. 模块属性是可写的:
你可以写 modname.the_answer = 42
. 可以利用:keyword:del 语句来删除可写属性.
例如, del modname.the_answer
将从名为 modname
的模块中移除属性 the_answer
.
命名空间们是在不同时刻创建的,并且有着不同的生命期. 包含内置名称的命名空间是在 Python
解释器启动时创建的, 而且它永远不被删除. 一个模块的全局命名空间在模块的定义被读取的时候创建;
通常情况下, 模块的命名空间一直持续到解释器退出时. 被最高级别的解释器调用的语句,
不论是从脚本还是从交互读取的, 都被认为是一个名叫 __main__
的模块的一部分,
所以它们有自己的全局命名空间. (内置名称实际上也存在于一个模块中; 这个模块叫 builtins
.)
函数的局部命名空间在函数调用时被创建, 在函数返回时或者发生异常而终止时被删除. (事实上, 忘记可能是更好的方式来描述真正发生了什么.) 当然, 递归调用会有它们自己的局部命名空间.
在 Python 中, 一个作用域只是一个结构上的区域, 在这里命名空间可以直接访问. “直接访问” 就意味着无须特殊的指明引用.
尽管作用域是静态的决定的, 它们使用时却是动态的. 在执行时的任何时刻, 至少有三个嵌套的作用域其命名空间可以直接访问:
- 最内层的作用域, 首先被搜索, 包含局部变量名
- 任意函数的作用域, 它从最接近的作用域开始搜索, 包括非局部的, 但也是非全局的名字
- 紧邻最后的作用域包含了当前模块的全局变量
- 最外层的作用域 (最后搜索) 是包含内置名字的命名空间
如果一个名字在全局声明, 那么所有的引用和赋值都直接到这个模块的全局名中.
为了在最内部作用域中重新绑定变量, 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 中的 spam,
而 global
则改变了模块级别的绑定.
你可以看到在 global
赋值之前并没有绑定 spam 的值.
9.3. 类的初印象¶
类引入了一些新的语法, 三种新的对象类型, 和一些新的语义.
9.3.1. 类定义的语法¶
最简单的类的定义形式看起来像这样:
class ClassName:
<statement-1>
.
.
.
<statement-N>
类的定义, 和函数定义 (def
语句) 一样必须在使用它们前执行.
(你可以将一个类定义放置于 if
语句的分支中, 或一个函数中.)
事实上, 类定义内部的语句一般是函数的定义, 但其他的语句也是允许的, 而且还很有用 — 我们在后面将会继续讨论该问题. 类内的函数定义一般有一个特殊形式的参数列表, 习惯上称之为方法 — 同样, 也将在后面解释.
当进入一个类定义, 新的命名空间就被创建了, 这一般作为局部的作用域 — 因此, 所有的局部变量都在这个新的作用域中. 特别是, 函数定义会绑定.
当离开一个类定义后, 一个 class object 就被创建.
通过类的定义, 就将这个命名空间包装了起来; 我们将在后面学到更多关于类对象的知识.
原来的局部作用域 (在进入一个类定义前的作用域) 将会复位,
而类对象就会在这里绑定, 并且命名为类定义时的名字 (在此例中是 ClassName
).
9.3.2. 类对象¶
类对象支持两种操作: 属性引用和实例化.
属性引用 使用的语法和 Python 中所有的属性引用一样. 合法的属性名是那些在类的命名空间中定义的名字. 所以一个类定义如果是这样:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
那么, MyClass.i
和 MyClass.f
就是合法的属性引用,
分别返回一个整数和一个函数对象. 类属性也可以被指定, 所以你可以给 MyClass.i
赋值以改变其数值. __doc__
也是一个合法的属性,
返回属于这个类的 docstring : "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. 实例对象¶
那么我们现在可以对实例对象做什么? 实例对象唯一能理解的操作就是属性引用. 有两种合法的属性, 数据属性和方法.
data attribute 在 Smalltalk 中相应于 “instance variable”,
在 C++ 中相应于 “data member”. 数据属性不需要声明; 像局部变量,
当它们第一次指定时就会被引入. 比如, 如果 x
是前面创建的 MyClass
的实例, 那么下面的例子就会打印出 16
, 而不会有问题:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
实例属性引用的另一种是方法. 一个方法就是 “属于” 一个对象的函数. (在 Python 中, 方法的概念并不是类实例所特有: 其他对象类型也可以有方法. 例如, 列表对象有 append, insert, remove, sort, 及等等的方法. 但是, 在下面的讨论中, 我们指的就是类实例对象的方法, 除非特别指出.)
合法的方法名依赖于实例的类. 在定义中, 类的属性如果是那些定义的函数对象,
而这也就是实例的方法. 所以在我们的例子中, x.f
是一个合法的方法引用,
因为 MyClass.f
是一个函数, 但是 x.i
就不是, 因为 MyClass.i
就不是.
但是 x.f
和 MyClass.f
并不一样 — 它是一个 method object,
而不是 function object.
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 个参数), 和调用相应的函数 (也有那 n 个参数,
但是再额外加入一个使用该方法的对象), 是等价的.
如果你仍然不知道方法如何工作, 那么看看实现或许会解决这些问题. 当一个实例属性被引用时, 但是不是数据属性, 那么它的类将被搜索. 如果该名字代表一个合法的类属性并且是一个函数对象, 一个方法对象就会被创建, 通过包装 (指向) 实例对象, 而函数对象仍然只是在抽象的对象中: 这就是方法对象. 当方法对象用一个参数列表调用, 新的参数列表会从实例对象中重新构建, 然后函数对象则调用新的参数列表.
9.4. 随机备注¶
数据属性覆写了同名的方法属性; 为了避免这个偶然的名字冲突, 在大型的程序中这会导致很难寻找的 bug, 使用某些命名约定是非常明智的, 这样可以最小的避免冲突. 可能的约定包括大写方法名称, 在数据类型前增加特殊的前缀 (或者就是一个下划线), 或对于方法使用动词, 而数据成员则使用名词.
数据属性可以被该类的方法或者普通的用户 (“客户”) 引用. 换句话说, 类是不能实现完全的抽象数据类型. 事实上, 在 Python 中没有任何东西是强制隐藏的 — 这完全是基于约定. (在另一方面, Python 是用 C 实现的, 这样就可以实现细节的隐藏和控制访问; 这可以通过编写 Python 的扩展实现.)
客户需要小心地使用数据属性 — 客户会弄乱被方法控制的不变量, 通过使用它们自己的方法属性. 注意用户可以增加它们自己的数据到实例对象上, 而没有检查有没有影响方法的有效性, 只要避免名字冲突 – 在说一次, 命名约定可以避免很多这样令人头疼的问题.
在引用数据属性 (或其他方法 !) 并没有快速的方法. 我发现这的确增加了方法的可读性: 这样就不会被局部变量和实例中的变量所困惑, 特别是在随便看看一个方法时.
通常, 方法的第一个参数称为 self
. 这更多的只是约定:
self
对于 Python 来说没有任何意义. 但注意, 如果不遵循这个约定,
对于其他的程序员来说就比较难以理解了, 一个 class browser 程序可能会依赖此约定.
作为类属性的任何函数对象, 定义了一个方法用于那个类的实例. 函数是否在一个类体中其实并不重要: 指定一个函数对象给类中的局部变量也是可以的. 例如:
# 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)
方法可以引用全局变量, 就像普通函数中那样. 与这个方法相关的全局作用域, 是包含那个类定义的模块. (类本身永远不会作为全局作用域使用.) 如果的确需要在方法中使用全局数据, 那么需要合法的使用: 首先一件事, 被导入全局作用域的函数和模块可以被方法使用, 就如定义在里面的函数和类一样. 通常来说, 定义在全局作用域中, 包含方法的类是它自己本身, 并且在后面我们会知道为何方法应该引用自己的类.
每个值都是一个对象, 所以对于 class (或称为它的 type) 也是这样.
它存于 object.__class__
.
9.5. 继承¶
当然, 一个有 “class” 的语言如果没有继承就没有多大的价值了. 派生类的定义如下:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
BaseClassName
的定义对于派生类而言必须是可见的.
在基类的地方, 任意的表达式都是允许的. 这就会非常有用,
比如基类定义在另一个模块:
class DerivedClassName(modname.BaseClassName):
派生类就可以像基类一样使用. 当一个类被构建, 那么它就会记下基类. 这是用于解决属性引用的问题: 当一个属性在这个类中没有被找到, 那么就会去基类中寻找. 然后搜索就会递归, 因为如果基类本身也是从其他的派生.
实例化一个派生类没有什么特别: DerivedClassName()
会创建这个类的新实例.
方法的引用如下: 相应的类的属性会被搜寻, 如果需要回去搜寻基类,
如果返回一个函数对象, 那么这个引用就是合法的.
派生类会覆写基类的方法. 因为当调用同样的对象的其他方法时方法并没有什么特别的,
基类的方法会因为先调用派生类的方法而被覆写.
(对于 C++ 程序员: 所有的方法在 Python 中都是 vitual
的.)
一个在派生类中覆写的方法可能需要基类的方法.
最简单的方式就是直接调用基类的方法: 调用 BaseClassName.methodname(self, arguments)
.
这对于可续来说也是很方便的. (这仅在 BaseClassName
可访问时才有效.)
Python 有两个内置函数用于继承:
- 使用
isinstance()
检查实例的类型:isinstance(obj, int)
只有在obj.__class__
是int
或其派生类时才为True
. - 使用
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-method 被熟知,
而且比单继承的语言中要有用.
动态的顺序是很有必要的, 因为在那些处于菱形继承体系中 (这里至少有个父类被多次派生).
比如, 所有的类都从 object
派生, 所以到达 object
的路径不止一条.
为了防止基类被多次访问, 动态的算法线性化了搜寻的路径, 先从左至右搜索指定的类,
然后这样就可以让每个父类只搜寻一次, 并且单一 (这就意味一个类可以被派生,
但是不会影响其父类的搜寻路径. 使用了这些, 就使得以多重继承设计的类更可靠和可扩展.
具体参考http://www.python.org/download/releases/2.3/mro/.
9.6. 私有变量¶
在 Python 之中, 并不存在那种无法访问的 “私有” 变量.
但是, 在多数的 Python 代码中有个约定: 以一个下划线带头的名字 (如 _spam
)
应该作为非公共的 API (不管是函数, 方法或者数据成员).
这应该作为具体的实现, 而且变化它也无须提醒.
因为有一个合法的情况用于使用私有的成员 (名义上是说在派生类中避免名字的冲突),
因此就有这样的一种机制称为 name mangling. 任何如 __spam
形式的标识符,
(在开头至少有两个下划线) 将被替换为 _classname__spam
, 此处的 classname
就是当前的类. 这样的处理无须关注标识符的句法上的位置,
尽管它是在一个类的定义中.
注意, 这样的规则只是用于防止冲突; 它仍然可以访问或修改, 尽管认为这是一个私有变量. 在某些特殊情况下, 如测试等, 是有用的.
注意, 传递给 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. 异常也是类¶
用户定义的异常其实也是类. 使用这个机制, 就可以创建可扩展的异常继承体系.
有两种合法的形式用于 raise
语句:
raise Class
raise Instance
在第一种形式下, Class
必须是 type
的实例或者其派生.
第一种形式可以简化为这样这样:
raise Class()
一个在 except
中的类, 可以与一个异常相容, 如果该异常是同样的类,
或是它的基类 (但是并不是另一种 – 一个 except 语句列出的派生类与其基类并不相容).
如下面的代码, 以那种顺序打印出 B, C, D:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for c in [B, C, D]:
try:
raise c()
except D:
print("D")
except C:
print("C")
except B:
print("B")
但是注意, 如果 except 语句是反着的 (先用 except B
),
那么打印的结果将是 B, B, B – 第一个总是匹配.
当因为一个未处理的异常发生时, 错误信息将被打印, 异常的类名将被打印,
然后是一个冒号和空格, 最后是使用 str()
转换后的实例.
9.9. 迭代器¶
到目前为止, 你可能注意到, 大多数的容器对象都可以使用 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)
这种形式简洁, 明了并且方便. 迭代器的使用遍布于 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.10. 发生器¶
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.index
和 self.data
.
除了自动创建方法和保存程序状态, 当生成器终止时, 它们会自动产生 StopIteration
异常. 在这些结合起来后, 这就使得能够很简单的创建迭代器, 除了仅需要编写一个函数.
9.11. 生成器表达式¶
有些简单的生成器可以简洁的写出来, 而且和列表推导很类似, 仅仅是将方括号换成了圆括号. 这些表达式设计用于在一个函数中正好可以用生成器的情况. 生成器表达式更加紧密, 但是功能相对来说也少点, 并且与同样的列表推导式来说更节约内存.
例子:
>>> 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] | 除了一种情况. Module 对象有一个私有的只读属性, 名为 __dict__ , 它返回实现这个模块命名空间的字典.
显然, 使用这会违反命名空间实现的抽象, 而只应当限于在如 post-mortem debuggers 的事情中使用. |
See also
(^.^)
- 原文: http://docs.python.org/py3k/tutorial/classes.html
- 初译: 刘鑫
- 精译: DocsPy3zh
- 校对: Zoom.Quiet
- 复审: