子类化io.RawIOBase时如何为write方法添加类型注解,避免违反里氏替换原则(Liskov substitution principle)
我太懂你这种闹心的感觉了——本来只是想给子类的write方法加个精准的类型注解,结果mypy直接抛出里氏替换原则的错误提示,用Any又太宽泛,加忽略注释又总觉得是在“偷懒”。别急,咱们来好好捋清楚这个问题,找到靠谱的解决方案。
首先得搞明白为什么mypy会报错:父类_RawIOBase的write方法参数类型是Buffer,这是一个范围更宽的协议类型——除了你指定的bytes、bytearray、memoryview之外,还支持array.array、mmap.mmap等符合缓冲区协议的对象。而你用的Union[bytes, bytearray, memoryview]是Buffer的子集,这就违反了里氏替换原则:当你把子类实例当作父类实例使用时,传入一个合法的Buffer对象但不在你的Union列表里,子类的write方法就可能无法正确处理,所以mypy的报错其实是合理的。
那怎么解决呢?这里有个既符合类型检查要求、又不用妥协的方案:
使用Buffer类型与父类保持一致
直接把write方法的参数类型改成Buffer,和父类的定义对齐,同时在方法内部用memoryview来统一处理缓冲区对象——这样既符合里氏替换原则,又能安全地获取长度并完成计数。
修改后的代码如下:
from io import RawIOBase # Python 3.10+ 直接用typing.Buffer;3.9及以下版本需要先安装typing_extensions,再从这里导入 from typing import Buffer class BytesWrittenCounterIO(RawIOBase): def __init__(self) -> None: self.written: int = 0 def write(self, b: Buffer) -> int: # 转换为memoryview,确保能安全获取长度并处理所有Buffer类型 buffer_view = memoryview(b) n = len(buffer_view) self.written += n return n
这样修改后,mypy的类型检查会完全通过,而且你的子类完全符合父类的契约——任何能传给RawIOBase.write的对象,都能正常传给子类的write方法,同时你也完美实现了字节计数的功能。
为什么这个方案可行?
Buffer是Python类型系统里用来表示“支持缓冲区操作的对象”的协议类型,它涵盖了你原本想支持的所有bytes-like类型,同时也兼容其他合法的缓冲区对象。用memoryview转换是因为所有符合Buffer协议的对象都能被转换成memoryview,这能保证你安全地获取缓冲区的长度,不会出现len()调用失败的情况。
如果你用的是Python 3.9或更早的版本,只需要先安装typing_extensions库,然后把导入语句改成from typing_extensions import Buffer就可以了。
内容来源于stack exchange

