Skip to content

Latest commit

 

History

History
330 lines (230 loc) · 15.2 KB

File metadata and controls

330 lines (230 loc) · 15.2 KB

1、关于接口,这里有个实用的补充定义:对象公开方法的子集,让对象在系统中扮演特定的角色。Python文档中的“文件类对象”或“可迭代对象”就是这个意思,这种说法指的不是特定的类。接口是实现特定角色的方法集合,这样理解正是Smalltalk程序员所说的协议,其他动态语言社区都借鉴了这个术语。协议与继承没有关系。一个类可能会实现多个接口,从而让实例扮演多个角色。

2、协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制(本章后面会说明抽象基类对接口一致性的强制)。一个类可能只实现部分接口,这是允许的。

3、定义__getitem__方法,只实现序列协议的一部分,这样足够访问元素、迭代和使用in运算符了.

>>> class Foo:
...     def __getitem__(self, pos):
...             return range(0, 30, 10)[pos]
... 
>>> f = Foo()
>>> f[1]
10
>>> for i in f: print(i)
... 
0
10
20
>>> 20 in f
True
>>> 15 in f
False

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

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

5、可变的序列还必须提供__setitem__方法。

6、Python是动态语言,因此我们可以在运行时修正这个问题,甚至还可以在交互式控制台中。

import collections
Card = collections.namedtuple('Card', ['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 = [Card(rank, suit) for rank in self.ranks for suit in self.suits]

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

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

第一次运行时

>>> from frenchDeck import FrenchDeck
>>> from random import shuffle
>>> deck = FrenchDeck()
>>> len(deck)
52
>>> shuffle(deck)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.6/random.py", line 277, in shuffle
    x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment

动态添加__setitem__:

>>> def set_card(deck, pos, card):
...     deck._cards[pos] = card
... 
>>> FrenchDeck.__setitem__ = set_card
>>> from random import shuffle
>>> shuffle(deck)
>>> deck[:5]
[Card(rank='K', suit='diamonds'), Card(rank='3', suit='hearts'), Card(rank='4', suit='spades'), Card(rank='6', suit='hearts'), Card(rank='4', suit='diamonds')]

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

7、Python的抽象基类还有一个重要的实用优势:可以使用register类方法在终端用户的代码中把某个类“声明”为一个抽象基类的“虚拟”子类(为此,被注册的类必须满足抽象基类对方法名称和签名的要求,最重要的是要满足底层语义契约;但是,开发那个类时不用了解抽象基类,更不用继承抽象基类)。这大大地打破了严格的强耦合,与面向对象编程人员掌握的知识有很大出入,因此使用继承时要小心。

8、此外,使用isinstance和issubclass测试抽象基类更为人接受。过去,这两个函数用来测试鸭子类型,但用于抽象基类会更灵活。毕竟,如果某个组件没有继承抽象基类,事后还可以注册,让显式类型检查通过。

9、然而,即便是抽象基类,也不能滥用isinstance检查,用得多了可能导致代码异味,即表明面向对象设计得不好。在一连串if/elif/elif中使用isinstance做检查,然后根据对象的类型执行不同的操作,通常是不好的做法;此时应该使用多态,即采用一定的方式定义类,让解释器把调用分派给正确的方法,而不使用if/elif/elif块硬编码分派逻辑。

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

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


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

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

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

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

    def __setitem__(self, pos, value): #1
        self._cards[pos] = value

    def __delitem__(self, pos):  #2
        del self._cards[pos]

    def insert(self, pos, value): #3
        self._cards.insert(pos, value)

❶ 为了支持洗牌,只需实现__setitem__方法。

❷ 但是继承MutableSequence的类必须实现__delitem__方法,这是MutableSequence类的一个抽象方法。

❸ 此外,还要实现insert方法,这是MutableSequence类的第三个抽象方法。

11、标准库中有两个名为abc的模块,这里说的是collections.abc。为了减少加载时间,Python3.4在collections包之外实现这个模块(在Lib/_collections_abc.py中),因此要与collections分开导入。另一个abc模块就是abc(即Lib/abc.py),这里定义的是abc.ABC类。每个抽象基类都依赖这个类,但是不用导入它,除非定义新抽象基类。

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的子类

12、numbers包定义的是“数字塔”(即各个抽象基类的层次结构是线性的),其中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,它做了相应的注册)提供的非复数类型。

13、抽象方法可以有实现代码。即便实现了,子类也必须覆盖抽象方法,但是在子类中可以使用super( )函数调用抽象方法,为它添加功能,而不是从头开始实现。

14、异常类的部分层次结构

Tombola是抽象基类,有两个抽象方法和两个具体方法

import abc
class Tombola(abc.ABC): #1
    @abc.abstractmethod  #2
    def load(self, iterable):
        """从可迭代对象中添加元素"""

    @abc.abstractmethod
    def pick(self):  #3
        """随机删除原属,然后将其返回
        如果实例为空,这个方法应该抛出'LookupError'
        """

    def loaded(self): #4
        return bool(self.inspect()) #5

    def inspect(self):
        items = []
        while True:
            try:
                items.append(self.pick()) #6
            except LookupError:
                break
        self.load(items) #7
        return tuple(sorted(items))

❶ 自己定义的抽象基类要继承abc.ABC。

❷ 抽象方法使用@abstractmethod装饰器标记,而且定义体中通常只有文档字符串

❸ 根据文档字符串,如果没有元素可选,应该抛出LookupError。

❹ 抽象基类可以包含具体方法。

❺ 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具体方法、抽象方法或特性)。

❻ 我们不知道具体子类如何存储元素,不过为了得到inspect的结果,我们可以不断调用.pick( )方法,把Tombola清空……

❼ ……然后再使用.load(...)把所有元素放回去。

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

>>> from tombola import Tombola
>>> class Fake(Tombola): #1
...     def pick(self):
...             return 13
... 
>>> Fake #2
<class '__main__.Fake'>
>>> f = Fake() #3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Fake with abstract methods load

❶ 把Fake声明为Tombola的子类。

❷ 创建了Fake类,目前没有错误。

❸ 尝试实例化Fake时抛出了TypeError。错误消息十分明确:Python认为Fake是抽象类,因为它没有实现load方法,这是Tombola抽象基类声明的抽象方法之一。

15、在函数上堆叠装饰器的顺序通常很重要,@abstractmethod的文档就特别指出:与其他方法描述符一起使用时,abstractmethod( )应该放在最里层,…也就是说,在@abstractmethod和def语句之间不能有其他装饰器。

16、虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了避免运行时错误,虚拟子类要实现所需的全部方法。

创建一个基类:

import abc
class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从可迭代对象中添加元素"""

    @abc.abstractmethod
    def pick(self):
        """随机删除原属,然后将其返回
        如果实例为空,这个方法应该抛出'LookupError'
        """

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

    def inspect(self):
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))

注册虚拟子类

from random import randrange
from tombola import Tombola

@Tombola.register
class TomboList(list):
    def pick(self):
        if self:
            pos = randrange(len(self))
            return self.pop(pos)
        else:
            raise  LookupError('pop from empty TomboList')

    load = list.extend

    def loaded(self):
        return bool(self)

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

注册之后,可以使用issubclass和isinstance函数判断TomboList是不是Tombola的子类:

>>> from tombola import Tombola
>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True

类的继承关系在一个特殊的类属性中指定——mro,即方法解析顺序(MethodResolution Order)。这个属性的作用很简单,按顺序列出类及其超类,Python会按照这个顺序搜索方法。查看TomboList类的__mro__属性,你会发现它只列出了“真实的”超类,即list和object:

>>> TomboList.__mro__
(<class 'tomnolist.TomboList'>, <class 'list'>, <class 'object'>)

Tombolist.__mro__中没有Tombola,因此Tombolist没有从Tombola中继承任何方法。

17、在自己定义的抽象基类中要不要实现__subclasshook__方法呢?可能不需要。我在Python源码中只见到Sized这一个抽象基类实现了__subclasshook__方法,而Sized只声明了一个特殊方法,因此只用检查这么一个特殊方法。鉴于__len__方法的“特殊性”,我们基本可以确定它能做到该做的事。但是对其他特殊方法和基本的抽象基类来说,很难这么肯定。例如,虽然映射实现了__len__、__getitem__和__iter__,但是不应该把它们视作Sequence的子类型,因为不能使用整数偏移值获取元素,也不能保证元素的顺序。当然,OrderedDict除外,它保留了插入元素的顺序,但是不支持通过偏移获取元素。

强类型和弱类型

如果一门语言很少隐式转换类型,说明它是强类型语言;如果经常这么做,说明它是弱类型语言。Java、C++和Python是强类型语言。PHP、JavaScript和Perl是弱类型语言。

静态类型和动态类型

在编译时检查类型的语言是静态类型语言,在运行时检查类型的语言是动态类型语言。静态类型需要声明类型(有些现代语言使用类型推导避免部分类型声明)。Fortran和Lisp是最早的两门语言,现在仍在使用,它们分别是静态类型语言和动态类型语言。

猴子补丁

猴子补丁的名声不太好。如果滥用,会导致系统难以理解和维护。补丁通常与目标紧密耦合,因此很脆弱。另一个问题是,打了猴子补丁的两个库可能相互牵绊,因为第二个库可能撤销了第一个库的补丁。

不过猴子补丁也有它的作用,例如可以在运行时让类实现协议。适配器设计模式通过实现全新的类解决这种问题。

为Python打猴子补丁不难,但是有些局限。与Ruby和JavaScript不同,Python不允许为内置类型打猴子补丁。其实我觉得这是优点,因为这样可以确保str对象的方法始终是那些。这一局限能减少外部库打的补丁有冲突的概率。