一句话:只要一个对象“像鸭子一样叫、像鸭子一样走”,就把它当成鸭子用——关注行为(拥有的方法/属性),不关心它的具体类型或继承关系。
⸻
核心要点
• 结构/行为决定可用性:函数只依赖“需要的方法名/属性名”,而非类名。
• 多态更松弛:比“必须继承某接口/基类”的名义类型(nominal typing)更灵活。
• 常配套 EAFP(Easier to Ask Forgiveness than Permission):先用,出错再处理。
⸻
最小示例:只要能 .write() 就能当“可写对象”
def dump_text(text, sink):
# 我只需要 sink 有 write(str) 方法,至于它是文件、socket 还是内存缓冲都无所谓
sink.write(text)
# 用真实文件
with open("out.txt", "w", encoding="utf-8") as f:
dump_text("hello\n", f)
# 用内存对象
from io import StringIO
buf = StringIO()
dump_text("world\n", buf)
⸻
EAFP vs LBYL
EAFP(推荐):不提前查类型,直接调用,异常兜底。
def send_greeting(ch):
try:
ch.send("hi") # 只要有 send 方法就行
except AttributeError as e:
raise TypeError("需要带有 send(str) 方法的对象") from e
LBYL(Look Before You Leap):
def send_greeting(ch):
if hasattr(ch, "send"):
ch.send("hi")
else:
raise TypeError("需要 send 方法")
在并发/动态场景下,EAFP 更稳(避免检查与调用之间对象被改变的竞态)。
⸻
与静态类型配合:typing.Protocol(结构化类型)
鸭子类型很自由,但缺少编译期校验。可用 Protocol 保留“按行为编程”的同时让 mypy/pyright 做静态检查。
from typing import Protocol
class Writer(Protocol):
def write(self, s: str) -> int: ...
def dump_text(text: str, sink: Writer) -> None:
sink.write(text)
# 任何实现了 .write(str)->int 的类型,都被视为 Writer(无需继承)
⸻
典型应用
• “文件样”对象:.read()/.write()/.flush()
• 可迭代对象:实现 iter 或 len+getitem
• 路径样对象:实现 fspath(许多标准库 API 都接受)
• 你自己的“模型样对象”:只要有 .predict()/.fit() 就能参与流水线
def evaluate(model, X):
# 只要求有 predict 方法
y = model.predict(X)
...
⸻
优缺点速览
优点:低耦合、可测试性强、易替身(mock)、更符合“面向接口而非实现”。
风险:错拼方法名/行为不符合时,错误在运行期才暴露;为此可用:
• 清晰文档/约定(说明需要哪些方法)
• Protocol 做静态检查
• 运行期用 EAFP 给出友好错误信息
⸻
一句收尾:在 Python 里,鸭子类型让你“按能力而非血统编程”;配上 Protocol 与 EAFP,就既灵活又靠谱。
6050

被折叠的 条评论
为什么被折叠?



