和其他编程语言相比,Python的类添加了最少的新语法和语义。他是C++和Modula-3中类的混合。Python的类提供了面向对象编程的所有标准特征:类继承机制允许多重基类,派生类能复写任何基类或类中的方法,方法能调用基类中相同名称的方法。对象可以包含任意数量和类型的数据。与模块一样,类和Python的动态性质有关系:他们在运行时创建,并且能在创建后进一步的修改。
在C++术语中,一般的类成员(包括数据成员)是Public,并且所有的函数都是virtual。在Modula-3中,从方法中引用对象的成员是没有缩写的:方法函数第一个参数显示的代表对象本身,有调用函数隐式的提供。在Smalltalk中,类就是对象。为导入和重命名提供了语义。不想C++和Modula-3,内置类型用来作为用户扩展的基类。同样,和C++中一样,大多数内置的有特殊语法的操作符(算数操作符,下标等等)都可以被重新定义为类的实例。
(缺少公用的被认可的术语来谈论类,我将偶尔使用Smalltalk和C++术语。我将使用Modual-3术语,因为它的面向对象语义比C++更接近Python,但我希望很少有读者听过它)
9.1 关于对象和名称的一句话
拥有独立的,多个名字(在多个作用域中)的对象能绑定到同一个对象上。这就是其他语言中的别名。这通常在对Python的第一眼得到重视,并且能在处理不可变的基本类型(数字,字符串,元组)时安全的忽略。然而,别名可能对Python的语义有一个惊人的影响,当涉及到可变对象例如列表,字典,和大多数其他的类型。这通常对程序是有好处的,因为别名在某种程度上就像指针。例如,传递一个对象是很容易的因为仅仅是一个指针传递被执行了;并且如果一个函数修改了作为参数传递的对象,调用者将会看到改变---这消除了在Pascal中需要2个不同参数传递的机制。
9.2 Python的作用域和命名空间
在介绍类之前,我首先必须告诉你们一些关于Python作用域的规则。类定义在名称空间上玩了一个巧妙的技巧,你需要了解作用域和名称空间是如何工作的来完全理解发生了什么。顺便说一下,关于这个主题的知识对任何高级的Python程序员都是有用的。
让我们从一些定义开始。
命名空间是从名称到对象的映射。大多数命名空间目前都作为Python的字典实现,那通常是在任何方面都是不可见的(除了性能),这也可能在未来改变。命名空间的例子是:内置名称集合(包含函数例如:abs(),和内置异常名称);模块中的全局名称;函数调用中的本地名称。在某种意义上来说,一个对象的属性的结合也可以形成一个命名空间。关于命名空间重要的是在不同的命名空间之间名称并没有绝对的关系;例如,2个不同的模块可能都定义了一个函数maximize而不会引起混乱---模块的使用者必须用模块的名称来当成它的前缀。
顺便说一下,我说的属性是指任何跟在点后面的属性----例如,表达式z.real,real就是对象z的属性。严格的说,在模块中引用一个名称是属性引用:表达式modname.funcname,modname是一个模板对象,funcname是它的一个属性。在这种情况下,在模块的属性和模块中定义的全局名称之间将有一个直接的映射:他们共享同一个命名空间。
属性可以是只读或者可写的。在后面的例子中,给属性赋值是可以的。模块属性是可写的:你可以写modname.the_answer = 42.可写的属性也可以用del语句来删除。例如del modname.the_answer将会从名为modname的对象中移除属性the_answer。
命名空间创建在不同的时刻并拥有不同的生命周期。包含内置名称的命名空间在Python解释器启动的时候就创建了,并且不会被删除。一个模块的全局命名空间在模块定义被读取是创建;一般的,模块命名空间也持续到解释器推出。由解释器的高层调用执行的语句,要么从一个脚本文件读取,要么交互式的读取,被认为是名为__main__的模块的一部分,所以它有它自己的全局变量(内置名称同样在一个模块内,它叫做builtins。)
一个函数的本地名称空间在函数调用的时候创建,在函数返回或者抛一个没有被函数处理的异常的时候删除。(事实上,遗忘是描述实际发生的一种好的方式。)当然,递归调用每一个都有他们自己的名称空间。
作用域是一个Python程序的一个文本区域,在这个程序中,名称空间是直接访问的。这里的‘直接访问’意思是一个不限定尝试在名称空间中找到那个名称的引用。
尽管作用域静态的被决定,但使用是动态的。在执行的任何时候,至少有3个嵌套的作用域,它的名称空间是可以直接访问的:
- 最内部的作用域,它是最新被搜索的,包含本地名称
- 任何闭合函数的作用域,从最近的闭合作用域开始搜索,包含非本地,非全局的名称。
- 下一个到最后一个作用域包含当前模板的全局名称
- 最外面的作用域(最后搜索的)是包含内置名称的命名空间
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 初识类
最简单的类的定义形式是这样的:
class ClassName:
<statement-1>
.
.
.
<statement-N>
123
类定义,和函数定义一样(def声明),必须在他们产生作用之前执行。(你可以想象在if语句的分支,或者在一个函数里放一个类的定义)
实际上,在一个类定义的语句通常是函数定义,但是其他的语句也是被允许的,某些时候可能更有用--我们稍后讨论。在一个类里的函数定义通常有一个特有的参数列表形式,由方法的调用约束约束--再一次,这将在之后解释。
当含义定义被输入时,一个新的名称空间就会创建,并且作为本地作用域--因此,对本地变量的所有赋值都会进入到这个新的名称空间。特殊的,函数定义在这儿绑定新函数的名称。
当一个类定义被保留(通过结束),一个类对象将会被创建。这根本上就是一个包围着名称空间内容的包装器,名称空间是由类定义的;我们将在下一节了解更多关于类对象。原始的本地作用域(在类定义输入前的那个)恢复,并且类对象在这里绑定给在类定义头的类名称(例子中的ClassName)。
9.3.2 类对象
类对象支持2种操作:属性引用和实例化。
在Python中属性引用使用用作所有属性引用的标准语法:obj.name。当类对象创建时有效的属性名称是所有在类的名称空间的名称。所以,如果类定义看起来是这样:
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 实例对象
用实例对象我们能做什么呢?通过实例对象理解的唯一的操作就是属性引用。有2种有效的属性名称,数据属性和方法。
数据引用对应于Smalltalk的‘实例变量’,对应于C++中的‘数据成员’。数据属性不需要声明;就像本地变量,当它第一次被赋值时就会存在。例如,如果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不一样-----它是一个方法对象,不是一个函数对象。
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'
共享的数据能有可能惊人的效果在调用可变对象例如列表和字典。例如,下面例子中的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']
9.4 一些说明
数据属性用相同的名字覆写方法属性;为了避免意外的名称冲突,这种冲突可能导致在大型程序中很难检测的BUG,使用某种约束将会是明智的来减少冲突的几率。可能的约束包括使用方法名,使用一个小的独一无二的字符串作为数据属性的前缀(可能就是一个下划线),或者对方法使用动词,对数据属性使用名词。
数据属性可以被方法引用,也可以被一个对象的字典用户(‘clients’)。换句话说,类不能用于实现纯粹抽象的数据类型。事实上,没有任何东西能强制数据隐藏-----这都是基于约束。(另一方面,Python实现,用C写的,能完全隐藏实现细节并且控制对一个对象的访问如果需要的话;这能够通过用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
现在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__。
9.5 继承
当然,如果不支持继承,一个语言的特征将不值得‘类’这个名字。派生类的定义语法如下:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
名称BaseClassName必须定义在一个包含派生类定义的作用域中。在基类名称的地方,其他任何表达式都是允许的。这将很有用,例如,当基类定义在其他模块:
class DerivedClassName(modname.BaseClassName):
123
派生类定义的执行与基类的执行一致。当类对象组成时,将记住基类。这用于解决属性引用:如果一个需要的属性在类中没有找到,将会在基类中去搜索。这条规则递归的应用如果基类本身也是从其他类派生出来的。
冠以派生类的实例化没有什么特殊的:DerivedClassName()创建了一个新的类的实例。方法指引按如下方式来处理:相关的类属性被搜索,如果需要的话,将沿着基类的链继续往下,如果这产生了一个函数对象,那么方法指引将是有效的。
Derived类可能覆写他们基类的方法。当调用同一对象的其他方法时,方法没有特权,一个基类的方法,调用定义在相同基类的其他方法,可以调用一个派生类的方法来覆写它。(对于C++程序员:在Python中所有的方法都是有效的virtual)
在派生类中的一个覆写的方法可以事实上去扩展而不是简单的替换基类中相同名称的方法。有个简单的方式可以直接调用基类方法:调用BaseClassName.methodname(self,arguments)。这对客户端偶尔也有帮助(注意,如果基类是在全局作用域下作为BaseClassName可以访问的情况下才有用)
Python有2个内置函数能用于继承:
- 使用isinstance()检查一个实例的类型:isinstance(obj,int)只有当obj.__class__是int或某个从int派生的类才会是True
- 使用issubclass()检查类继承:issubclass(bool,int)当bool是int的一个子类的时候为True。然而,issubclass(float,int)当float不是int的一个子类时是False。
9.5.1 多重继承
Python也支持多重继承的形式。一个有着多个基类的类定义如下:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
在大多数场合,最简单的情况,你可以将从一个父类中继承的属性认为是深度优先,从左到右的搜索,而不是在相同的具有重叠层次的类中搜索2次.因此,如果一个属性没有在DerivedClassName中找到,它将会在Base1中继续搜索,然后(递归的)在Base1的基类中,如果没有找到,将继续搜索Base2等等。
事实上,它比上述的要稍微复杂一点;方法解析顺序会动带的改变为了支持super()。这个放在在其他多重继承语言中被成为调用下一个方法,并且比单进程语言中的super调用要更强大。
动态的排序是必要的,因为所有多重继承的例子都显示了一个或多个菱形的关系(其中至少一个父类能被最底层的类通过多个路径来访问)。例如所有的类都继承自object,所以任何多重继承的例子都提供超过一个到达object的路径。为了防止基类被多次访问,动态算法以一种方式将搜索顺序线性化,这种方式保持每个类的从左到右的顺序,只调用一次父类,那是单调的(也就是说,类可以在不影响父类优先级顺序的前提下被子类化)。综合起来,这些属性可以使设计可靠可有扩展性的类具有多重继承。
9.6 私有变量
除了从一个对象内部否则不能访问的私有实例变量在Python中是不存在的。然而,在Python代码中有个遵循的约束:用下划线作为前缀的名称(如: _spam)应该被认为是一个非公有的API部分(不管是函数,方法还是一个数据成员)。它应该被看作是一个实现的细节和主题为了在没有通知的情况下改变。
因为对于类的私有成员有一个有效的用例(为了避免由子类定义的名称冲突),对这种机制的支持是有限的,称为名称编码。形式__spam的任何标识符(至少2个前置下划线,最多一个后置下划线)都会被替换成_classname__spam,其中calssname是当前的类名称。这个编码不管标识符的语法位置,只要它发生在类的定义中。
名称编码让子类覆写方法而不破坏同类的方法调用变得更好:
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中'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 生成器
生成器是用来创建迭代器的一个简单和强有力的工具。他们像普通的函数一样编写但是使用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.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
>>> 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']