FluentPythonCh11

 21st November 2017 at 4:35pm

这一章主要讲 ABC 的机制。但是处处提醒读者 不要 随意自定义新的 ABC,而是使用已有的 ABC:

ABCs, like descriptors and metaclasses, are tools for building frameworks. Therefore, only a very small minority of Python developers can create ABCs without imposing unreasonable limitations and needless work on fellow programmers.

Interfaces and Protocols in Python Culture

对于 Python 来说(或者对于大部分动态语言来说),接口(interface)是用类的一些公共 attibutes (method or data attributes) 来表达的。

Protocol 是不正式的 interfaces,它是靠文档和约定(convention)来定义的。比如 "a file-like object" 是一个实现了 .read() 函数的类。而且它不保证一整套函数都实现了,比如一个支持 slicing 的 sequence 类,它有可能没有长度这个概念,于是它可以不实现 __len__ 函数。

Python Digs Sequences

Python 总是尽力去实现基础的 protocol(这个表达可能不好)。比如下面这个类:

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

这个类并没有实现 __iter__, __contains__ 函数,但是也可以被使用在相应的语法中,因为聪明的 Python 会把 __getitem__ 作为一个 fallback 去遍历。

Monkey-Patching to Implement a Protocol at Runtime

这节在我看来不是 best practise。它通过 hack 进一个 __setitem__ 函数到 FrenchDeck 类中来使这个类支持被 random.shuffle() 操作。这意味着 hack 进去的函数必需知道 FrenchDeck 类的内部实现是怎样的,而且它还使用了类里面的私有变量。但是这也展示了一种可能性,后面写代码时可以权衡下。

Alex Martelli’s Waterfowl

Alex Martelli 的故事很长且难看懂,主要是从特种的分类学出发,描述其分类从按语音(phonetics)到按其 DNS 序列的吻合程度(cladistics)的演变。里面有一段提到 ABC 的好处,即可以 register 而不需要继承,使得减少了很多继承带来的耦合性:

Among the many conceptual advantages of ABCs over concrete classes (e.g., Scott Meyer’s “all non-leaf classes should be abstract”—see Item 33 in his book, More Effective C++), Python’s ABCs add one major practical advantage: the register class method, which lets end-user code “declare” that a certain class becomes a “virtual” subclass of an ABC (for this purpose the registered class must meet the ABC’s method name and signature requirements, and more importantly the underlying semantic contract—but it need not have been developed with any awareness of the ABC, and in particular need not inherit from it!). This goes a long way toward breaking the rigidity and strong coupling that make inheritance something to use with much more caution than typically practiced by most OOP programmers…

Subclassing an ABC

Python does not check for the implementation of the abstract methods at import time

(when the frenchdeck2.py module is loaded and compiled), but only at runtime when we actually try to instantiate FrenchDeck2.

Python 只会在运行时,实例化一个类时才去检查它是否实现了它的 ABC 父类要求的函数。

图中的斜体部分,表示虚函数,是你需要实现的函数。Python 的 ABC 也会向提供一些函数供你使用,比如 MutableSequence 中的 append 是不需要你实现的,你只需要实现 insert 函数,append 函数就会调用它来实现自身逻辑。

同时,对于 ABC 默认实现的函数,你也可以写自己的版本覆盖它,实现更好的性能。比如 Sequence 类的 __contains__ 函数,默认实现是遍历 sequence,你可以写一个性能更好的版本替代它。

ABCs in the Standard Library

Python 中最重要的 ABC 分布在两个地方:

书中的 UML 图比较直观地表示了各 ABC 间的关系。

decimal.Decimal 没有注册在 numbers.Real 中。因为 Decimal 的精度跟普通符点数不一样。

Defining and Using an ABC

上代码:

import abc
class Tombola(abc.ABC):

    @abc.abstractmethod
    def load(self, iterable):
        """Add items from an iterable."""

    @abc.abstractmethod
    def pick(self):
        """Remove item at random, returning it.
        This method should raise `LookupError` when the instance is empty."""

    def loaded(self):
        """Return `True` if there's at least 1 item, `False` otherwise."""
        return bool(self.inspect())

    def inspect(self):
        """Return a sorted tuple with the items currently inside."""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))

划重点:

  1. ABC 中可以提供具体的函数实现。但是函数实现必须依赖于公开接口,而不能有私有数据
  2. 用了 @abstractmethod 的函数,也可以有具体实现,只是子类仍然需要写一个自己的版本;不过子类可以调用父类的同名函数,不用重头写起
  3. pick 函数抛出一个 LookupError有意为之,因为 LookupError 有两个子类 IndexErrorKeyError,方便子类抛出更具体的异常。虽然没有具体机制去约束之类可能抛出的异常,但是子类还是应该与父类保持一致

@abc.abstractmethod 可以叠 @classmethod, @staticmethod, @property,只需要 @abc.abstractmethod 在最内层。

A Virtual Subclass of Tombola

register 让一个类声明自己实现了 ABC 所要求的接口,而不需要继承它。特点:

  • 能过 issubclass isinstanceof 检查
  • Python 解释器不会在任何时刻检查它的接口是否实现(甚至是在子类实例初始化时)

How the Tombola Subclasses Were Tested

演示了一把怎样用 doc test 来测试一个 reStructuredText 文档中的测试样例。

Geese Can Behave as Ducks

Sized 用了 __subclasshook__,会用来做 ABCMeta.__subclasscheck__

class Sized(metaclass=ABCMeta):
    __slots__ = ()
    
    @abstractmethod
    def __len__(self):
        return 0
        
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Sized:
            if any("__len__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

所以:

>>> class Struggle:
...     def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True

但是这个 __subclasshook__ 适用的范围很小。自己写的代码不要用它。比如 dict 虽然实现了 Sequence 所要求的 __len__ __getitem__ __iter__,但是它不是 Sequence,因为它的内部元素不是有序的。所以 dict 的实现上不会用 __subclasshook__ 做动态的 dunder function 判断。

Soapbox

Strong versus weak typing

If the language rarely performs implicit conversion of types, it’s considered strongly typed; if it often does it, it’s weakly typed. Java, C++, and Python are strongly typed. PHP, JavaScript, and Perl are weakly typed.

Static versus dynamic typing

If type-checking is performed at compile time, the language is statically typed; if it happens at runtime, it’s dynamically typed. Static typing requires type declarations (some modern languages use type inference to avoid some of that). Fortran and Lisp are the two oldest programming languages still alive and they use, respectively, static and dynamic typing.

这节把强 / 弱类型、静态 / 动态类型做了描述。所以 Python 是强类型 + 动态类型。