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的异常实例。