Python协议和鸭子类型

什么是协议?

协议是非正式的接口, 是让Python这种动态类型语言实现多态的方式.

在C++中可以通过定义抽象基类, 在Java/Go中可以通过使用关键字interface, 在语言层面强制实现接口. 而在Python中, 协议是非正式的接口, 是一组方法, 但只是一种文档, 语言层面不强制实现.

虽然协议是非正式的, 在Python中, 应该把协议当成正式的接口.

Python中存在多种协议, 用于实现鸭子类型(对象的类型无关紧要, 只要实现了特定的协议 — 一组方法, 即可).

需要称为相对应的鸭子类型, 那就实现相关的协议. 例如, 实现序列协议(__len____getitem__), 这个类就表现的像序列.

协议是非正式的, 没有强制力, 可以根据具体场景实现一个具体协议的一部分. 例如, 为了支持迭代, 只需要实现__getitem__, 不需要实现__len__.

在Python文档中, 如果看到”文件类对象”(表现得像文件的对象), 通常说的就是协议, 这个对象就是鸭子类型. 这是一种简短的说法, 意思是: “行为基本与文件一致, 实现了部分文件接口, 满足上下文需求的东西.”

鸭子类型(Duck Typing)

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck. - James Whitcomb Riley

只要走起来像鸭子, 游起来像鸭子, 加起来像鸭子, 那它就是鸭子(它的行为是鸭子的行为, 那么可以认为它就是鸭子.)

鸭子类型不关注对象的类型, 而关注对象的行为(方法). 只要一个对象实现了相应的方法, 那么它就是鸭子类型, 就是多态的一种形式.

注解

协议, 定义了非正式的接口. 一个对象, 只要它实现了相应的协议, 那它就是相应的鸭子类型, 就是相应的多态的一种形式. 所以说, 协议是让Python实现多态的方式.

在不使用鸭子类型的语言中, 我们编写一个函数, 它接收一个类型鸭子的对象, 并调用它的的方法. 在使用鸭子类型的语言中, 这样的一个函数可以接收一个任意类型的对象, 并调用它的方法. 如果这些需要被调用的方法不能存在, 那么将引发一个运行时错误.

在实现多态时, 如果不使用鸭子类型, 则使用的对象要继承自某个类或接口, 即该对象要属于某一种特定的类型, 从而保证了该对象具有特定的方法. 如果使用鸭子类型, 则不需要继承自特定的类或接口, 即不需要对象属于某一个特定的类型, 只要其实现了特定的方法即可.

关于鸭子类型的批评

关于鸭子类型常常被引用的一个批评是它要求程序员在任何时候都必须很好地理解他/她正在编写的代码.

本质上, 问题是: “如果它走起来像鸭子并且叫起来像鸭子”, 它也可以是一只正在模仿鸭子的龙.

因为鸭子类型只关心行为, 不关心类型, 一个对象只要具有相应的行为, 就视为相应的鸭子类型. 但有时候, 可能一个对象可能具有某种行为, 但却不是我们想要的. 例如, 在Python中, 你可以创建一个名为Wine的类, 并在其中实现需要的press方法. 然而, 一个称为Trousers的类可能也实现了press方法. 但是Trousers类的press方法中的具体实现并不是我们想要的, 因此这就要求我们了解每一个”press”方法.

鸭子类型的提倡者认为这个问题通过在测试和维护代码库前拥有足够的了解来解决.

举例说明

鸭子类型在Python中被广泛使用.

在Python中, 鸭子类型的典型例子就是类似file的类, 这些类可以实现file的一些或全部方法, 并可以用于file通常使用的地方. 例如, GzipFile实现了一个用于访问gzip压缩数据的类似file的对象. cStringIO允许把一个Python字符串视作一个文件. 套接字(socket)也和文件共同拥有许多相同的方法, 然而套接字缺少tell()方法, 不能用于GzipFile可以使用的地方. 这体现了鸭子类型的可伸缩性: 一个类似file的对象可以实现它有能力实现的方法, 且只能用于它有意义的情形下(只实现file的部分方法).

检查一个自称为Duck的对象是否拥有一个quack()方法, 可以使用内置函数hasattr()来进行检查, 人们通常更倾向于使用异常来处理:

try:
    mallard.quack()
except(AttributeError, TypeError):
    print('mallard没有quack()方法')

这种写法的优势在于它鼓励结构化处理其它来自类的异常, 在这里, “鸭子类型”产生的异常可以在它自己的子句中捕获, 与操作系统, I/O等其它可能的错误分别处理.