Python 抽象基类(ABC)和协议(Protocol)对比与选择.md

环境

采用 Python 3.8, pyright 1.1

接口的样子:

1class ConfigSource:
2    def sync(self, dest: str) -> bool:
3        ...

将会变化不同的 ConfigSource 实现。

下面是具体类型,在整个实验中不会修改:

 1class ObtuseAngle:
 2    def talk(self):
 3        print("I'm obtuse")
 4
 5
 6class LocalConfigSource:
 7    def sync(self, dest: str) -> bool:
 8        print(f"Syncing from local to {dest}")
 9        return True
10
11
12class S3ConfigSource(ConfigSource):
13    def sync(self, dest: str) -> bool:
14        print(f"Syncing from s3 to {dest}")
15        return True
16
17
18class OnedriveConfigSource(ConfigSource):
19    def blabula(self):
20        pass
  • ObtuseAngle 是一个与 ConfigSource 无关的类。

  • LocalConfigSource 实际上实现了 ConfigSource 接口。但是它没有显式地声明它是 ConfigSource 的子类。

  • S3ConfigSource 不但实现了 ConfigSource 接口,而且显式地声明了它是 ConfigSource 的子类。

  • OnedriveConfigSource 声称是 ConfigSource 的子类,但是它的接口与 ConfigSource 不一致。

示例代码

完整代码:

情况 1

说明

采用抽象基类的方式来定义 ConfigSource 接口:

1class ConfigSource(metaclass=abc.ABCMeta):
2    @abc.abstractmethod
3    def sync(self, dest: str) -> bool:
4        return NotImplemented

类静态检查结果

  1. ObtuseAngle 无报错。

  2. LocalConfigSource 无报错。

  3. S3ConfigSource 无报错。

  4. OnedriveConfigSource 报错。Method ‘sync’ is abstract in class ‘ConfigSource’ but is not overridden in child class ‘OnedriveConfigSource’PylintW0223:abstract-method

参数静态检查结果

  1. ObtuseAngle 报错。“ObtuseAngle” is incompatible with “ConfigSource"PylancereportGeneralTypeIssues

  2. LocalConfigSource 报错。“LocalConfigSource” is incompatible with “ConfigSource"PylancereportGeneralTypeIssues

  3. S3ConfigSource 无报错。

  4. OnedriveConfigSource 报错。

    1. Cannot instantiate abstract class “OnedriveConfigSource”

    2. Abstract class ‘OnedriveConfigSource’ with abstract methods instantiatedPylintE0110:abstract-class-instantiated

运行时实例化结果

  1. ObtuseAngle 无报错。

  2. LocalConfigSource 无报错。

  3. S3ConfigSource 无报错。

  4. OnedriveConfigSource 报错。TypeError: Can’t instantiate abstract class OnedriveConfigSource with abstract methods sync

运行时调用结果

  1. ObtuseAngle 报错:AttributeError: ‘ObtuseAngle’ object has no attribute ‘sync’

  2. LocalConfigSource 无报错。

  3. S3ConfigSource 无报错。

  4. OnedriveConfigSource 报错:ValueError: Unknown variant onedrive

结论

  1. 静态检查能够检查出所有问题。

  2. 对于不符合接口的类,只要非抽象,依然可以实例化,调用时才暴露出所有问题。

情况 2

说明

改用 Protocol 的方式来定义 ConfigSource 接口:

1class ConfigSource(Protocol):
2    @abc.abstractmethod
3    def sync(self, dest: str) -> bool:
4        return NotImplemented

保留了抽象方法的定义,但是不再使用抽象基类。

类静态检查结果

  1. ObtuseAngle 无报错。

  2. LocalConfigSource 无报错。

  3. S3ConfigSource 无报错。 4 .OnedriveConfigSource 报错。Method ‘sync’ is abstract in class ‘ConfigSource’ but is not overridden in child class ‘OnedriveConfigSource’PylintW0223:abstract-method

参数静态检查结果

  1. ObtuseAngle 报错。“ObtuseAngle” is incompatible with “ConfigSource"PylancereportGeneralTypeIssues

  2. LocalConfigSource 无报错。

  3. S3ConfigSource 无报错。

  4. OnedriveConfigSource 报错。 Cannot instantiate abstract class “OnedriveConfigSource"“ConfigSource.sync” is abstract

运行时实例化结果

TypeError: Can’t instantiate abstract class OnedriveConfigSource with abstract methods sync Available sources: dict_keys([‘obtuse’, ’local’, ‘s3’]) AttributeError: ‘ObtuseAngle’ object has no attribute ‘sync’ Syncing from local to dir1 Syncing from s3 to dir1 ValueError: Unknown variant onedrive

  1. ObtuseAngle 无报错。

  2. LocalConfigSource 无报错。

  3. S3ConfigSource 无报错。

  4. OnedriveConfigSource 报错。TypeError: Can’t instantiate abstract class OnedriveConfigSource with abstract methods sync

运行时调用结果

  1. ObtuseAngle 报错:AttributeError: ‘ObtuseAngle’ object has no attribute ‘sync’

  2. LocalConfigSource 无报错。

  3. S3ConfigSource 无报错。

  4. OnedriveConfigSource 报错:ValueError: Unknown variant onedrive

结论

  1. 对于显式实现 Protocol 的,静态检查能够检查出问题。

  2. 对于不符合接口的类,只要非抽象,依然可以实例化,调用时才暴露出所有问题。这一点与抽象基类的情况一致。

情况 3

说明

采用 Protocol 的方式来定义 ConfigSource 接口,但是不再使用抽象方法的定义:

1class ConfigSource(Protocol):
2    def sync(self, dest: str) -> bool:
3        return NotImplemented

类静态检查结果

无任何报错

参数静态检查结果

只产生一个错误:

“ObtuseAngle” is incompatible with protocol “ConfigSource” “sync” is not present

运行时实例化结果

无任何报错

运行时调用结果

  1. ObtuseAngle 报错:AttributeError: ‘ObtuseAngle’ object has no attribute ‘sync’

无其它报错

结论

  1. 对于显式实现 Protocol 的,静态检查能够检查出问题。

  2. 不符合接口的类都可以实例化,调用时才暴露出所有问题。

最终结论

  1. 仅仅依赖一个 Protocol 类,适用于鸭子类型的情况。例如实际上实现 Protocol 的类,其不知道 Protocol 的存在,而又希望依赖接口而非抽象。

    • 好处:最高的灵活性、提供类型提示。

    • 坏处:只有在具体成员被使用时才会暴露出问题。

  2. 更多情况下,可以使用 Protocol + 抽象方法的方式。无论子类是否显式声明实现了 Protocol,都可以在静态检查时暴露出问题。

    • 好处:在静态检查时就能暴露出问题、类型提示。

    • 坏处:如果不显式声明实现了 Protocol,则缺失方法时无法在实现处得知,只能在调用处得知。

  3. 使用 ABC + 抽象方法的方式,可以提供最强的检查和约束。

    • 好处:在静态检查时就能暴露出问题、类型提示。

    • 坏处:灵活性差,必须显式实现,并且具有很大的性能开销。

建议

如果你的项目比较规范,更推荐采用 Protocol + 抽象方法的方式。而如果你的项目开发者质量参差不齐,比如有些人开发时不使用静态分析工具,只是能运行不报错就行,那么还是用 ABC + 抽象方法的方式吧。