鸭子类型

"If it walks like a duck and quacks like a duck, it's a duck."

Python 不在乎对象的类型,只在乎它有没有需要的方法

def make_quack(duck):
    duck.quack()

class RealDuck:
    def quack(self): print("Quack!")

class FakeDuck:
    def quack(self): print("噶噶噶")

make_quack(RealDuck())     # OK
make_quack(FakeDuck())     # 也 OK

ABC:明确规定"必须实现哪些方法"

abstractmethod 装饰的方法子类必须实现,否则不能实例化:

from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def save(self, key, value): pass

    @abstractmethod
    def load(self, key): pass


class MemoryStorage(Storage):
    def __init__(self):
        self.data = {}

    def save(self, key, value):
        self.data[key] = value

    def load(self, key):
        return self.data.get(key)


# class IncompleteStorage(Storage):
#     def save(self, k, v): self.x = v
# IncompleteStorage()    # TypeError: 必须实现 load

ABC 的好处

  1. 早报错:忘了实现某方法,实例化时就报错,不用等到运行时
  2. 文档作用:看一眼 ABC 就知道"这一族"对象的接口
  3. isinstance 检查isinstance(s, Storage) 能查所有子类

Protocol:结构化子类型(PEP 544)

ABC 要显式继承才生效。Protocol 不需要——只要"长得像":

from typing import Protocol

class Quackable(Protocol):
    def quack(self) -> None: ...

class Duck:
    def quack(self):
        print("Quack!")

def make_quack(d: Quackable):     # 类型注解
    d.quack()

make_quack(Duck())                # OK,Duck 没继承 Quackable,但符合协议

mypy / pyright 会根据"长得像不像"来判断,不需要继承。这就是 PEP 544 的"结构化子类型"。

何时用 ABC,何时用 Protocol

场景
内部代码、明确知道实现者 ABC
鸭子类型 + 静态类型检查 Protocol
第三方库不能改它 Protocol(不需要它继承你的类)
想用 isinstance 运行时检查 ABC
Python 3.7 及以下 ABC(Protocol 是 3.8+)

标准库里的 ABC 例子

collections.abc 模块有大量预定义 ABC:

from collections.abc import Iterable, Sized, Mapping

class MyCol:
    def __iter__(self): yield 1
    def __len__(self): return 1

isinstance(MyCol(), Iterable)    # True
isinstance(MyCol(), Sized)       # True

定义自己的容器时常常继承 MutableMapping / Sequence 等,免得每个魔术方法都自己写。

runtime_checkable Protocol

Protocol 默认是静态的(只供类型检查)。加 @runtime_checkable 后可以 isinstance:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Closable(Protocol):
    def close(self) -> None: ...

isinstance(open("f.txt"), Closable)    # True

下一篇讲魔术方法 dunder——让自定义类长得像内置类型。