鸭子类型与继承解决不同问题:继承定义“是什么”,鸭子类型关注“能做什么”;滥用继承导致耦合,鸭子类型通过运行时方法检查实现松耦合与灵活扩展。

鸭子类型不是“比继承更灵活”的替代方案,而是Python里解决不同问题的两种思路——继承回答“它是什么”,鸭子类型回答“它能做什么”。真正在项目里写崩的,往往不是用了鸭子类型,而是把继承用在了本该用协议的地方。
继承容易卡在“is-a”关系上,而现实业务根本不管这个
比如你写了个DataProcessor类,后来要支持从数据库、API、本地CSV三种来源读数据。如果硬拉一条继承链:DBProcessor(DataProcessor)、APIProcessor(DataProcessor)……很快就会发现:它们除了都叫“Processor”,几乎没共享逻辑;但为了塞进继承树,你得往基类里塞一堆if isinstance(self, DBProcessor)分支,或者搞出一堆空方法让子类选择性覆写。
更麻烦的是,一旦新加一个S3Processor,就得改继承结构、测所有子类——这不是复用,是耦合。
- 继承适合建模稳定、有明确层级的领域概念,比如
Dog(Animal)、AdminUser(User) - 只要出现“为了复用方法而继承”“基类开始加
abstractmethod但只有一两个子类实现”,就该警觉 - Python里没有编译期检查,子类漏实现某个方法,可能到运行时才报
AttributeError
鸭子类型天然适配Python的运行时行为检查
Python不靠类声明来判断能否调用.read(),它只看对象有没有这个方法。这意味着你可以混用标准库对象、第三方库对象、甚至字典或自定义类,只要它们有.read()和.close(),就能传给json.load()或你自己写的parse_config()函数。
这种松耦合带来的直接好处是:测试更容易(mock一个带.fetch()方法的字典就行)、扩展更轻量(加个新数据源不用动任何已有类)、第三方集成成本更低(比如用requests.Response直接代替自己写的HTTPResponse类)。
- 常见错误:试图用
isinstance(obj, SomeBaseClass)去判断能力,结果漏掉 duck-typed 对象 - 正确做法是用
hasattr(obj, "read")或直接try/except AttributeError捕获 - 别迷信
typing.Protocol——它只是给IDE和mypy加提示,运行时照样走鸭子类型逻辑
协议(Protocol)是鸭子类型的工程化收口,不是继承的升级版
typing.Protocol本质是把隐式约定显式写出来,比如定义一个Reader协议要求必须有.read()和.close()。但它不强制你继承,也不生成任何运行时类型信息,只是告诉人“我期望你长这样”,同时让mypy能提前报错。
注意:协议不能替代继承,也不能解决“多个类需要共享实现代码”的问题。如果你发现几个类重复写同一段retry_logic,该用组合或函数提取,而不是硬造一个Retryable基类再让它们继承。
- 协议适合接口稳定、被多处消费的场景,比如你写的SDK暴露给外部调用者
- 协议字段名、方法签名必须完全一致,连参数名不同(如
def read(self, size)vsdef read(self, n))都会被mypy标为不兼容 - 协议本身不能实例化,也不能加
__init__,它纯粹是“形状契约”
真正容易被忽略的点是:鸭子类型不等于放任不管。你需要在文档里写清“期望对象支持哪些方法”,在关键路径做hasattr或getattr防御,必要时用Protocol配合类型检查——否则所谓“灵活”,最后只会变成“谁也不知道这个函数到底接受什么”。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











