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类图。

Python学习笔记28:从协议到抽象基类

从类图可以看到,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方法:

Python学习笔记28:从协议到抽象基类

摘抄自官方文档

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中,抽象基类最重要的用途是进行类型判断,比如isinstanceissubclass等。

我们先来看抽象基类的基本语法。

语法

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在辽宁号着陆

可以看到carrier1getAllPlanes是通过基类的低效率方式实现的,如果我们想提高效率,最好在子类重写。

    def getAllPlanes(self):
        return self._garage

除了继承,Python还可以通过注册实现“虚拟子类”。

注册

我们再创建一个子类QueenElizabethCarrier

from carrier import Carrier
from plane import Plane
@Carrier.register
class QueenElizabethCarrier():
    pass

这里不是直接继承,而是使用Carrier.register装饰器进行“注册”的方式声明QueenElizabethCarrierCarrier的子类。

通过这种方式构建的子类并非传统意义上的子类,在Python中被称为“虚拟子类”。

我们测试一下:

from queen_elizabeth_carrier import QueenElizabethCarrier
from carrier import Carrier
carrier2 = QueenElizabethCarrier()
print(isinstance(carrier2, Carrier))
print(issubclass(QueenElizabethCarrier, Carrier))
# True
# True

结果很糟糕,明明QueenElizabethCarrier只是一个空架子,但没有任何类型错误出现,而且isinstanceissubclass函数都认为这就是一个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画了一个类图:

没有在官方文档找到相应的类图,只能自己画了,如果有谁知道有官方提供的,麻烦告知一下。

Python学习笔记28:从协议到抽象基类

图中的抽象类和抽象方法为斜体。

这里提供一个pdf版本:

链接: https://pan.baidu.com/s/16pgb0TrDbu0U3gAhnfZ4qQ

提取码: 1jnz

numbers

numbers提供一些数字相关的抽象基类。

有以下抽象类:

  1. Number
  2. Complex
  3. Real
  4. Rational
  5. Integral

抽象层级相比collection简单的多,就是从上到下,详细情况可以参考官方文档

好了,以上。

用EA画UML真是个累人的活。

最后附上Carrier相关示例的工程文件:

链接: https://pan.baidu.com/s/1g2BTwlCxpidWCY8X2zb48w

提取码: q6js

还有思维导图:

Python学习笔记28:从协议到抽象基类

上一篇:Carrier Sense Multiple Access


下一篇:android – 以编程方式更改网络选择模式