python 接口_从协议到抽象基类详解


Posted in Python onAugust 24, 2017

抽象基类的常见用途:实现接口时作为超类使用。然后,说明抽象基类如何检查具体子类是否符合接口定义,以及如何使用注册机制声明一个类实现了某个接口,而不进行子类化操作。最后,说明如何让抽象基类自动“识别”任何符合接口的类——不进行子类化或注册。

Python文化中的接口和协议

接口在动态类型语言中是怎么运作的呢?首先,基本的事实是,Python语言没有 interface 关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性(方法或数据属性),包括特殊方法,如__getitem__ 或 __add__。

按照定义,受保护的属性和私有属性不在接口中:即便“受保护的”属性也只是采用命名约定实现的(单个前导下划线);私有属性可以轻松地访问,原因也是如此。不要违背这些约定。

另一方面,不要觉得把公开数据属性放入对象的接口中不妥,因为如果需要,总能实现读值方法和设值方法,把数据属性变成特性,使用obj.attr 句法的客户代码不会受到影响。 Vector2d 类就是这么做的,Vector2d 类的第一版,x 和 y 是公开属性。

vector2d_v0.py:x 和 y 是公开数据属性

class Vector2d:

 def __init__(self, x, y):
 self.x = x
 self.y = y

 def __iter__(self):
 return (n for n in (self.x, self.y))

我们把 x 和 y 变成了只读特性。这是一项重大重构,但是 Vector2d 的接口基本没变:用户仍能读取my_vector.x 和 my_vector.y。

class Vector2d:

 def __init__(self, x, y):
 self.__x = x
 self.__y = y

 @property
 def x(self):
 return self.__x

 @property
 def y(self):
 return self.__y

 def __iter__(self):
 return (i for i in (self.x, self.y))

Python喜欢序列

Python 数据模型的哲学是尽量支持基本协议。对序列来说,即便是最简单的实现,Python 也会力求做到最好。

下图展示了定义为抽象基类的 Sequence 正式接口。

python 接口_从协议到抽象基类详解

Sequence 抽象基类和 collections.abc 中相关抽象类的UML 类图,箭头由子类指向超类,以斜体显示的是抽象方法

现在,看看下面?中的 Foo 类。它没有继承 abc.Sequence,而且只实现了序列协议的一个方法: __getitem__ (没有实现 __len__ 方法)。

>>> class Foo:
... def __getitem__(self, pos):
... 

return range(0, 30, 10)[pos]
...
>>> f = Foo()
>>> f[1]

>>> for i in f: print(i)
...



>>> 20 in f
True
>>> 15 in f
False

虽然没有 __iter__ 方法,但是 Foo 实例是可迭代的对象,因为发现有__getitem__ 方法时,Python 会调用它,传入从 0 开始的整数索引,尝试迭代对象(这是一种后备机制)。尽管没有实现 __contains__ 方法,但是 Python 足够智能,能迭代 Foo 实例,因此也能使用 in 运算符:Python 会做全面检查,看看有没有指定的元素。

综上,鉴于序列协议的重要性,如果没有 __iter__ 和 __contains__方法,Python 会调用 __getitem__ 方法,设法让迭代和 in 运算符可用。

下面的FrenchDeck 类也没有继承 abc.Sequence,但是实现了序列协议的两个方法:__getitem__ 和 __len__。

class FrenchDeck:
 ranks = [str(n) for n in range(2, 11)] + list('JQKA')
 suits = 'spades diamonds clubs hearts'.split()

 def __init__(self):
 self._cards = [Cards(rank, suit) for suit in self.suits
      for rank in self.ranks]

 def __len__(self):
 return len(self._cards)

 def __getitem__(self, pos):
 return self._cards[pos]

使用猴子补丁在运行时实现协议

FrenchDeck 类有个重大缺陷:无法洗牌。如果 FrenchDeck 实例的行为像序列,那么它就不需要 shuffle 方法,因为已经有 random.shuffle 函数可用,文档中说它的作用是“就地打乱序x”(https://docs.python.org/3/library/random.html#random.shuffle)。

标准库中的 random.shuffle 函数用法如下:

>>> from random import shuffle
>>> my_list = list(range(1, 11))
>>> shuffle(my_list)
>>> my_list
[5, 7, 9, 2, 10, 1, 8, 6, 4, 3]

然而,如果尝试打乱 FrenchDeck 实例,会出现异常,如下面的 ? 所示。

Traceback (most recent call last):
 File "/Users/demon/PycharmProjects/FluentPython/接口:从协议到抽象基类/FrenchDeck.py", line 37, in <module>
 shuffle(deck)
 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/random.py", line 274, in shuffle
 x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment

错误消息相当明确,“'FrenchDeck' object does not support itemassignment”('FrenchDeck' 对象不支持为元素赋值)。这个问题的原因是,shuffle 函数要调换集合中元素的位置,而 FrenchDeck 只实现了不可变的序列协议。可变的序列还必须提供 __setitem__ 方法。

解决办法为FrenchDeck 打猴子补丁,把它变成可变的,让random.shuffle 函数能处理

from collections import namedtuple
from random import shuffle

Cards = namedtuple('Cards', ['rank', 'suit'])

class FrenchDeck:
 ranks = [str(n) for n in range(2, 11)] + list('JQKA')
 suits = 'spades diamonds clubs hearts'.split()

 def __init__(self):
 self._cards = [Cards(rank, suit) for suit in self.suits
      for rank in self.ranks]

 def __len__(self):   #获取长度的len
 return len(self._cards)

 def __getitem__(self, position): #能够支持切片取值的处理
 return self._cards[position]


deck = FrenchDeck()    #实例化

def set_card(deck, position, card):  #猴子不定,其实就是启动调用内部__setitem__设置值得外部函数
 deck._cards[position] = card

FrenchDeck.__setitem__ = set_card  #把猴子补丁传递给内部的魔术方法
shuffle(deck)    #打乱洗牌的序列
print(deck[:5])    #获取前五个卡牌的值

以上代码的执行结果为:

[Cards(rank='K', suit='diamonds'), Cards(rank='4', suit='spades'), Cards(rank='A', suit='clubs'), Cards(rank='K', suit='spades'), Cards(rank='6', suit='clubs')]

这里的关键是,set_card 函数要知道 deck 对象有一个名为 _cards 的属性,而且 _cards 的值必须是可变序列。然后,我们把 set_card 函数赋值给特殊方法 __setitem__,从而把它依附到 FrenchDeck 类上。这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。猴子补丁很强大,但是打补丁的代码与要打补丁的程序耦合十分紧密,而且往往要处理隐藏和没有文档的部分。

定义抽象基类的子类

在下面的? 中,我们明确把 FrenchDeck2 声明为collections.MutableSequence 的子类。

frenchdeck2.py:FrenchDeck2,collections.MutableSequence的子类

import collections


Card = collections.namedtuple('Card', ['rank', 'suit'])


class FrenchDeck2(collections.MutableSequence):
 ranks = [str(i) for i in range(1, 11)] + list('JQKA')
 suits = 'spades diamonds clubs hearts'.split()

 def __init__(self):
 self._cards = [Card(rank, suit) for suit in self.suits
     for rank in self.ranks]

 def __len__(self):    #支持查看长度
 return len(self._cards)

 def __getitem__(self, position):  #支持切片
 return self._cards[position]

 def __setitem__(self, position, value):  #支持洗牌
 self._cards[position] = value

 def __delitem__(self, position):  #但是继承MutableSequence的类必须实现 __delitem__ 方法,这是MutableSequence 类的一个抽象方法。
 del self._cards[position]

 def insert(self, position, value):  #此外,还要实现insert方法,这是MutableSequence类的第三个抽象方法
 self._cards.insert(position, value)

Sequence 和 MutableSequence 抽象基类的方法不全是抽象的。

python 接口_从协议到抽象基类详解

MutableSequence 抽象基类和 collections.abc 中它的超类的 UML 类图(箭头由子类指向祖先;以斜体显示的名称是抽象类和抽象方法)

FrenchDeck2 从 Sequence 继承了几个拿来即用的具体方法__contains__、__iter__、__reversed__、index 和count。FrenchDeck2 从MutableSequence 继承了append、extend、pop、remove 和__iadd__。

标准库中的抽象基类

从 Python 2.6 开始,标准库提供了抽象基类。大多数抽象基类在collections.abc 模块中定义,不过其他地方也有。例如,numbers和 io 包中有一些抽象基类。但是,collections.abc 中的抽象基类最常用。我们来看看这个模块中有哪些抽象基类。

collections.abc模块中的抽象基类

python 接口_从协议到抽象基类详解

collections.abc 模块中各个抽象基类的 UML 类图

Iterable、Container 和 Sized

各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable 通过 __iter__ 方法支持迭代,Container 通过__contains__ 方法支持 in 运算符,Sized 通过 __len__ 方法支持len() 函数。

Sequence、Mapping 和 Set

这三个是主要的不可变集合类型,而且各自都有可变的子类

MappingView

在 Python 3 中,映射方法 .items()、.keys() 和 .values() 返回的对象分别是 ItemsView、KeysView 和 ValuesView 的实例。前两个类还从 Set 类继承了丰富的接口。

Callable 和 Hashable

这两个抽象基类与集合没有太大的关系,只不过因为collections.abc 是标准库中定义抽象基类的第一个模块,而它们又太重要了,因此才把它们放到 collections.abc 模块中。我从未见过Callable 或 Hashable 的子类。这两个抽象基类的主要作用是为内置函数 isinstance 提供支持,以一种安全的方式判断对象能不能调用或散列。

Iterator

注意它是 Iterable 的子类。继 collections.abc 之后,标准库中最有用的抽象基类包是numbers

抽象基类的数字塔

numbers 包(https://docs.python.org/3/library/numbers.html)定义的是“数字塔”(即各个抽象基类的层次结构是线性的),其中 Number 是位于最顶端的超类,随后是 Complex 子类,依次往下,最底端是 Integral类:

Number

Complex

Real

Rational

Integral

因此,如果想检查一个数是不是整数,可以使用 isinstance(x,numbers.Integral),这样代码就能接受 int、bool(int 的子类),或者外部库使用 numbers 抽象基类注册的其他类型。为了满足检查的需要,你或者你的 API 的用户始终可以把兼容的类型注册为numbers.Integral 的虚拟子类。

与之类似,如果一个值可能是浮点数类型,可以使用 isinstance(x,numbers.Real) 检查。这样代码就能接受bool、int、float、fractions.Fraction,或者外部库(如NumPy,它做了相应的注册)提供的非复数类型。

定义并使用一个抽象基类

为了证明有必要定义抽象基类,我们要在框架中找到使用它的场景。想象一下这个场景:你要在网站或移动应用中显示随机广告,但是在整个广告清单轮转一遍之前,不重复显示广告。假设我们在构建一个广告管理框架,名为 ADAM。它的职责之一是,支持用户提供随机挑选的无重复类。 为了让 ADAM 的用户明确理解“随机挑选的无重复”组件是什么意思,我们将定义一个抽象基类。

受到“栈”和“队列”(以物体的排放方式说明抽象接口)启发,我将使用现实世界中的物品命名这个抽象基类:宾果机和彩票机是随机从有限的集合中挑选物品的机器,选出的物品没有重复,直到选完为止。

我们把这个抽象基类命名为 Tombola,这是宾果机和打乱数字的滚动容器的意大利名。

Tombola 抽象基类有四个方法,其中两个是抽象方法。

.load(...):把元素放入容器。

.pick():从容器中随机拿出一个元素,返回选中的元素

另外两个是具体方法。

.loaded():如果容器中至少有一个元素,返回 True。

.inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)。

展示了 Tombola 抽象基类和三个具体实现。

python 接口_从协议到抽象基类详解

一个抽象基类和三个子类的 UML 类图。根据 UML 的约定,Tombola 抽象基类和它的抽象方法使用斜体。虚线箭头用于表示接口实现,这里它表示 TomboList 是 Tombola 的虚拟子类,因为TomboList 是注册的

import abc


class Tombola(abc.ABC):      #自己定义的抽象基类要继承abc.ABC

 @abc.abstractmethod
 def load(self, iterable):     #抽象方法使用@abstractmethod装饰器标记,而且定义体中通常只有文档字符串
 """从可迭代对象中添加元素"""

 @abc.abstractmethod
 def pick(self):      #根据文档字符串,如果没有元素可选,应该抛出LookupError
 """随机删除元素,然后返回
  如果实例为空,这个方法应该抛出'LookupError'
 """

 def loaded(self):      #抽象基类可以包含具体方法
 """如果至少有一个元素,返回`True`,否则返回`False`。"""
 return bool(self.inspect())

 def inspect(self):
 """返回一个有序元组,由当前元素构成。"""
 items = []
 while True:
  try:
  items.append(self.pick())   
  except LookupError:
  break
 self.load(items)     #使用 .load(...)把所有元素放回去
 return tuple(sorted(items))    #返回排序好的items攻loaded调用

选择使用 LookupError 异常的原因是,在 Python 的异常层次关系中,它与 IndexError 和 KeyError 有关,这两个是具体实现 Tombola的数据结构最有可能抛出的异常。据此,实现代码可能会抛出LookupError、IndexError 或 KeyError 异常。

异常类的部分层次结构

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
 ├── StopIteration
 ├── ArithmeticError
 │ ├── FloatingPointError
 │ ├── OverflowError
 │ └── ZeroDivisionError
 ├── AssertionError
 ├── AttributeError
 ├── BufferError
 ├── EOFError
 ├── ImportError
 ├── LookupError #我们在 Tombola.inspect 方法中处理的是 LookupError 异常 
 │ ├── IndexError #IndexError 是 LookupError 的子类,尝试从序列中获取索引超过最后位置的元素时抛出 
 │ └── KeyError #使用不存在的键从映射中获取元素时,抛出 KeyError 异常
 ├── MemoryError
 ... etc.

我们自己定义的 Tombola 抽象基类完成了。为了一睹抽象基类对接口所做的检查,下面我们尝试使用一个有缺陷的实现来糊弄 Tombola,如下面的 ? 所示

不符合 Tombola 要求的子类无法蒙混过关

>>> from tombola import Tombola
>>> class Fake(Tombola): 





# 把Fake声明为Tombole的子类,继承抽象类
...  def pick(self):
...  return 13
...
>>> Fake 










  # 创建Fake类,目前木有毛线问题~
<class '__main__.Fake'>
>>> f = Fake() 









# 报错了,Python认为Fake类是抽象类,因为没有实现load方法
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Fake with abstract methods load

抽象基类句法详解

声明抽象基类最简单的方式是继承 abc.ABC 或其他抽象基类。

然而,abc.ABC 是 Python 3.4 新增的类,因此如果你使用的是旧版Python,那么无法继承现有的抽象基类。此时,必须在 class 语句中使用 metaclass= 关键字,把值设为 abc.ABCMeta(不是 abc.ABC)。在下面的? ,可以写成:

class Tombola(metaclass=abc.ABCMeta):
 # ...

metaclass= 关键字参数是 Python 3 引入的。在 Python 2 中必须使用__metaclass__ 类属性:

class Tombola(object): # 这是Python 2!!!
 __metaclass__ = abc.ABCMeta
 # ...

除了 @abstractmethod 之外,abc 模块还定义了@abstractclassmethod、@abstractstaticmethod 和@abstractproperty 三个装饰器。然而,后三个装饰器从 Python 3.3起废弃了,因为装饰器可以在 @abstractmethod 上堆叠,那三个就显得多余了。例如,声明抽象类方法的推荐方式是:

class MyABC(abc.ABC):
 @classmethod
 @abc.abstractmethod
 def an_abstract_classmethod(cls, ...):
 pass

定义Tombola抽象基类的子类

定义好 Tombola 抽象基类之后,我们要开发两个具体子类,满足Tombola 规定的接口。

下面的? 中的 BingoCage 类,使用了更好的随机发生器。 BingoCage 实现了所需的抽象方法 load 和 pick,从 Tombola 中继承了 loaded 方法,覆盖了 inspect 方法,还增加了__call__ 方法。

import random
import abc


class Tombola(abc.ABC):      #自己定义的抽象基类要继承abc.ABC

 @abc.abstractmethod
 def load(self, iterable):     #抽象方法使用@abstractmethod装饰器标记,而且定义体中通常只有文档字符串
 """从可迭代对象中添加元素"""

 @abc.abstractmethod
 def pick(self):      #根据文档字符串,如果没有元素可选,应该抛出LookupError
 """随机删除元素,然后返回
  如果实例为空,这个方法应该抛出'LookupError'
 """

 def loaded(self):      #抽象基类可以包含具体方法
 """如果至少有一个元素,返回`True`,否则返回`False`。"""
 return bool(self.inspect())

 def inspect(self):
 """返回一个有序元组,由当前元素构成。"""
 items = []
 while True:
  try:
  items.append(self.pick())
  except LookupError:
  break
 self.load(items)     #使用 .load(...)把所有元素放回去
 return tuple(sorted(items))    #返回排序好的items攻loaded调用


#下面的代码应该单端放到一个py文件中,为了省事,就不在单独放到一个模块里面导入了~

class BigoCage(Tombola):     #明确指定BingoCage类扩展Tombola类

 def __init__(self, items):
 self._randomizer = random.SystemRandom()  #假设我们将在线上游戏中使用这个。random.SystemRandom使用os.urandom(...) 函数实现randomAPI
 self._items = []
 self.load(items)     #委托.load(...)方法实现初始加载

 def load(self, items):
 self._items.extend(items)    #如果通过load方法传递一个可迭代的对象进来,可以扩展到以后的self._items的列表中
 self._randomizer.shuffle(self._items)   #没有使用random.shuffle而是使用了SystemRandom中的shuffle方法

 def pick(self):
 try:
  return self._items.pop()
 except IndexError:
  raise LookupError('pick from empty BingoCage')

 def __call__(self):
 self.pick()

b = BigoCage(range(10))
print(b.pick())

下面是 Tombola 接口的另一种实现,虽然与之前不同,但完全有效。LotteryBlower 打乱“数字球”后没有取出最后一个,而是取出一个随机位置上的球。

lotto.py:LotteryBlower 是 Tombola 的具体子类,覆盖了继承的 inspect 和 loaded 方法

import random

from tombola import Tombola


class LotteryBlower(Tombola):

 def __init__(self, iterable):
 self._balls = list(iterable)  #创建一个副本

 def load(self, iterbale):
 self._balls.extend(iterbale)  #把一个可迭代的对象添加到列表中

 def pick(self):
 try:
  position = random.randrange(len(self._balls)) #获取一个随机数
 except ValueError:
  raise LookupError('pick from empty LotteryBlower')
 return self._balls.pop(position)  #删除列表中通过列表长度获取的随机数位置的索引值

 def loaded(self):
 return bool(self._balls)

 def inspect(self):
 return tuple(sorted(self._balls))

以上这篇python 接口_从协议到抽象基类详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
简单介绍Python中的filter和lambda函数的使用
Apr 07 Python
Python使用tablib生成excel文件的简单实现方法
Mar 16 Python
python socket多线程通讯实例分析(聊天室)
Apr 06 Python
使用Python实现博客上进行自动翻页
Aug 23 Python
python实现视频分帧效果
May 31 Python
python GUI库图形界面开发之PyQt5输入对话框QInputDialog详细使用方法与实例
Feb 27 Python
在python中求分布函数相关的包实例
Apr 15 Python
python爬取网易云音乐热歌榜实例代码
Aug 07 Python
如何在vscode中安装python库的方法步骤
Jan 06 Python
使用豆瓣源来安装python中的第三方库方法
Jan 26 Python
python利用后缀表达式实现计算器功能
Feb 22 Python
Python办公自动化之教你如何用Python将任意文件转为PDF格式
Jun 28 Python
Python调用ctypes使用C函数printf的方法
Aug 23 #Python
使用Python实现博客上进行自动翻页
Aug 23 #Python
Python模拟鼠标点击实现方法(将通过实例自动化模拟在360浏览器中自动搜索python)
Aug 23 #Python
Python PyQt5标准对话框用法示例
Aug 23 #Python
Python PyQt5实现的简易计算器功能示例
Aug 23 #Python
Python实现的密码强度检测器示例
Aug 23 #Python
python+selenium+autoit实现文件上传功能
Aug 23 #Python
You might like
Linux下ZendOptimizer的安装与配置方法
2007/04/12 PHP
PHP采用自定义函数实现遍历目录下所有文件的方法
2014/08/19 PHP
浅谈PHP拦截器之__set()与__get()的理解与使用方法
2016/10/18 PHP
自制PHP框架之设计模式
2017/05/07 PHP
阿里云PHP SMS短信服务验证码发送方法
2017/07/11 PHP
laravel学习笔记之模型事件的几种用法示例
2017/08/15 PHP
根据分辩率调用不同的CSS.
2007/01/08 Javascript
关于js拖拽上传 [一个拖拽上传修改头像的流程]
2011/07/13 Javascript
jquery实现table鼠标经过变色代码
2013/09/25 Javascript
jquery实现可拖拽弹出层特效
2015/01/04 Javascript
jQuery实用技巧必备(上)
2015/11/02 Javascript
利用node.js本地搭建HTTP服务器
2017/04/19 Javascript
express框架实现基于Websocket建立的简易聊天室
2017/08/10 Javascript
使用jQuery实现两个div中按钮互换位置的实例代码
2017/09/21 jQuery
mescroll.js上拉加载下拉刷新组件使用详解
2017/11/13 Javascript
Node.js API详解之 util模块用法实例分析
2020/05/09 Javascript
Node.js API详解之 zlib模块用法分析
2020/05/19 Javascript
vue监听浏览器原生返回按钮,进行路由转跳操作
2020/09/09 Javascript
vue keep-alive实现多组件嵌套中个别组件存活不销毁的操作
2020/10/30 Javascript
跟老齐学Python之有容乃大的list(1)
2014/09/14 Python
Python 爬虫图片简单实现
2017/06/01 Python
利用python 更新ssh 远程代码 操作远程服务器的实现代码
2018/02/08 Python
python并发编程多进程之守护进程原理解析
2019/08/20 Python
python查看数据类型的方法
2019/10/12 Python
python 求定积分和不定积分示例
2019/11/20 Python
Python爬虫解析网页的4种方式实例及原理解析
2019/12/30 Python
BLACKMORES澳洲官网:澳大利亚排名第一的保健品牌
2018/09/27 全球购物
英国家喻户晓的家居商店:The Range
2019/03/25 全球购物
技术副厂长岗位职责
2013/12/26 职场文书
大学生职业生涯规划书模板
2014/01/03 职场文书
初中优秀教师事迹材料
2014/08/18 职场文书
2014年绿化工作总结
2014/12/09 职场文书
2015年班级工作总结范文
2015/04/03 职场文书
反四风问题学习心得体会
2016/01/22 职场文书
深入解读Java三大集合之map list set的用法
2021/11/11 Java/Android
mysql sock文件存储了什么信息
2022/07/15 MySQL