先看一段很常见的"防御性"代码:
python
def greet_all(names):
if not isinstance(names, list): # 先确认它是 list
raise TypeError("必须是列表")
for n in names:
print("你好,", n)
很多从静态语言转过来的人,第一反应就是先检查类型:"传进来的必须是 list,不然不放心。"
可 Python 老手看到这段会皱眉:你凭什么非得让它是个 list? 只要它能被 for 遍历,tuple、set、甚至一个自定义对象,凭什么不能传?
这就是 Python 里最反直觉、也最舒服的一条哲学------鸭子类型(Duck Typing)。
"鸭子类型"这个名字,本身就是个比喻
它来自一句俏皮话:
如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。
翻译成代码语言就是:别关心对象"是什么",只关心它"能做什么" 。一个对象只要能调用 quack(),那不管它是真鸭子、橡皮鸭子还是机器人鸭子,对我来说就是鸭子。
静态语言的"证件" vs 动态语言的"本事"
在 Java 这类静态语言里,一个方法要收什么,得写在脸上:
java
void makeDuckSound(Duck duck) { // 参数类型锁死:必须是 Duck
duck.quack();
}
你想传个"会叫的狗"进去?对不起,它没继承 Duck,编译都过不了。想让它被接受,得先让它实现某个接口或继承某个类------相当于先办张"鸭子证"。
Python 反过来了:
python
def make_duck_sound(anything):
anything.quack() # 不查户口,直接看它会不会 quack
传进来的是啥我不管,你只要会 quack(),我就敢用你 。一个类只要定义了 quack 方法,它就是"鸭子",哪怕它名字叫 Dog。
好处坏处,都藏在这份"不设防"里
好处是灵活得让人上瘾 。写一个通用函数,不用为每一种类型写一份重载,也不用搞一堆抽象基类。很多 Python 的"魔法"都建立在鸭子类型上------比如 for 循环不关心你是什么,只要你实现了 __iter__;len() 不关心你是什么,只要你实现了 __len__;with 不关心你是什么,只要你实现了 __enter__ 和 __exit__(写了三十行 try...finally,Python 一个 with 就替你兜底了------别再用完才想起关文件)。
坏处是"不报错"不等于"对" 。静态语言在编译期就把"类型不对"拦下来了;鸭子类型则把检查推迟到了运行期------你传错对象,只有跑到那一行、调用那个不存在的方法时,才 AttributeError 崩掉。
所以 Python 社区演化出了两句心法:
- LBYL(Look Before You Leap) :跳之前先看一眼------
if isinstance(...),就是开头那种"防御式"; - EAFP(Easier to Ask Forgiveness than Permission) :与其请求许可,不如事后道歉------先直接干,出错了再
except兜着。
Python 官方偏爱的,是后者。看这段:
python
def get_user_agent(request):
try:
return request.user_agent # 直接访问,赌它有这个属性
except AttributeError:
return "unknown"
不检查"你是不是有 user_agent",直接取,取不到就默认。这就是鸭子类型的日常。
一份自由,配一份责任
鸭子类型给的是自由 :你可以不办任何"证件",就写出极其通用、极其简洁的代码。但它同时也把责任悄悄推给了你:因为编译器不帮你把关了,你得自己心里清楚"我期望这个对象会什么",并把边界(或异常)处理干净。
说到底,鸭子类型信的是这么一句话:能力比出身重要,行为比标签重要。
一个对象配不配得上某个角色,不该看它"登记"成了什么,而该看它"实际"能做什么。写代码是这个理,看人,不也常常是这个理么。
想系统掌握 Python 这些动态特性背后的机制,推荐 B站【408实验室】的《Python 完全自学教程》。