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
类静态检查结果
-
ObtuseAngle 无报错。
-
LocalConfigSource 无报错。
-
S3ConfigSource 无报错。
-
OnedriveConfigSource 报错。Method ‘sync’ is abstract in class ‘ConfigSource’ but is not overridden in child class ‘OnedriveConfigSource’PylintW0223:abstract-method
参数静态检查结果
-
ObtuseAngle 报错。“ObtuseAngle” is incompatible with “ConfigSource"PylancereportGeneralTypeIssues
-
LocalConfigSource 报错。“LocalConfigSource” is incompatible with “ConfigSource"PylancereportGeneralTypeIssues
-
S3ConfigSource 无报错。
-
OnedriveConfigSource 报错。
-
Cannot instantiate abstract class “OnedriveConfigSource”
-
Abstract class ‘OnedriveConfigSource’ with abstract methods instantiatedPylintE0110:abstract-class-instantiated
-
运行时实例化结果
-
ObtuseAngle 无报错。
-
LocalConfigSource 无报错。
-
S3ConfigSource 无报错。
-
OnedriveConfigSource 报错。TypeError: Can’t instantiate abstract class OnedriveConfigSource with abstract methods sync
运行时调用结果
-
ObtuseAngle 报错:AttributeError: ‘ObtuseAngle’ object has no attribute ‘sync’
-
LocalConfigSource 无报错。
-
S3ConfigSource 无报错。
-
OnedriveConfigSource 报错:ValueError: Unknown variant onedrive
结论
-
静态检查能够检查出所有问题。
-
对于不符合接口的类,只要非抽象,依然可以实例化,调用时才暴露出所有问题。
情况 2
说明
改用 Protocol 的方式来定义 ConfigSource 接口:
1class ConfigSource(Protocol):
2 @abc.abstractmethod
3 def sync(self, dest: str) -> bool:
4 return NotImplemented
保留了抽象方法的定义,但是不再使用抽象基类。
类静态检查结果
-
ObtuseAngle 无报错。
-
LocalConfigSource 无报错。
-
S3ConfigSource 无报错。 4 .OnedriveConfigSource 报错。Method ‘sync’ is abstract in class ‘ConfigSource’ but is not overridden in child class ‘OnedriveConfigSource’PylintW0223:abstract-method
参数静态检查结果
-
ObtuseAngle 报错。“ObtuseAngle” is incompatible with “ConfigSource"PylancereportGeneralTypeIssues
-
LocalConfigSource 无报错。
-
S3ConfigSource 无报错。
-
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
-
ObtuseAngle 无报错。
-
LocalConfigSource 无报错。
-
S3ConfigSource 无报错。
-
OnedriveConfigSource 报错。TypeError: Can’t instantiate abstract class OnedriveConfigSource with abstract methods sync
运行时调用结果
-
ObtuseAngle 报错:AttributeError: ‘ObtuseAngle’ object has no attribute ‘sync’
-
LocalConfigSource 无报错。
-
S3ConfigSource 无报错。
-
OnedriveConfigSource 报错:ValueError: Unknown variant onedrive
结论
-
对于显式实现 Protocol 的,静态检查能够检查出问题。
-
对于不符合接口的类,只要非抽象,依然可以实例化,调用时才暴露出所有问题。这一点与抽象基类的情况一致。
情况 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
运行时实例化结果
无任何报错
运行时调用结果
- ObtuseAngle 报错:AttributeError: ‘ObtuseAngle’ object has no attribute ‘sync’
无其它报错
结论
-
对于显式实现 Protocol 的,静态检查能够检查出问题。
-
不符合接口的类都可以实例化,调用时才暴露出所有问题。
最终结论
-
仅仅依赖一个 Protocol 类,适用于鸭子类型的情况。例如实际上实现 Protocol 的类,其不知道 Protocol 的存在,而又希望依赖接口而非抽象。
-
好处:最高的灵活性、提供类型提示。
-
坏处:只有在具体成员被使用时才会暴露出问题。
-
-
更多情况下,可以使用 Protocol + 抽象方法的方式。无论子类是否显式声明实现了 Protocol,都可以在静态检查时暴露出问题。
-
好处:在静态检查时就能暴露出问题、类型提示。
-
坏处:如果不显式声明实现了 Protocol,则缺失方法时无法在实现处得知,只能在调用处得知。
-
-
使用 ABC + 抽象方法的方式,可以提供最强的检查和约束。
-
好处:在静态检查时就能暴露出问题、类型提示。
-
坏处:灵活性差,必须显式实现,并且具有很大的性能开销。
-
建议
如果你的项目比较规范,更推荐采用 Protocol + 抽象方法的方式。而如果你的项目开发者质量参差不齐,比如有些人开发时不使用静态分析工具,只是能运行不报错就行,那么还是用 ABC + 抽象方法的方式吧。