前言
最近在读Python相关书籍,看到一个词叫"鸭子类型",觉得很有意思,啥是鸭子类型呢?
鸭子类型
"鸭子类型"(Duck Typing)是动态类型语言中的一个概念,它的名字来源于英文中的一句话:"If it walks like a duck and quacks like a duck, then it is a duck."(如果它像鸭子一样走路,像鸭子一样叫,那么它就是一只鸭子。)
在程序设计中,鸭子类型的含义是:关注对象的行为,而不是对象所属的类型。也就是说,如果一个对象拥有某个方法,我们不关心它是什么类型,只关心它能做什么。
案例:统计文件中元音字母(aeiou)的数量
python
def count_vowels(fp):
"""统计某个文件中,包含元音字母(aeiou)的数量"""
if not hasattr(fp, 'read'):
raise TypeError('must provide a valid file object')
VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}
count = 0
for line in fp:
for char in line:
if char.lower() in VOWELS_LETTERS:
count += 1
return count
然后,我们通常会这么调用
python
with open('demo.txt', 'r') as fp:
print(count_vowels(fp))
核心代码中,我们只判断了fp对象有没有read方法来确定是否执行。在纯粹的鸭子类型编程风格下,不应该出现任何的isinstance类型判断语句。
鸭子类型只关注对象的行为,对类型不做强制要求,大大提高了代码的灵活性。上面我们也可以这样调用
python
from io import StringIO
print(count_vowels(StringIO('Hello, world!')))
StringIO
是Python的io
模块下的一个类,它的功能是在内存中读写str。StringIO
提供了一个文件类的接口来操作文本数据。
这里,StringIO('Hello, world!')
就像在内存中打开了一个文件,该文件的内容是'Hello, world!'
,然后你可以使用StringIO
对象的各种方法,如read()
, write()
, seek()
等,来操作这个"文件"。
当然,我们也可以自己实现一个类型,实现read方法
python
class StringList:
"""用于保存多个字符串的数据类,实现了 read() 和可迭代接口"""
def __init__(self, strings):
self.strings = strings
def read(self):
return ''.join(self.strings)
def __iter__(self):
for s in self.strings:
yield s
print(count_vowels(StringList('Hello, world!')))
看看,就是因为count_vowels()函数属于鸭子类型编程风格,StringList类型实现了read接口。
当然这些都是鸭子类型带来的好处
局限
- 类型不明确,可能导致运行时错误: 在静态类型语言中,如果试图调用一个对象不存在的方法,会在编译时就抛出错误。但在使用鸭子类型的动态语言中,如果调用的方法不存在,错误只会在运行时出现。这可能导致在开发和调试阶段难以发现问题。
- 代码可读性降低: 鸭子类型不关注对象的本质,只关注对象的行为,这可能会使得代码的可读性降低。其他开发者在阅读代码时,可能会对一个函数允许接收什么样的参数,或者一个对象会有哪些行为存在所困扰。
- 缺乏有效的文档工具: 由于不明确注明输入和输出的类型,这使得用工具自动生成文档变得困难。因为工具无法通过分析代码来推断出函数或方法可能接受哪些类型的参数。
- 无法利用一些语言特性: 一些语言特性,例如Python的类型提示,可以帮助开发者在写代码时发现潜在的错误,但使用鸭子类型会使这些特性的作用大打折扣。
- 可能会破坏封装: 有些行为或属性应该是对象私有的,不应该被外界访问。但如果只注重行为,不注重对象的类型,可能会导致调用方访问了不应该访问的行为或属性,从而破坏对象的封装。
最后
在选择是否使用鸭子类型时,需要根据具体的情况和需求来权衡利弊。在简单、灵活的情况下,鸭子类型可以是一种有效的编程方式;但在需要明确接口定义、类型安全性较高的情况下,可能需要考虑其他类型系统或设计模式。