9.5 继承(Inheritance)
当然,一个有“类”的语言中如果不支持继承的特性,是没有什么价值的。派生类(Derived Class)的定义语法如下:
class DerivedClassName(BaseClassName): <statement-1> . . . <statement-N>
BaseClassName这个名字必须定义在包含派生类定义的作用域中。在放基类名的地方,任何其他的表达式也是可以使用的。例如,当基类定义在其他模块的时候这会很有用:
class DerivedClassName(modname.BaseClassName):
派生类的执行和基类一样。当类对象构造完成后,基类对象也会被记住。在处理属性引用时会用到:如果需要的属性在当前类中没找到,会继续在基类中搜索。如果基类本身又是另一个类的派生类的话,这项规则会递归的执行。
派生类的实例化没有什么特别之处:DerivedClassName()
创建类的一个新实例。方法引用会有如下的处理:相应的类属性会被查找,如果有必要的话会在基类链上继续查找,只有过程中找到一个函数对象这个方法引用就是合法的。
派生类可以重写基类的方法。由于方法在调用同一个对象的其他方法时没有特殊权限,父类的一个方法调用这个父类定义的另一个方法时,会调用到派生类中重写的方法。(对于C++程序员来说:Python中所有的方法都是virtual的。)
在派生类中重写一个方法实际上是想拓展基类的同名方法,而不只是替换它。有一个直接调用基类方法的简单方式:直接调用BaseClassName.methodname(self, arguments)
。有时候这对客户也是有用的。(注意,这段代码只有当BaseClassName
在全局作用域中可访问时才是可用的。)
Python有两个和继承相关的内建函数:
- 使用
isinstance()
检查一个实例的类型:isinstance(obj, int)
只有当obj.__class__
是int或其他派生自int的类时才会是True
。 - 使用
issubclass()
检查类的继承关系:issubclass(bool, int)
是True
,因为bool是int的一个子类。然而,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()的调用而动态变化(附上原文:method resolution order changes dynamically to support cooperative calls to super())。这种方法在其他的多继承语言中以call-next-method被熟知,并且比单继承语言中的超类调用更有用。
动态排序是很有必要的,因为所有的多继承关系都是呈一个或多个菱形结构(在从最底层的类到父类的多条路径中至少有一条是可以访问到的)。例如,所有的类都继承自object,所以任何一个多继承都提供了不止一条到达object的路径。为了保证基类可以被多次访问到,动态算法将查找顺序线性化,使每个类中都是保持从左到右的顺序(翻译的比较混乱,原文是:the dynamic algorithm linearizes the search order in a way that preserves the left-to-right ordering specified in each class),这样每个父类只被调用一次,就不再改变了(这意味着,一个类被继承时不会影响它父类的搜索顺序。)。综上考虑,这个属性使得使用多继承设计一个可靠的、可拓展的类成为可能。更多的细节,请参考https://www.python.org/download/releases/2.3/mro/。
9.6 私有变量(Private Variables)
在Python中,不存在无法访问的“私有”实例变量。然而,在大多数的Python代码中都会遵循这样的约定:以下划线'_'为前缀的名称(例如_spam)应当作为非公有的API来对待(无论它是函数、方法还是一个数据成员)。应该把它作为一个实现细节对待,并且如果改变了也不会有任何通知。
因为有一个中合法的使用类私有成员的情况(即为了避免和子类中的名称冲突),所以为这个机制提供了有限的支持,称为名称重整(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 杂记(Odds and Ends)
有时候,有和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 cls in [B, C, D]: try: raise cls() except D: print("D") except C: print("C") except B: print("B")
注意,如果这些异常子句的顺序倒过来(except B
放在第一个位置),它会打印B,B,B——第一个except子句总会匹配上。
当一个未处理的异常的错误消息被打印出来时,异常类的类名会被打印,后面跟着一个冒号和一个空格,最后是使用内建函数str()转换为string的异常实例。