类与继承
1 多重继承
1.1 方法解析顺序MRO
提出问题:如果同级别的超类定义了同名属性,Python如何决定使用哪一个?
答案是:方法解析顺序(Method Resolution Order)
任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突叫做砖石(菱形)继承问题,如下图:
第一个图是菱形问题的UML类图,第二个图橙色标记是示例的方法解析顺序。示例1:
class A:
def ping(self):
print('ping:',self)
class B(A):
def pong(self):
print('pong:',self)
class C(A):
"""B和C都实现了pong()方法,唯一不同在pong与PONG"""
def pong(self):
print('PONG:',self)
class D(B,C):
def ping(self):
super().ping()
print('post-ping:',self)
直接调用d.pong()
运行的是B类的pong
方法;而且超类中的方法可以直接调用,只需要把实例作为显式参数传入。演示1:
>>> d = D()
>>> d.ping()
ping: <__main__.D object at 0x000002A6544ABB00>
post-ping: <__main__.D object at 0x000002A6544ABB00>
>>> d.pong()
pong: <__main__.D object at 0x000002A6544ABB00>
>>> C.pong(d)
PONG: <__main__.D object at 0x000002A6544ABB00>
>>>
Python能区分d.pong()
调用的是哪个方法,是因为Python会按特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order)。
类有一个名为__mro__
的属性(类实例没有),它是一个元组,按照方法解析顺序列出各个超类,从当前类一直到object类。如下D.__mro__
:
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
运行结果所示,D类的方法解析顺序为D、B、C、A,所以调用d.pong()
时,运行的是B类的pong
方法。
查看类__mro__
属性的例子:
>>> dict.__mro__
(<class 'dict'>, <class 'object'>)
>>> bool.__mro__
(<class 'bool'>, <class 'int'>, <class 'object'>)
1.2 super()
函数
从演示1看到,可以绕过方法解析顺序(C.pong(d)
),直接调用超类中的方法(直接在类上调用实例方法,必须显示传入self
参数,因为这样访问的是未绑定方法);那么,D.ping
方法可以改写成:
def ping(self):
A.ping(self) #替换super().ping()
print("post-ping:",self)
注意:使用super()
最安全。super()
调用方法时,会遵守方法解析顺序(MRO)。
1.3 如何处理多重继承?
2 抽象基类
抽象基类是用于封装框架引入的一般性概念和抽象的。基本上不需要自己编写新的抽象基类,只要正确使用现有的抽象基类,就能获得99.9%的好处,而不用冒着设计不当导致的巨大风险。
抽象类表示接口,协议是非正式的接口;抽象基类的常见用途:定义接口时作为超类使用。
继承抽象基类非常简单,只需要实现所需的方法(这样也能明确开发者的意图)。
协议与接口概念
- 协议是Python中非正式的接口,是令Python这种动态类型语言实现多态的方式。
- 接口:对象公开方法的子集,让对象在系统中扮演特定的角色。接口是实现特定角色的方法集合,这样理解也就是所说的协议。
- 类的接口:类实现或继承的公开属性(方法或数据属性),包括特殊方法,如
__getitem__
。
2.1 抽象基类的定义
声明抽象基类最简单的方式是继承abc.ABC
或其它抽象基类。而抽象方法使用@abstractmethod
装饰器标记,定义体中通常只有文档字符串;抽象方法也可以有实现,不过就算实现了,子类也必须覆盖抽象方法。但是在子类可以使用super()函数调用抽象方法,为它添加新功能,而不是重头开始实现。
当然,抽象基类中可以包括具体方法。
示例2:实现一个MyList抽象基类,该类提供两个抽象方法和一个具体方法。
import abc
class MyList(abc.ABC):
@abc.abstractmethod
def load(self, iterable):
"""从iterable中添加对象"""
@abc.abstractmethod
def pick(self):
"""随机删除元素,然后将其返回
如果实例为空,应抛出'LookupError'
"""
def getdata(self):
"""返回当前所有元素的元组"""
data = []
while True:
try:
data.append(self.pick())
except LookupError:
break
self.load(data)
return tuple(data)
抽象类无法实例化。如下演示2:Fake类继承了抽象基类MyList,但没有实现所需的抽象方法(只实现了load
抽象方法,未实现pack
抽象方法),所以Python认为Fake类仍然是抽象类,导致实例化异常。
>>> class Fake(MyList):
... def load(self, iterable):
... self.data = list(iterable)
...
>>> f = Fake()
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
f = Fake()
TypeError: Can't instantiate abstract class Fake with abstract methods pick
2.2 @abstractmethod
装饰器
abc
模块定义了abstractmethod
装饰器,用于声明抽象方法。
如果需要定义抽象类方法(抽象静态方法),可使用装饰器叠加,装饰器叠加使用时,@abstractmethod
应放在最下层。
class MyABC(abc.ABC):
@classmethod
@abstractmethod
def abstrct_cls_method(cls, ...):
pass
2.3 register方法声明(注册)虚拟子类
注册虚拟子类的方式是在抽象基类上调用register方法。注册的类会变成抽象基类的虚拟子类,而且issubclass
和isinstance
等函数也能识别,但是注册的类不会从抽象基类中继承任何方法和属性。
可以将虚拟子类理解为干儿子:大家都承认你是抽象基类子类,但并没有遗传其任何东西。
示例3:两种方式(方法调用与装饰器)声明虚拟子类。
from abc import ABC
class MyABC(ABC):
pass
MyABC.register(tuple)#将tuple注册为MyABC的虚拟子类
from abc import ABC
class MyABC(ABC):
pass
@MyABC.register#将MyTuple注册为MyABC的虚拟子类(python3.3+)
class MyTuple:
pass
示例4:使用register注册为MyList
的虚拟子类。
虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查是否符合抽象基类的接口。示例4展示了不符合抽象基类接口的虚拟子类(为了避免未知错误,最好实现所需的全部方法):
@MyList.register
class Virtual:
def get(self, key):
return 'Virtual SubClasses Of ABC MyList'
注册虚拟子类后,issubclass
和isinstance
都判断Virtual类是MyList的子类。
>>> v = Virtual()
>>> issubclass(Virtual,MyList)
True
>>> isinstance(v,MyList)
True
>>> v.get(2)#v.get(any)
'Virtual SubClasses Of ABC MyList'
__mro__
类属性中只包含“真实”子类,所以Virtual.__mro__
中并不会存在注册的抽象基类。
>>> Virtual.__mro__
(<class '__main__.Virtual'>, <class 'object'>)
2.4 类属性__subclasses__()
与_abc_registry
-
__subclasses__()
这个方法返回类的直接子类列表(内存中存在的直接子代),不包含虚拟子类。
-
_abc_registry
只有抽象基类有这个属性,其值是一个
WeakSet
对象,即抽象类注册的虚拟子类的弱引用。>>> MyList._abc_registry <_weakrefset.WeakSet object at 0x000002412BC54390> >>> list(MyList._abc_registry) [<class '__main__.Virtual'>]
3 子类化内置类型会出乎你的意料
在Python2.2之后,内置类型都可以子类化,但是有一个注意事项:内置类型(使用C语言编写)不会调用用户定义的类覆盖的特殊方法。意思是内置类型的方法可能不会调用子类覆盖的方法,导致结果出现与预期不一致。
示例5:子类化内置类型dict
,dict.update
方法会忽略DiyDict.__getitem__
方法。
class DiyDict(dict):
def __getitem__(self, key):
return 'hello,world!'
>>> dd = DiyDict(one=1)
>>> dd
{'one': 1}
>>> dd['one']
'hello,world!'
>>> d = dict()
>>> d.update(dd)#忽略子类覆盖的__setitem__方法
>>> d
{'one': 1}
>>>
示例6:内置类型dict
的__init__
和__update__
方法会忽略子类覆盖的__setitem__
方法。
class DIYDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, "hello world")
>>> dd = DIYDict(one=1)
>>> dd#dict.__init__方法忽略子类覆盖的__setitem__方法
{'one': 1}
>>> dd['two'] = 2#__setitem__方法正常运行
>>> dd
{'one': 1, 'two': 'hello world'}
>>> #dict.__init__方法忽略子类覆盖的__setitem__方法
>>> dd.update(three=3)
>>> dd
{'one': 1, 'two': 'hello world', 'three': 3}
>>>
注意:直接子类化内置类型(如str
、list
、dict
)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。如果需要子类化,用户定义的类应该继承colletcions
模块中的类(如UserDict
、UserList
、UserString
)。
示例7:子类化collections.UserDict
,不会出现忽略子类覆盖方法的问题。
import collections
class MyDict(collections.UserDict):
def __setitem__(self, key, value):
super().__setitem__(key, "hello world")
>>> dd = MyDict(one=1)
>>> dd
{'one': 'hello world'}
>>> dd['two'] = 2
>>> dd
{'one': 'hello world', 'two': 'hello world'}
>>> dd.update(three=3)
>>> dd
{'one': 'hello world', 'two': 'hello world', 'three': 'hello world'}
>>>