Python学习笔记28:从协议到抽象基类
2021/5/1 20:25:43
本文主要是介绍Python学习笔记28:从协议到抽象基类,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
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
还有思维导图:
这篇关于Python学习笔记28:从协议到抽象基类的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-21Python编程基础教程
- 2024-11-20Python编程基础与实践
- 2024-11-20Python编程基础与高级应用
- 2024-11-19Python 基础编程教程
- 2024-11-19Python基础入门教程
- 2024-11-17在FastAPI项目中添加一个生产级别的数据库——本地环境搭建指南
- 2024-11-16`PyMuPDF4LLM`:提取PDF数据的神器
- 2024-11-16四种数据科学Web界面框架快速对比:Rio、Reflex、Streamlit和Plotly Dash
- 2024-11-14获取参数学习:Python编程入门教程
- 2024-11-14Python编程基础入门