我教编程这么多年,发现很多初学者一听到“鸭子类型”这四个字就开始犯晕。其实这个概念特别简单,今天我用一个真实的Python例子讲明白。
先说说这个怪名字的来历。有个作家说过一句话:“如果它看起来像鸭子,叫起来像鸭子,走路像鸭子,那它就是鸭子。”这句话被编程界拿来用,意思是:判断一个东西是什么,不看它是不是鸭子,看它有没有鸭子的行为。放在Python里就是:我不关心对象是什么类型,我只关心它能不能做我要的事情。
举个例子。假设你写了个函数,要打印一首诗。诗可以来自不同地方,比如从文件里读,从用户输入里拿,或者从网上抓。
没有学习鸭子类型的时候,你可能会这样写:
def print_poem(source): if isinstance(source, FileSource):
poem = source.read_from_file()
elif isinstance(source, UserInput):
poem = source.get_input()
elif isinstance(source, WebSource):
poem = source.download()
print(poem)
这段代码有个明显的问题:每增加一种新的诗来源,你就要改这个函数。而且你要记住每种来源调用哪个方法。这种写法很僵硬。
理解了鸭子类型后,你会这样写:
def print_poem(source): poem = source.get_poem()
print(poem)
你没看错,就这么短。所有能提供诗的对象,你只管让它们都有一个叫get_poem()的方法。函数不关心source是FileSource还是UserInput,它只关心source有没有get_poem这个方法。
让我们写几个具体的类看看:
class FileSource: def get_poem(self):
with open('poem.txt', 'r') as f:
return f.read()
class UserInput:
def get_poem(self):
return input('请写下你的诗:')
class WebSource:
def get_poem(self):
假装从网站获取
return '床前明月光'
现在你使用起来特别灵活:
print_poem(FileSource()) print_poem(UserInput())
print_poem(WebSource())
每个对象都有自己的get_poem方法,它们虽然类型不同,但在print_poem函数看来都一样。这就是鸭子的核心思想。
你可能遇到过这种情况:写代码的时候,明明某个对象有这个功能,但你要调的方法名不一样。比如文件对象的read(),网络请求的get()。如果是你自己定义类,最好的做法是把方法名统一起来。大家都叫同一个名字,处理起来就省事多了。
有人会说,这样做会不会太随意了。Python社区的看法不一样,他们觉得这反而是优势。写代码的时候更关注对象能做什么,而不是它是什么。这样代码更短更灵活。
我见过一个学生,他刚学这个知识点的时候特别困惑。他问:“我不用判断类型,万一传错了对象怎么办?”我让他写了一个测试。他写了一个假的FileSource,里面故意不写get_poem方法。运行后Python直接报错说找不到方法。他一下就明白了,错误会立刻暴露出来,而且错误信息很清楚。
真实项目里,经常用鸭子类型来处理长得不一样但功能类似的对象。比如日志系统:你可以把日志写到文件、写到数据库、写到控制台。只要它们都提供write_log(message)方法就行。处理用户数据也一样:用户可能来自数据库、来自缓存、来自外部API,只要它们都提供get_user(id)方法就行。
有个很重要的点要记住:别滥用鸭子类型。如果某些对象虽然长得像鸭子,但叫声不同,走路的姿势也不同,硬凑在一起反而会出问题。比如一个对象虽然有get_poem方法,但返回的是中文倒序的诗,那就不符合预期了。鸭子类型不是说名字一样就行,行为也得一致。
回头来看,鸭子类型真正的价值是让代码变轻了。你不用写那么多类型判断,不用搞复杂的继承体系。只要对象的行为一致,代码就能正常工作。这对快速开发特别友好。
写代码的人都知道,改代码是常有的事。鸭子类型让你改代码的时候更轻松。加一种新的诗来源,你只需要写一个新类,保证它有get_poem方法就行。原来调用print_poem的地方一行都不用改。这种感觉很舒服。
我教了这么多年,发现凡是理解了鸭子类型的人,写起代码来都慢慢有了自己的风格。代码更简洁,更贴近问题本身。这个道理说透了就这么简单:关心行为,别关心身份。这也是Python语言设计的哲学精髓之一。