Python学习笔记28:从协议到抽象基类
在Python学习笔记27:类序列对象中我们讨论过Python中协议这个概念,其和主流编程语言中的接口概念类似,但缺乏强制约束。
事实上这和语言特性是密切相关的。
像Java或者C++这类静态语言,通过接口和抽象类提供的“模版”,可以在编译期让编译器识别和处理所有的多态调用,而Python是一门动态语言,它完全不不受此类束缚,也无需在调用前去保证**“此对象实现了某个接口或者继承某个抽象基类,所以才能按照某种方式调用“。作为动态语言,只需要在确实调用某种方法的时候去检测该对象是否的确有该方法**就可以了,仅此而已。
这无疑带来极大的灵活性,同时也就造成了在语言特性上和静态语言的很多不同之处。
接下来我们就讨论Python中应该如何正确对待协议、接口和抽象基类这些概念,以及如何使用。
关于协议、接口和抽象基类在不同语言中的发展和所处位置,《Fluent Python》中第11章的杂谈有详细论述,而且写得极为精彩,强烈推荐阅读。
接口
通过前边我们对Python的学习,应该知道Python中并不存在类似Java中的那种interface
概念。在Python中,接口更像是某种对于方法实现的约定,而协议、接口、类某某对象在Python中往往指的是一回事。
至于抽象基类和接口的关系,则是有时候接口的实现会借助前者,关于这点我们会在稍后进行讨论。
事实上Java8开始
interface
可以实现方法了,其概念更像是抽象类了,称作接口的默认方法。
协议的灵活性
在Python学习笔记27:类序列对象中我们展示了如何实现一个序列协议,也说明了协议的“宽泛性”,即有时候并不需要实现全部协议,也可以让目标很好地“扮演”协议对象很好的工作。
我们这里再次用序列协议说明协议的灵活性。
部分实现
依据官方文档中对collection.abc.Sequence
的继承结构说明,我绘制了以下的UML类图。
从类图可以看到,Sequence
除了继承和重写父类的方法外,定义了两个抽象方法__getitem__
和__len__
。
所以理论上如果我们要让一个Python中的对象表现的像“序列”,至少需要实现__getitem__
和__len__
,但事实并非如此。
class LikeSequence():
def __init__(self):
self._contents = [i for i in range(10)]
def __getitem__(self, index):
return self._contents[index]
ls = LikeSequence()
for i in ls:
print(i, end=' ')
# 0 1 2 3 4 5 6 7 8 9
可以看到,LikeSequence
并没有实现__iter__
方法,但却可以在for/in
语句中遍历,这是因为Python解释器在把ls
作为序列使用的时候,如果没有实现__iter__
,但是实现了__getitem__
,就会通过__getitem__
“自动”实现一个__iter__
方法。
事实上在Sequence
抽象基类的实现中也体现了这一思想,对此官方文档有明确说明:
实现笔记:一些混入(Maxin)方法比如
__iter__()
on.org/zh-cn/3/reference/datamodel.html#object.reversed) 和index()
会重复调用底层的__getitem__()
n.org/zh-cn/3/reference/datamodel.html#object.getitem)那么相应的混入方法会有一个线性的表现;然而,如果底层方法是线性实现(例如链表),那么混入方法将会是平方级的表现,这也许就需要被重构了。
Python中协议的这种灵活性甚至会超出你的想象,我们会用更进一步的示例说明。
猴子补丁
我们现在尝试把类序列对象中的元素顺序打乱,这里可以使用random
模块中的shuffle
方法:
摘抄自官方文档。
import random
class LikeSequence():
def __init__(self):
self._contents = [i for i in range(10)]
def __getitem__(self, index):
return self._contents[index]
ls = LikeSequence()
random.shuffle(ls)
for i in ls:
print(i, end=' ')
# Traceback (most recent call last):
# File "D:\workspace\python\test\test.py", line 13, in <module>
# random.shuffle(ls)
# File "D:\software\Coding\Python\lib\random.py", line 360, in shuffle
# for i in reversed(range(1, len(x))):
# TypeError: object of type 'LikeSequence' has no len()
错误提示我们LikeSequence
缺少方法len
,看来调用需要此方法,我们给LikeSequence
添加上:
import random
class LikeSequence():
def __init__(self):
self._contents = [i for i in range(10)]
def __getitem__(self, index):
return self._contents[index]
def __len__(self):
return len(self._contents)
ls = LikeSequence()
random.shuffle(ls)
for i in ls:
print(i, end=' ')
# Traceback (most recent call last):
# File "D:\workspace\python\test\test.py", line 16, in <module>
# random.shuffle(ls)
# File "D:\software\Coding\Python\lib\random.py", line 363, in shuffle
# x[i], x[j] = x[j], x[i]
# TypeError: 'LikeSequence' object does not support item assignment
错误信息提示我们目标对象不支持元素赋值操作,这个错误可以预见,因为random.shuffle
是在序列基础上进行打乱顺序的操作,所以必然需要对元素进行赋值操作。
我们再修改一下:
import random
class LikeSequence():
def __init__(self):
self._contents = [i for i in range(10)]
def __getitem__(self, index):
return self._contents[index]
def __len__(self):
return len(self._contents)
def __setitem__(self, index, value):
self._contents[index] = value
ls = LikeSequence()
random.shuffle(ls)
for i in ls:
print(i, end=' ')
# 6 1 7 5 3 9 0 2 8 4
现在没有问题了。
但是这里要说明的是,除了像其它传统编程语言中那样,通过在类定义中增加方法来“适配”协议所需外,作为动态语言,Python还可以通过一种叫做“猴子补丁”的方式实现:
import random
class LikeSequence():
def __init__(self):
self._contents = [i for i in range(10)]
def __getitem__(self, index):
return self._contents[index]
def likeSequenceLen(self):
return len(self._contents)
LikeSequence.__len__ = likeSequenceLen
def likeSequenceSetitem(self, index, value):
self._contents[index] = value
LikeSequence.__setitem__ = likeSequenceSetitem
ls = LikeSequence()
random.shuffle(ls)
for i in ls:
print(i, end=' ')
# 7 5 0 6 1 3 8 9 2 4
可以看到,我们可以在类定义之外,通过动态的方式给类添加新的方法,从而实现对协议的支持。
这种方式和给软件“打补丁”很像,在Python中称作“猴子补丁”。
需要注意的是,示例中的猴子补丁函数的定义和之前类定义中的函数完全相同,其实猴子补丁中的参数签名中的首个参数命名并不一定需要是self
,在类定义之外的函数仅仅是一个普通函数,我们只不过是将其以“打补丁”的方式添加给LikeSequence
类而已。
此外还需要注意给补丁函数名命时候不要太过随意,比如我一开始名命为len
,出现了一些奇怪的bug,后来发现是因为名命覆盖了内建函数。
可以看出,这种“猴子补丁”和Python的语言特性相当搭配,很灵活。在没有改变原有类定义的情况下我们给类添加了新的特性,但是同样需要指出的是,这也会给代码维护添加额外成本,有时候你可能会遇到一些奇怪的bug。
比如说两个模块分别对同一个模块“打补丁”,最后我们要厘清其中的互相影响那可能是场灾难。
所以我们在使用这种特性的时候也不能太过随意。
所以你对Python的了解越多,越会发现这并不是一门对初学者友好的语言。反而是那些限制颇多,即使是初学者也很难写出糟糕代码的强类型静态语言更适合初学者。
抽象基类
我们之前说过,作为动态语言,协议这一概念对Python更为重要,抽象基类反而是对协议的一种补充。
事实也是如此,抽象基类是在Python2的某个版本中才引入的,Python在很长一段时间内是没有此类概念和组件的,而那个时候的Python表现的依然不错。
所以我们要明确的是,在Python中,抽象基类远没有在其它语言(如Java)中那么重要,它只是对协议的完善和补充。
在Python中,抽象基类最重要的用途是进行类型判断,比如isinstance
和issubclass
等。
我们先来看抽象基类的基本语法。
语法
ABC和ABCmeta
定义抽象类我们需要用到abc
模块。
关于该模块的详细介绍见官方文档。
在Python3.4之前,定义抽象基类我们需要这样:
import abc
class Carrier(metaclass=abc.ABCMeta):
pass
在那之后更为简单直观,可以这样:
import abc
class Carrier(abc.ABC):
pass
这里的ABC意思是abstract base class(抽象基类)。
abstractmethod
抽象方法的定义也相当简单,只要使用装饰器就行了:
import abc
class Carrier(abc.ABC):
@abc.abstractmethod
def land(self):
pass
@abc.abstractmethod
def takeoff(self):
pass
如果要定义抽象类方法,也很简单:
import abc
class Carrier(abc.ABC):
@abc.abstractmethod
def land(self):
pass
@abc.abstractmethod
def takeoff(self):
pass
@classmethod
@abc.abstractmethod
def build(cls):
pass
通过装饰器“叠放”我们可以实现我们想要的方法定义,但是需要注意的是,就像之前我们说过的,在叠放函数装饰器的时候要注意顺序,对于abstractmethod
,在实践中往往会放在最里层。
Python中的抽象方法其实是可以实现函数体的,这点和大多数变成语言并不相同。并且子类可以通过
super().xxx()
的方式进行调用。
继承
使用抽象基类最简单也是最容易想到的就是继承,这也是很多语言中的唯一途径。
在用继承实现子类之前我们先把Carrier
抽象基类完善一下:
为了方便格式化输出,额外创建一个Plane
类:
class Plane():
def __init__(self, model, number):
self._model = model
self._number = number
def __str__(self):
return "{} No:{:0>3d}".format(self._model, self._number)
完善Carrier
:
import abc
from collections import namedtuple
from plane import Plane
class Carrier(abc.ABC):
@abc.abstractmethod
def loadPlanes(self, planes):
'''加载飞机'''
@abc.abstractmethod
def land(self, plane: Plane):
'''着陆飞机'''
@abc.abstractmethod
def takeoff(self) -> Plane:
'''起飞一架飞机,如果没有飞机了,返回False'''
@classmethod
@abc.abstractmethod
def build(cls):
'''建造航母'''
def getAllPlanes(self):
'''显示所有的飞机'''
planes = []
while True:
plane = self.takeoff()
if plane != False:
planes.append(plane)
else:
break
for plane in planes:
self.land(plane)
return planes
这里我们给基类添加了一个getAllPlanes
方法,并且利用抽象方法完成目的,但是可以看到实现的方式很“笨拙”,这很像使用序列协议时候没有实现__iter__
时候解释器通过__getitem__
“笨拙”实现迭代一样。
新建一个liao_ning_carrier.py
:
from carrier import Carrier
class LiaoNingCarrier(Carrier):
pass
在测试程序test.py
中导入:
import liao_ning_carrier
执行后发现并未报错,明明我们在LiaoNingCarrier
中并没有实现Carrier
的抽象方法。
这是因为Python并不会在导入类定义的时候进行类型检查,而是在类被实例化的时候才会进行继承的先关类型检查:
from liao_ning_carrier import LiaoNingCarrier
carrier1 = LiaoNingCarrier()
# Traceback (most recent call last):
# File "D:\workspace\python\test\test.py", line 2, in <module>
# carrier1 = LiaoNingCarrier()
# TypeError: Can't instantiate abstract class LiaoNingCarrier with abstract methods build, land, takeoff
我们现在完善LiaoNingCarrier
:
from carrier import Carrier
from plane import Plane
class LiaoNingCarrier(Carrier):
def __init__(self):
self._garage = []
def land(self, plane: Plane):
self._garage.append(plane)
print("{}在辽宁号着陆".format(plane))
def takeoff(self) -> Plane:
try:
plane = self._garage.pop(0)
except IndexError:
return False
print("{}从辽宁号起飞".format(plane))
return plane
@classmethod
def build(cls):
return cls()
def loadPlanes(self, planes):
self._garage.extend(planes)
进行测试:
from liao_ning_carrier import LiaoNingCarrier
from plane import Plane
carrier1 = LiaoNingCarrier.build()
planes = [Plane("歼15",i) for i in range(1,6)]
carrier1.loadPlanes(planes)
carrier1.getAllPlanes()
# 歼15 No:001从辽宁号起飞
# 歼15 No:002从辽宁号起飞
# 歼15 No:003从辽宁号起飞
# 歼15 No:004从辽宁号起飞
# 歼15 No:005从辽宁号起飞
# 歼15 No:001在辽宁号着陆
# 歼15 No:002在辽宁号着陆
# 歼15 No:003在辽宁号着陆
# 歼15 No:004在辽宁号着陆
# 歼15 No:005在辽宁号着陆
可以看到carrier1
的getAllPlanes
是通过基类的低效率方式实现的,如果我们想提高效率,最好在子类重写。
def getAllPlanes(self):
return self._garage
除了继承,Python还可以通过注册实现“虚拟子类”。
注册
我们再创建一个子类QueenElizabethCarrier
:
from carrier import Carrier
from plane import Plane
@Carrier.register
class QueenElizabethCarrier():
pass
这里不是直接继承,而是使用Carrier.register
装饰器进行“注册”的方式声明QueenElizabethCarrier
是Carrier
的子类。
通过这种方式构建的子类并非传统意义上的子类,在Python中被称为“虚拟子类”。
我们测试一下:
from queen_elizabeth_carrier import QueenElizabethCarrier
from carrier import Carrier
carrier2 = QueenElizabethCarrier()
print(isinstance(carrier2, Carrier))
print(issubclass(QueenElizabethCarrier, Carrier))
# True
# True
结果很糟糕,明明QueenElizabethCarrier
只是一个空架子,但没有任何类型错误出现,而且isinstance
和issubclass
函数都认为这就是一个Carrier
的子类。
之前有提到过,我们通过类的__mro__
属性可以查看类的继承关系:
from queen_elizabeth_carrier import QueenElizabethCarrier
from carrier import Carrier
print(QueenElizabethCarrier.__mro__)
# (<class 'queen_elizabeth_carrier.QueenElizabethCarrier'>, <class 'object'>)
可以看到实际上QueenElizabethCarrier
是直接继承自object
的,并非Carrier
,只不过它“表现得”像是其的一个子类。
mro的意思是method revolution order,即方法解析顺序。
事实上通过这种注册的方式定义的虚拟子类,也不会从“虚拟父类”那里继承任何东西,它只是顶着一个子类的“头衔”。
所以如果要在程序中能真正“表现地”像是一个子类,就需要实现父类的所有方法。
from carrier import Carrier
from plane import Plane
@Carrier.register
class QueenElizabethCarrier(list):
def loadPlanes(self, planes):
'''加载飞机'''
self.extend(planes)
def land(self, plane: Plane):
'''着陆飞机'''
self.append(plane)
print("{}从伊丽莎白女王号降落")
def takeoff(self) -> Plane:
'''起飞一架飞机,如果没有飞机了,返回False'''
try:
plane = self.pop()
except IndexError:
return False
print("{}从伊丽莎白女王号起飞")
return plane
@classmethod
def build(cls):
'''建造航母'''
return cls()
def getAllPlanes(self):
'''显示所有的飞机'''
return self
def __str__(self):
string = ""
for plane in self:
string += "{} ".format(plane)
return string
这里我们通过将QueenElizabethCarrier
直接继承list
的方式快速实现了对Plane
存储的支持,而在这种情况下对Carrier
的注册反而更像是Java中的interface
。
进行测试:
from queen_elizabeth_carrier import QueenElizabethCarrier
from carrier import Carrier
from plane import Plane
carrier2 = QueenElizabethCarrier.build()
planes = [Plane("F35B",i) for i in range(1,6)]
carrier2.loadPlanes(planes)
print(carrier2.getAllPlanes())
# F35B No:001 F35B No:002 F35B No:003 F35B No:004 F35B No:005
事实上,就和之前我们介绍装饰器的时候一样,我们完全可以不使用@
符号,“手动”进行注册。
我们可以在QueenElizabethCarrier
的类定义最后这样:
Carrier.register(QueenElizabethCarrier)
这也是完全可行的,Python官方就通过这种方式完成了一些容器的注册。
虽然从理论上这种注册是相当灵活的,但实际上通常是在类定义之后马上进行注册,否则可能会对代码的可维护性带来一些问题。
如果你觉得到这里已经很能说明Python中的继承关系是多么的灵活,但实际上远远不止如此。
实现方法
事实上,没有任何直接继承,也没有任何注册,仅仅是具有抽象基类的所有方法,就可以被认为是该种类型了。
我们构建一个SFCarrier
:
from plane import Plane
class SFCarrier():
def loadPlanes(self, planes):
'''加载飞机'''
pass
def land(self, plane: Plane):
'''着陆飞机'''
pass
def takeoff(self) -> Plane:
'''起飞一架飞机,如果没有飞机了,返回False'''
pass
@classmethod
def build(cls):
'''建造航母'''
return cls()
def getAllPlanes(self):
'''显示所有的飞机'''
return []
测试一下:
from sf_carrier import SFCarrier
from carrier import Carrier
carrier3 = SFCarrier()
print(isinstance(carrier3, Carrier))
print(issubclass(SFCarrier, Carrier))
# False
# False
此时并没有被认可为子类。
但是我们可以通过一个神奇的classhook
实现。
对Carrier
进行修改,添加一个类方法:
@classmethod
def __subclasshook__(cls, C):
if cls is Carrier:
for baseCls in C.__mro__:
allFuncs = baseCls.__dict__.keys()
mustFuncs = {"loadPlanes","land","takeoff","build","getAllPlanes"}
if set(mustFuncs)<=set(allFuncs):
return True
return NotImplemented
这个方法的作用是,如果一个对象包含一些指定方法,则认为这个对象就是Carrier
的子类。
再次执行测试程序就能发现Python已经认可了。
事实上Python中内建的Sized
接口就实现了__subclasshook__
,所以所有实现了__len__
的类都会自动被认为是Sized
的子类。
当然,这里使用__subclasshook__
只是说明Python中继承关系是有多么的灵活,实际中基本是不会有使用它的情况出现的。
使用原则
最后再次强调一下,在Python中,抽象基类并没有其他语言中那么重要,其最主要的用途就是提供类型判断。而非是像其他静态语言中那样提供多态支持,实际上在Python中不需要任何抽象基类你就可以多态调用,只要在执行调用的时候目标对象拥有相应的方法就行,无需任何类型验证。
所以基于上面的原因,在Python中对于抽象基类的态度是尽可能少的使用。除非是某些框架开发或者高级程序员,确切地知道如何创建和使用。在大多数情况下,基本都是直接继承Python内建的抽象基类。
最后介绍一下Python中的内建抽象基类。
标准库中的抽象基类
collections.abc
标准库中的大多数抽象基类都位于collections.abc
。
为了直观理解,我根据官方文档花时间用EA画了一个类图:
没有在官方文档找到相应的类图,只能自己画了,如果有谁知道有官方提供的,麻烦告知一下。
图中的抽象类和抽象方法为斜体。
这里提供一个pdf版本:
链接: https://pan.baidu.com/s/16pgb0TrDbu0U3gAhnfZ4qQ
提取码: 1jnz
numbers
numbers提供一些数字相关的抽象基类。
有以下抽象类:
- Number
- Complex
- Real
- Rational
- Integral
抽象层级相比collection
简单的多,就是从上到下,详细情况可以参考官方文档。
好了,以上。
用EA画UML真是个累人的活。
最后附上Carrier
相关示例的工程文件:
链接: https://pan.baidu.com/s/1g2BTwlCxpidWCY8X2zb48w
提取码: q6js
还有思维导图: