10 KiB
sources, brief
| sources | brief | ||||||||
|---|---|---|---|---|---|---|---|---|---|
|
鸭子类型根据对象是否具备所需行为来使用对象,而非依赖其具体类型。 |
鸭子类型
定义
鸭子类型(Duck Typing)是一种编程思想:判断一个对象能否用于某个场景时,不主要看它的具体类型、类名或继承关系,而看它是否具备代码所需的行为、方法或协议。
经典说法是:
如果它看起来像鸭子、游泳像鸭子、叫声像鸭子,那么它大概就是鸭子。
在 Python 中,这意味着:如果一个对象能完成代码要求的操作,它就可以被使用,而不必显式属于某个特定类。
鸭子类型与 接口设计、多态、可迭代对象 和 库设计 密切相关。它强调“对象能做什么”,而不是“对象是什么类”。
核心思想:依赖行为,而不是依赖类型
鸭子类型体现了一种重要设计原则:
面向对象的行为编程,而不是面向对象的具体类型编程。
在 Python 中,这通常意味着:
- 不必过度检查类型;
- 优先使用对象支持的协议,例如迭代协议;
- 让函数接受更抽象的输入;
- 只在必要时添加防御性检查;
- 通过清晰文档说明接口期望。
例如,如果一段代码只需要对象支持迭代,那么它不必要求参数一定是 list、文件对象或某个自定义类。只要对象能被 for 循环迭代,它就满足需求。
文件读取中的例子
在 summaries/06_Design_discussion 中,鸭子类型通过 read_data() 的设计体现出来。
一种设计是让函数接收文件名:
def read_data(filename):
records = []
with open(filename) as f:
for line in f:
...
records.append(r)
return records
这种函数只能直接处理文件名,并且内部负责打开文件。
另一种设计是让函数接收“可迭代的行对象”:
def read_data(lines):
records = []
for line in lines:
...
records.append(r)
return records
这里 read_data() 并不关心 lines 是什么类型。它只关心一件事:lines 能不能被 for line in lines 迭代。
因此,只要对象表现得像“一组文本行”,它就可以被传入函数。这就是鸭子类型的核心。
为什么鸭子类型带来灵活性
采用鸭子类型后,函数可以处理多种输入来源:
# 普通 CSV 文件
lines = open('data.csv')
data = read_data(lines)
# gzip 压缩文件
lines = gzip.open('data.csv.gz', 'rt')
data = read_data(lines)
# 标准输入
lines = sys.stdin
data = read_data(lines)
# 字符串列表
lines = ['ACME,50,91.1', 'IBM,75,123.45']
data = read_data(lines)
这些对象的具体类型不同,但它们都有共同点:都可以逐行迭代。
这使函数依赖于更抽象的行为协议,而不是依赖某种具体实现。这与 可迭代对象、接口设计 和 库设计 密切相关。
继承、多态与鸭子类型
在 summaries/02_Inheritance 中,继承被用于构建可扩展的表格输出系统。文档定义了一个 TableFormatter 基类:
class TableFormatter:
def headings(self, headers):
raise NotImplementedError()
def row(self, rowdata):
raise NotImplementedError()
然后通过继承实现不同格式:
class TextTableFormatter(TableFormatter):
def headings(self, headers):
...
def row(self, rowdata):
...
class CSVTableFormatter(TableFormatter):
def headings(self, headers):
...
def row(self, rowdata):
...
print_report() 函数只调用两个方法:
def print_report(reportdata, formatter):
formatter.headings(['Name','Shares','Price','Change'])
for rowdata in reportdata:
formatter.row(rowdata)
从继承角度看,TextTableFormatter、CSVTableFormatter、HTMLTableFormatter 都是 TableFormatter 的子类,因此它们可以被当作格式化器使用。这是典型的 多态:同一段代码可以处理不同的具体对象。
但从鸭子类型角度看,print_report() 实际上并不一定需要对象真的是 TableFormatter 的子类。它真正需要的是:
- 对象有
headings(headers)方法; - 对象有
row(rowdata)方法; - 这两个方法按照约定完成输出。
也就是说,只要某个对象“表现得像一个表格格式化器”,它就可以被传给 print_report()。这正是鸭子类型的思想。
鸭子类型与显式继承的关系
鸭子类型并不排斥继承。二者关注点不同:
- 继承 通过类层次表达“是什么”,例如
CSVTableFormatter是一种TableFormatter。 - 鸭子类型通过方法和行为表达“能做什么”,例如对象能不能执行
headings()和row()。
在 Python 中,经常会同时使用二者:
- 用基类定义接口或设计规范;
- 用子类实现不同版本;
- 调用代码只依赖对象提供的方法,而不过度检查具体类型。
例如,TableFormatter 可以作为一种抽象基类,说明格式化器应该提供哪些方法。但 print_report() 最好只关心传入对象是否支持这些方法,而不是强制检查:
isinstance(formatter, TableFormatter)
这种设计让代码既有清晰接口,又保持 Python 式的灵活性。
对库设计的意义
在库函数设计中,鸭子类型通常鼓励更通用的接口。
例如,parse_csv() 如果只接收文件名,那么它只能直接解析磁盘上的普通文件:
portfolio = fileparse.parse_csv('Data/portfolio.csv', types=[str, int, float])
但如果它接收一个可迭代对象,就可以解析:
- 普通文件对象;
- gzip 打开的压缩文件对象;
- 标准输入
sys.stdin; - 测试用的字符串列表;
- 任何自定义的逐行数据源。
类似地,print_report() 如果只知道如何输出纯文本,就很难扩展。但如果它只依赖一个“格式化器接口”,那么调用者可以传入文本、CSV、HTML 或其他自定义格式化器。
这种设计让核心逻辑更独立,也更容易测试和复用。它体现了 松耦合 和 可扩展设计:业务代码不需要知道对象内部如何实现,只需要知道对象支持哪些行为。
拥有自己的抽象
summaries/02_Inheritance 中还强调了“拥有自己的抽象”。即使已有第三方表格格式化库,应用程序也可以先定义自己的 TableFormatter 接口,然后在具体实现内部选择:
- 使用自定义输出代码;
- 调用某个第三方库;
- 将来替换为另一个第三方库。
只要外部保持 headings() 和 row() 这样的行为接口不变,应用代码就不会受到内部实现变化的影响。
这与鸭子类型高度一致:应用代码依赖的是对象表现出的行为,而不是对象背后的具体类、库或实现细节。
需要注意的风险
鸭子类型带来灵活性的同时,也可能引入意外行为。
在 summaries/06_Design_discussion 中,一个重要陷阱是:字符串本身也是可迭代对象。
如果 parse_csv() 被修改为接收可迭代对象,而用户仍然传入文件名字符串:
port = fileparse.parse_csv('Data/portfolio.csv', types=[str, int, float])
函数可能不会打开这个文件,而是把 'Data/portfolio.csv' 当作字符序列来迭代。也就是说,它会逐字符处理文件名,从而产生错误或荒谬的结果。
类似地,如果某个对象碰巧有 headings() 和 row() 方法,但语义不符合 TableFormatter 的约定,也可能导致错误或混乱。因此,鸭子类型依赖的不只是“方法名存在”,还包括“行为符合预期”。
因此,在使用鸭子类型时,常常需要适当的安全措施:
- 在文档中明确函数期望的协议或接口;
- 对常见错误输入抛出清晰异常;
- 在边界层处理路径字符串、文件打开等特殊情况;
- 对核心函数保持简洁,让它只处理抽象输入;
- 在必要时使用抽象基类或测试来约束对象行为。
与其他概念的关系
- 可迭代对象:鸭子类型常通过可迭代协议体现,只要对象能被迭代,就可作为输入使用。
- 接口设计:鸭子类型鼓励接口依赖行为,而不是依赖具体类。
- 库设计:库函数通常应拥抱灵活性,让调用者可以提供不同来源的数据或不同实现对象。
- 文件处理:文件对象、压缩文件对象和标准输入都可以表现为“可迭代行对象”。
- CSV解析:CSV 解析函数可以只关心输入是否提供逐行文本,而不关心文本来自哪里。
- 继承:继承可以显式组织对象层次,但鸭子类型更关注对象是否提供所需方法。
- 多态:鸭子类型是 Python 中实现多态体验的重要方式;不同对象只要支持同一行为,就能被同一段代码使用。
- 可扩展设计:通过依赖抽象行为而非具体实现,程序更容易添加新格式、新数据源或新策略。
- 松耦合:调用者与被调用对象之间只通过小而稳定的行为接口协作。
核心收获
鸭子类型让代码更灵活、更可复用。对于 read_data() 或 parse_csv() 这样的函数来说,真正需要的不是“文件名”,而是“可以逐行迭代的对象”。对于 print_report() 来说,真正需要的也不是某个特定类的实例,而是“提供 headings() 和 row() 方法的格式化器对象”。
继承可以帮助定义和组织这些接口,但 Python 代码通常更看重对象是否具备所需行为。只要输入对象符合这种行为要求,它就可以被使用。
不过,灵活性也需要配合清晰的接口说明和必要的安全检查,尤其要注意字符串这类“看似不是行集合但实际上可迭代”的对象,以及那些方法名相同但语义不符合约定的对象。
See also: summaries/00_Overview
See also: summaries/03_Special_methods
See also: summaries/01_Dicts_revisited
See also: summaries/01_Iteration_protocol
See also: summaries/01_Testing
See also: summaries/07_Objects