首页 > 其他 > 详细

接口:从协议到抽象基类

时间:2019-03-06 12:40:08      阅读:137      评论:0      收藏:0      [点我收藏+]

抽象类表示接口。                    ---------Bjarne StroustrupC++之父

Python中的接口和协议

引入抽象基类之前,python就已经很成功了,即便现在也很少有代码使用抽象基类。协议可以看成是非正式的接口,是python这种动态语言实现动态的方式。

接口在动态语言中是如何运作的呢?首先,python中除了抽象基类,每个类都有接口:类实现和公开的属性,包括特殊方法如__getitem__, __iter__等。

关于接口,接口是实现特定角色的方法的集合。一个类可能会实现多个接口,从而让实例扮演多种角色。

协议是接口,但不是正式的,一个类允许只实现部分接口。有时,有些API只要求对象拥有.read()方法即可。

序列协议是pyhton最基础的协议之一。即便只实现了那个协议最基础的一部分,解释器也会对其进行处理。

Python喜欢序列

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

序列Sequence的抽象基类接口:

技术分享图片

我们自己实现一个Foo类,它并不继承abc.Sequence,而是实现序列协议的一个方法__getitem__。

 1 class Foo:
 2     def __getitem__(self, index):
 3         return (range(30))[index]
 4 
 5 foo = Foo()
 6 for val in foo:
 7     print(val)
 8 print(foo[2])
 9 print(foo[-1])
10 print(-7 in foo)

虽然我们并没有实现__iter__和__contains__方法,但是foo却是一个可迭代的对象,为什么呢?

我们定义了__getitem__方法,python会调用它,从下标为0开始,尝试着迭代对象(这属于python的一种后备机制)。同理,尽管并没有__contains__方法,但是仍然可以使用in运算符来遍历对象查找指定元素是否存在。

也就是说,如果没有__iter__和__contains__方法,python会退而求其次调用__getitem__方法,设法让迭代和in运算符可用。

 1 class FrenchDeck:
 2     ranks = [str(n) for n in range(2, 11)] + list(JQKA)
 3     suits = spades diamonds clubs hearts.split()
 4     def __init__(self):
 5         self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits]
 6 
 7     def __len__(self):
 8         return len(self._cards)
 9 
10     def __getitem__(self, index):
11         return self._cards[index]    
12 
13 deck = FrenchDeck()
14 print(len(deck))
15 print(deck[2])

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

FrenchDeck有个缺陷就是无法洗牌,random模块中有个shuffle方法。python文档对齐描述是就地打乱列表顺序。

1 import random
2 l = list(range(30))
3 print(l)
4 random.shuffle(l)
5 print(l)

将shuffle应用在deck上:

技术分享图片

解释器报错,根据异常信息可以知道,FrenchDeck不支持元素赋值,那就手动添加一下吧。

方法一:在类内实现__setitem__方法

 1 class FrenchDeck:
 2     ranks = [str(n) for n in range(2, 11)] + list(JQKA)
 3     suits = spades diamonds clubs hearts.split()
 4     def __init__(self):
 5         self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits]
 6 
 7     def __len__(self):
 8         return len(self._cards)
 9 
10     def __getitem__(self, index):
11         return self._cards[index]
12 
13     def __setitem__(self, index, val):
14         self._cards[index] = val    
15 
16 deck = FrenchDeck()
17 print(len(deck))
18 print(deck[2])
19 
20 for card in deck:
21     print(card)
22 
23 random.shuffle(deck)
24 for card in deck:
25     print(card)

方法二:打补丁(运行时添加)

 1 class FrenchDeck:
 2     ranks = [str(n) for n in range(2, 11)] + list(JQKA)
 3     suits = spades diamonds clubs hearts.split()
 4     def __init__(self):
 5         self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits]
 6 
 7     def __len__(self):
 8         return len(self._cards)
 9 
10     def __getitem__(self, index):
11         return self._cards[index]
12 
13     #def __setitem__(self, index, val):
14     #    self._cards[index] = val    
15 
16 def set_card(deck, index, val):
17     deck._cards[index] = val
18 deck = FrenchDeck()
19 print(len(deck))
20 print(deck[2])
21 
22 for card in deck:
23     print(card)
24 FrenchDeck.__setitem__ = set_card
25 random.shuffle(deck)
26 for card in deck:
27     print(card)

方法二在类外部定义了set_card函数,然后设置类属性__setitem__引用set_card,__setitem__接收三个参数,第一个是对象自身,第二个是索引下标,第三个是值,参数名是无关紧要的,在类内部方法第一个参数命名成self是中python约定俗成的惯例,你命名成其他名字除了不规范之外,对程序本身没有任何影响。

1 class Foo:
2     def f(python):
3         print(python)
4         print("--------")
5 
6 foo = Foo()
7 foo.f()

 

方法二这种技术叫猴子补丁:在运行时修改类或模块,不改动源码。

此外,还说明一点,random.shuffle不关心它的参数类型,只要参数实现了序列协议即可,这就是鸭子类型。

鸭子类型:不关心对象的具体类型,对象只需实现特定的协议即可。

什么是抽象基类

抽象基类是用于封装框架引入的一般性概念和抽象的,例如“一个序列”和“一个
确切的数”。(读者)基本上不需要自己编写新的抽象基类,只要正确使用现有的抽象基类,就能获得 99.9% 的好处,而不用冒着设计不当导致的巨大风险。          


 

 

接口:从协议到抽象基类

原文:https://www.cnblogs.com/forwardfeilds/p/10482179.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!