
我记得我学 Python 多态那会儿,教材上来就是"继承"、"运行时绑定"。概念堆概念,看完一章,脑子里依然一团浆糊。
后来在实际项目里踩过几次坑,才搞明白——Python 的多态压根不需要这些花架子。核心逻辑很简单:一个对象能不能用,取决于它有没有你要调的那个方法。 类型是什么、有没有继承关系、有没有实现某个ABC,Python 根本不在意。
这种风格有个名字,叫鸭子类型(Duck Typing)。名字来自一句俚语:
"If it walks like a duck and quacks like a duck, it must be a duck."
翻译成代码就是:有 pay 方法,就是能付款的。不管这对象是 Alipay 还是 WechatPay 还是 BankCard。
def process_payment(p, amount):
p.pay(amount)
三行代码。没有继承关系声明,没有类型约束。p 是什么类型不重要,有 pay 方法就能跑。
Java 程序员看到这里可能心里就发毛了——这代码万一传了个没 pay 方法的对象进来怎么办?
确实会炸:
class Cash:
def give_money(self, amount):
print(f"现金支付了 {amount} 元")
process_payment(Cash(), 100)
# AttributeError: 'Cash' object has no attribute 'pay'
Cash 类的方法名是 give_money,不是 pay。运行到 p.pay(amount) 这一行才报错。小项目还好排查,几百行的代码量堆起来,错误出现在深层调用链里的时候,排查起来的确会挺折腾人。
这其实是鸭子类型的代价——把类型检查推迟到运行时。
实际开发中,可以采取下面几种方法来兜底。
第一种,用 hasattr 做防御性检查。 多花两行代码,报错位置从深层调用链提前到函数入口:
def process_payment(p, amount):
if not hasattr(p, 'pay'):
raise TypeError(f"对象必须支持 pay 方法,当前类型: {type(p).__name__}")
p.pay(amount)
其实相比用 hasattr 做防御性检查,用try...except异常处理更直观、易于理解。
def process_payment(p,amount):
try:
p.pay(amount)
except AttributeError:
...
无需管对象 p 的类型是什么,其中有没有 pay() 方法,先干了再说,这其实更Pythonic。Python 的 EAFP 风格——“请求原谅比请求许可要容易得多。”
第二种,文档里说清楚参数协议。 这不是给 Python 看的,是给接下来接手你代码的人看的:
def process_payment(p, amount):
"""
处理支付操作。
参数 p 需要支持 pay(amount) 方法。
"""
p.pay(amount)
第三种,用 Protocol 做静态类型检查。 Python 3.8 引入的这个东西,本质上是个"不强制执行"的接口——运行时不检查,但一般 IDE 能帮你提前发现类型不匹配的问题:
from typing import Protocol
class Payable(Protocol):
def pay(self, amount: float) -> None: ...
def process_payment(p: Payable, amount: float) -> None:
p.pay(amount)
对大项目来说,Protocol 的收益很实在。写代码的时候 IDE 能提示"这个对象没有 pay 方法",可以在运行时报错前给出提示。
继续来点更实际的。鸭子类型不只是躺在教程里的一个概念,其在 Python 标准库里的实际应用随处可见。
最常见的例子是迭代协议。for 循环能遍历列表、字典、字符串、文件对象——这些类型之间没有任何继承关系,共同点是有 __iter__ 方法。想让自定义对象能放进 for 循环,实现 __iter__ 就行,不需要继承任何基类:
class Range:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
current = self.start
while current < self.end:
yield current
current += 1
for i in Range(0, 5):
print(i) # 0, 1, 2, 3, 4
with 语句也是这个思路。要求对象实现 __enter__ 和 __exit__,文件、数据库连接、锁——全都能用,不需要它们有继承关系。
鸭子类型最大的价值,在实际业务的扩展场景里更能体现得出来。(其实就是多态)
做电商系统的时候,支付方式最初只支持支付宝,随后慢慢扩展支付方式加上了微信、银联、加密货币等等。不用多态的话,订单处理代码会越来越长:
if payment_type == 'alipay':
Alipay().pay(amount)
elif payment_type == 'wechat':
WechatPay().pay(amount)
elif payment_type == 'unionpay':
BankCard().pay(amount)
elif payment_type == 'bitcoin':
Bitcoin().pay(amount)
每加一种支付方式就要改这段代码,加个判断分支。改的人不一定是写这段代码的人,改完还要回归测试,挺容易出 bug 的。
用多态的思路,新增支付方式只需要加一个支持 pay 方法的类:
def pay_order(payment_method, amount):
payment_method.pay(amount)
调用代码不用动。新增功能不触碰已有逻辑,测试也只针对新加支付方式测试即可——这就是开闭原则,也是多态在真实项目里的重要意义。
总结
Python 的多态不需要继承关系,靠鸭子类型就够了。它把类型检查推迟到运行时,换来的是更少的样板代码和更灵活的扩展方式。大项目里配合 Protocol 和静态类型检查工具,灵活性和安全性完全可以兼得。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14276/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料