第零章 学前准备
第一章 数据结构 – 基本数据类型
第一章 数据结构 – 字符串
第一章 数据结构 – 列表、元组和切片
第一章 数据结构 – 字典
第一章 数据结构 – 集合
第一章 – 数组、队列、枚举
第一章 数据结构 – 序列分类
第二章 控制流程
第三章 函数也是对象 – 函数定义以及参数
第三章 函数也是对象 – 高阶函数以及装饰器
第三章 函数也是对象 – lambda 表达式、可调用函数及内置函数
第四章 面向对象编程 – 自定义类、属性、方法和函数
第四章 面向对象编程–魔术方法1
第四章 面向对象编程 – 魔术方法2
第四章 面向对象编程 – 可迭代的对象、迭代器和生成器
第四章 面向对象编程 – 继承、接口
文章目录
第四章 第四章 面向对象编程 – 继承、接口
4.7 继承
一个类继承另一个类时,它将自动获得另一个类的所有属性和方法;原有的类称为父类,而新类称为子类。子类继承了其父类的所有属性和方法,同时还可以定义自己的属性和方法。用户自定义类默认继承了基类 object
。
如果你想重写父类的方法,只需要定义与父类同名的方法而已。
python
里面不像 java
、 c++
,支持不同函数签名的函数。 python
中不允许两个函数同名,如果有同名函数,后面的会覆盖前面的函数。
4.7.1 多重继承
python
支持多重继承,任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为"菱形问题"。
注意, B
和 C
都实现了 pong
方法,二者之间唯一的区别是, C.pong
方法输出的是大写的 PONG
。在 D
的实例上调用 d.pong()
方法的话,运行的是哪个 pong
方法呢?在 C++
中,程序员必须使用类名限定方法调用来避免这种歧义。 Python
也能这么做。
Python
能区分 d.pong()
调用的是哪个方法,是因为 Python
会按照特定的顺序遍历继承图。这个顺序叫方法解析顺序( Method Resolution Order,MRO
)。类都有一个名为 __mro__
的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到 object
类。 D
类的 __mro__
属性如下: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
import doctest
class A:
def ping(self):
print('ping:', self)
class B(A):
@property
def name(self):
return self.__class__.__name__
def pong(self):
print('pong:', self)
class C(A):
def pong(self):
print('PONG:', self)
class D(B, C):
"""
>>> d=D()
直接调用d.pong()运行的是B类中的版本。
>>> d.pong() # doctest:+ELLIPSIS
pong: <__main__.D object at 0x...>
超类中的方法都可以直接调用,此时要把实例作为显式参数传入。
>>> C.pong(d) # doctest:+ELLIPSIS
PONG: <__main__.D object at 0x...>
D类的__mro__属性如下
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
直接调用d.ping()运行的是D类中的版本。
>>> d.ping() # doctest:+ELLIPSIS
ping: <__main__.D object at 0x...>
D
post-ping: <__main__.D object at 0x...>
"""
def ping(self):
super().ping()
print(super().name)
print('post-ping:', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong(self)
if __name__ == "__main__":
doctest.testmod()
4.7.2 使用 super
调用超类方法
super([type[, object-or-type]])
返回一个代理对象,它会将方法调用委托给 type
的父类或兄弟类。 这对于访问已在类中被重载的继承方法很有用。 object-or-type
确定用于搜索的 method resolution order
。 搜索会从 type
之后的类开始。
举例来说,如果 object-or-type
的 __mro__
为 D -> B -> C -> A -> object
并且 type
的值为 B,则 super()
将会搜索 C -> A -> object
。
object-or-type
的 __mro__
属性列出了 getattr()
和 super()
所共同使用的方法解析搜索顺序。 该属性是动态的,可以在任何继承层级结构发生更新的时候被改变。
如果省略第二个参数,则返回的超类对象是未绑定的。 如果第二个参数为一个对象,则 isinstance(obj, type)
必须为真值。 如果第二个参数为一个类型,则 issubclass(type2, type)
必须为真值(这适用于类方法)。
super
有两个典型用例:
- 在具有单继承的类层级结构中,
super
可用来引用父类而不必显式地指定它们的名称,从而令代码更易维护。 这种用法与其他编程语言中super
的用法非常相似。 - 第二个用例是在动态执行环境中支持协作多重继承。 此用例为
Python
所独有而不存在于静态编码语言或仅支持单继承的语言当中。 这使用实现“菱形图”成为可能,即有多个基类实现相同的方法。 好的设计强制要求这样的方法在每个情况下都具有相同的调用签名(因为调用顺序是在运行时确定的,也因为这个顺序要适应类层级结构的更改,还因为这个顺序可能包括在运行时之前未知的兄弟类)。
对于以上两个用例,典型的超类调用看起来是这样的:
class C(B):
def method(self, arg):
super().method(arg) # This does the same thing as:super(C, self).method(arg)
除了方法查找之外, super()
也可用于属性查找。 属性查找的一个场景就是在上级或同级类中调用描述器。
4.8 接口:从协议到抽象基类(Abstract Base Class,ABC)
4.8.1 鸭子类型和白鹅类型
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。对象的类型无关紧要,只要实现了特定的协议即可。忽略对象真正的类型,转而关注对象有没有实现所需的方法、签名和语义。 一个用户定义的类型,不需要真正的继承自抽象基类,但是却可以当作其子类一样来使用。比如用户实现了序列协议,就可以当作内置序列类型来用,对其使用 len()
等函数,调用 __len__()
等用于内置类型的方法。比如用户实现了 __getitem__
方法, Python
就可以直接去迭代这个类型。 Python
内置库和第三方的库,虽然是针对 Python
的类型设计的,但是都可以直接用于用户自定义的类型上。
而白鹅类型与此恰好相反。
使用抽象基类明确声明接口,子类显示地继承抽象基类,抽象基类会检查子类是否符合接口定义。白鹅类型和鸭子类型相比,一些直接继承自抽象基类的接口是可以拿来即用的,但是子类为了经过抽象基类的接口检查,必须实现一些接口,虽然这些接口你可能不需要。
from collections import abc
class Struggle:
def __len__(self):
return 23
# 只要实现了__len__方法,就是Sized的子类
print(isinstance(Struggle(), abc.Sized))
True
4.8.2 定义并使用抽象基类
Tombola
抽象基类有四个方法,其中两个是抽象方法。
load(...)
:把元素放入容器。pick(...)
:从容器中随机拿出一个元素,返回选中的元素。
另外两个是具体方法。
loaded()
:如果容器中至少有一个元素,返回True
。inspect()
:返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)。
4.8.3 Tombola的虚拟子类
白鹅类型的一个基本特性(也是值得用水禽来命名的原因):即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我们保证注册的类忠实地实现了抽象基类定义的接口,而 Python
会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会把我们捕获。
注册虚拟子类的方式是在抽象基类上调用 register
方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且 issubclass
和 isinstance
等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了避免运行时错误,虚拟子类要实现所需的全部方法。
import abc
import random
class Tombola(abc.ABC): # ➊ 自己定义的抽象基类要继承abc.ABC。
@abc.abstractmethod
def load(self, iterable): # ➋ 抽象方法使用@abstractmethod装饰器标记,而且定义体中通常只有文档字符串。与其他方法描述符一起使用时,abstractmethod()应该放在最里层
"""从可迭代对象中添加元素。"""
@abc.abstractmethod
def pick(self): # ➌ 根据文档字符串,如果没有元素可选,应该抛出LookupError。
"""随机删除元素,然后将其返回。
如果实例为空,这个方法应该抛出`LookupError`。
"""
def loaded(self): # ➍ 抽象基类可以包含具体方法
"""如果至少有一个元素,返回`True`,否则返回`False`。"""
return bool(self.inspect()) # ➎ 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具体方法、抽象方法或特性)。
def inspect(self):
"""返回一个有序元组,由当前元素构成。"""
items = []
while True: # ➏ 我们不知道具体子类如何存储元素,不过为了得到inspect的结果,我们可以不断调用.pick()方法,把Tombola清空
try:
items.append(self.pick())
except LookupError:
break
self.load(items) # ➐ 然后再使用load()把所有元素放回去。
return tuple(sorted(items))
class BingoCage(Tombola): # ➊ 明确指定BingoCage类扩展Tombola类
def __init__(self, items):
# ➋ 假设我们将在线上游戏中使用这个。random.SystemRandom使用os.urandom(...)函数实现random API。根据os模块的文档,os.urandom(...)函数生成“适合用于加密”的随机字节序列。
self._randomizer = random.SystemRandom()
self._items = []
self.load(items) # ➌ 委托.load(...)方法实现初始加载。
def load(self, items):
self._items.extend(items)
# ➍ 没有使用random.shuffle()函数,而是使用SystemRandom实例的.shuffle()方法。
self._randomizer.shuffle(self._items)
def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')
def __call__(self): # ➏ bingoCage.pick()的快捷方式是bingoCage()
self.pick()
class LotteryBlower(Tombola):
def __init__(self, iterable):
self._balls = list(iterable) # ➊ 初始化方法接受任何可迭代对象:把参数构建成列表。
def load(self, iterable):
self._balls.extend(iterable)
def pick(self):
try:
position = random.randrange(len(self._balls)) # ➋ 如果范围为空,random.randrange(...)函数抛出ValueError,为了兼容Tombola,我们捕获它,抛出LookupError。
except ValueError:
raise LookupError('pick from empty LotteryBlower')
return self._balls.pop(position) # ➌ 否则,从self._balls中取出随机选中的元素。
def loaded(self): # ➍ 覆盖loaded方法,避免调用inspect方法。我们可以直接处理self._balls而不必构建整个有序元组,从而提升速度。
return bool(self._balls)
def inspect(self): # ➎ 使用一行代码覆盖inspect方法。
return tuple(sorted(self._balls))
@Tombola.register # ➊ 把Tombolist注册为Tombola的虚拟子类。
class TomboList(list): # ➋ Tombolist扩展list。
def pick(self):
if self: # ➌ Tombolist从list中继承__bool__方法,列表不为空时返回True。
position = randrange(len(self))
return self.pop(position) # ➍ pick调用继承自list的self.pop方法,传入一个随机的元素索引。
else:
raise LookupError('pop from empty TomboList')
load = list.extend # ➎ Tombolist.load与list.extend一样。
def loaded(self):
return bool(self) # ➏ loaded方法委托bool函数。
def inspect(self):
return tuple(sorted(self))
# Tombola.register(TomboList) # ➐ 如果是Python 3.3或之前的版本,不能把.register当作类装饰器使用,必须使用标准的调用句法。