今后本系列笔记的示例代码都将存放在Github项目:https://github.com/icexmoon/python-learning-notes
在中我们讨论过Python中协议这个概念,其和主流编程语言中的接口概念类似,但缺乏强制约束。
事实上这和语言特性是密切相关的。
像Java或者C++这类静态语言,通过接口和抽象类提供的“模版”,可以在编译期让编译器识别和处理所有的多态调用,而Python是一门动态语言,它完全不不受此类束缚,也无需在调用前去保证“此对象实现了某个接口或者继承某个抽象基类,所以才能按照某种方式调用“。作为动态语言,只需要在确实调用某种方法的时候去检测该对象是否的确有该方法就可以了,仅此而已。
接下来我们就讨论Python中应该如何正确对待协议、接口和抽象基类这些概念,以及如何使用。
关于协议、接口和抽象基类在不同语言中的发展和所处位置,《Fluent Python》中第11章的杂谈有详细论述,而且写得极为精彩,强烈推荐阅读。
接口
通过前边我们对Python的学习,应该知道Python中并不存在类似Java中的那种interface
概念。在Python中,接口更像是某种对于方法实现的约定,而协议、接口、类某某对象在Python中往往指的是一回事。
至于抽象基类和接口的关系,则是有时候接口的实现会借助前者,关于这点我们会在稍后进行讨论。
事实上Java8开始
interface
可以实现方法了,其概念更像是抽象类了,称作接口的默认方法。
协议的灵活性
在中我们展示了如何实现一个序列协议,也说明了协议的“宽泛性”,即有时候并不需要实现全部协议,也可以让目标很好地“扮演”协议对象很好的工作。
我们这里再次用序列协议说明协议的灵活性。
部分实现
依据中对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)方法比如 on.org/zh-cn/3/reference/datamodel.html#object.reversed) 和
index()
会重复调用底层的 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):
abstractmethod
. def land(self):
pass
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
还有思维导图:
文章评论