python白鹅类型_fluent python 11.4节 Alex Martelli的水禽

介绍完 Python 常规的协议风格接口后,下面讨论抽象基类。不过在分析 示例和细节之前,我们要看 Alex Martelli 写的一篇短文。这篇短文说明 了 Python 为什么引入抽象基类。

水禽和抽象基类

维基百科(http://en.wikipedia.org/wiki/Duck_typing#History)说是我 协助传播了“鸭子类型”这种言简意赅的说法(即忽略对象的真正类型,转而关注对象有没有实现所需的方法、签名和语义)。

对 Python 来说,这基本上是指避免使用 isinstance 检查对象的类型(更别提 type(foo) is bar 这种更糟的检查方式了,这样做 没有任何好处,甚至禁止最简单的继承方式)。

总的来说,鸭子类型在很多情况下十分有用;但是在其他情况下, 随着发展,通常有更好的方式。事情是这样的……

近代,属和种(包括但不限于水禽所属的鸭科)基本上是根据表型系统学(phenetics)分类的。表征学关注的是形态和举止的相似性……主要是表型系统学特征。因此使用“鸭子类型”比喻是贴切的。

然而,平行进化往往会导致不相关的种产生相似的特征,形态和举止方面都是如此,但是生态位的相似性是偶然的,不同的种仍属不同的生态位。编程语言中也有这种“偶然的相似性”,比如说下述经典的面向对象编程示例:

class Artist:

def draw(self): ...

class Gunslinger:

def draw(self): ...

class Lottery:

def draw(self): ...

显然,只因为 x 和 y 两个对象刚好都有一个名为 draw 的方法,而且调用时不用传入参数,即 x.draw() 和 y.draw(),远远不能确 保二者可以相互调用,或者具有相同的抽象。也就是说,从这样的 调用中不能推导出语义相似性。相反,我们需要一位渊博的程序员主动把这种等价维持在一定层次上。

生物(和其他学科)遇到的这个问题,迫切需要(从很多方面来 说,是催生)表征学之外的分类方式解决,即支序系统学 (cladistics)。这种分类学主要根据从共同祖先那里继承的特征分类,而不是单独进化的特征。(近些年,DNA 测序变得便宜又快,这使支序学的实用地位变得更高。)

例如,草雁(以前认为与其他鹅类比较相似)和麻鸭(以前认为与 其他鸭类比较相似)现在被分到 Tadornidae 亚科(表明二者的相似性比鸭科中其他动物高,因为它们的共同祖先比较接近)。此外, DNA 分析表明,白翅木鸭与美洲家鸭(属于麻鸭)不是很像,至 少没有形态和举止看起来那么像,因此把木鸭单独分成了一属,完全不在 Tadornidae 亚科中。

知道这些有什么用呢?视情况而定!比如,逮到一只水禽后,决定 如何烹制才最美味时,显著的特征(不是全部,例如一身羽毛并不 重要)主要是口感和风味(过时的表征学),这比支序学重要得 多。但在其他方面,如对不同病原体的抗性(圈养水禽还是放 养),DNA 接近性的作用就大多了……

因此,参照水禽的分类学演化,我建议在鸭子类型的基础上增加 白鹅类型(goose typing)。

白鹅类型指,只要 cls 是抽象基类,即 cls 的元类是 abc.ABCMeta,就可以使用 isinstance(obj, cls)。

collections.abc 中有很多有用的抽象类(Python 标准库的 numbers 模块中还有一些)。

Python 的抽象基类还有一个重要的实用优势:可以使用 register 类方法在终端用户的代码中把某个类“声明”为一个抽象基类的“虚 拟”子类(为此,被注册的类必须满足抽象基类对方法名称和签名 的要求,最重要的是要满足底层语义契约;但是,开发那个类时不用了解抽象基类,更不用继承抽象基类)。这大大地打破了严格的强耦合,与面向对象编程人员掌握的知识有很大出入,因此使用继承时要小心。其实上面这一堆话我都没看懂。。

有时,为了让抽象基类识别子类,甚至不用注册。

其实,抽象基类的本质就是几个特殊方法。例如:

>>> class Struggle:

... def __len__(self): return 23

...

>>> from collections import abc

>>> isinstance(Struggle(), abc.Sized)

True

可以看出,无需注册,abc.Sized 也能把 Struggle 识别为自己的子类,只要实现了特殊方法 __len__ 即可(要使用正确的句法和 语义实现,前者要求没有参数,后者要求返回一个非负整数,指明 对象的长度;如果不使用规定的句法和语义实现特殊方法,如 __len__,会导致非常严重的问题)。

最后我想说的是:如果实现的类体现了numbers、collections.abc 或其他框架中抽象基类的概念,要么继承相应的抽象基类(必要时),要么把类注册到相应的抽象基类中。开始开发程序时,不要使用提供注册功能的库或框架,要自己动手注册;如果必须检查参数的类型(这是最常见的),例如检查是不是“序列”,那就这样做:

isinstance(the_arg, collections.abc.Sequence)

此外,不要在生产代码中定义抽象基类(或元类)……如果你很想 这样做,我打赌可能是因为你想“找茬”,刚拿到新工具的人都有大 干一场的冲动。如果你能避开这些深奥的概念,你(以及未来的代 码维护者)的生活将更愉快,因为代码会变得简洁明了。再会!

除了提出“白鹅类型”之外,Alex 还指出,继承抽象基类很简单,只需要 实现所需的方法,这样也能明确表明开发者的意图。这一意图还能通过 注册虚拟子类来实现。

此外,使用 isinstance 和 issubclass 测试抽象基类更为人接受。过 去,这两个函数用来测试鸭子类型,但用于抽象基类会更灵活。毕竟, 如果某个组件没有继承抽象基类,事后还可以注册,让显式类型检查通过。

然而,即便是抽象基类,也不能滥用 isinstance 检查,用得多了可能 导致代码异味,即表明面向对象设计得不好。在一连串 if/elif/elif 中使用 isinstance 做检查,然后根据对象的类型执行不同的操作,通 常是不好的做法;此时应该使用多态,即采用一定的方式定义类,让解 释器把调用分派给正确的方法,而不使用 if/elif/elif 块硬编码分派逻辑。

具体使用时,上述建议有一个常见的例外:有些 Python API 接受一个字符串或字符串序列;如果只有一个字符串,可以把它放 到列表中,从而简化处理。因为字符串是序列类型,所以为了把它 和其他不可变序列区分开,最简单的方式是使用 isinstance(x, str) 检查。可惜,在 Python 3.4 中没有能把字符串和元组或其他不可变序列区分开的抽象基类,因此必 须测试 str。在 Python 2 中,basestr 类型可以协助这样的测试。basestr 不是抽象基类,但 它是 str 和 unicode 的超类;然而,Python 3 把 basestr 去掉了。奇怪的是,Python 3 中有 个 collections.abc.ByteString 类型,但是它只能检测 bytes 和 bytearray 类型。

另一方面,如果必须强制执行 API 契约,通常可以使用 isinstance 检 查抽象基类。“老兄,如果你想调用我,必须实现这个”,正如本书技术 审校 Lennart Regebro 所说的。这对采用插入式架构的系统来说特别有 用。在框架之外,鸭子类型通常比类型检查更简单,也更灵活。

例如,本书有几个示例要使用序列,把它当成列表处理。我没有检查参 数的类型是不是 list,而是直接接受参数,立即使用它构建一个列 表。这样,我就可以接受任何可迭代对象;如果参数不是可迭代对象, 调用立即失败,并且提供非常清晰的错误消息。本章后面示例 11-13 中 的代码就是这么做的。当然,如果序列太长或者需要就地修改序列而导 致无法复制参数,就不能采用这种方式;此时,使用 isinstance(x, abc.MutableSequence) 更好。如果可以接受任何可迭代对象,也可 以调用 iter(x) 函数获得一个迭代器,详情参见 14.1.1 节。

模仿

collections.namedtuple 处理 field_names 参数的方式也是一例:field_names 的值可以是单 个字符串,以空格或逗号分隔标识符,也可以是一个标识符序列。此时可能想使用 isinstance,但我会使用鸭子类型,如下例所示。

使用鸭子类型处理单个字符串或由字符串组成的可迭代 对象

try:

field_names = field_names.replace(',', ' ').split()

# 抱歉,field_names 看起来不像是字符串……没有 .replace 方 法,或者返回值不能使用 .split 方法拆分。

except AttributeError:

pass

# 为了确保的确是可迭代对象,也为了保存一份副本,使用所得值创建一个元组。

field_names = tuple(field_names)

在那篇短文的最后,Alex 多次强调,要抑制住创建抽象基类的冲动。滥 用抽象基类会造成灾难性后果,表明语言太注重表面形式,这对以实用 和务实著称的 Python 可不是好事。在审阅本书的过程中,Alex 写道:抽象基类是用于封装框架引入的一般性概念和抽象的,例如“一个 序列”和“一个确切的数”。(读者)基本上不需要自己编写新的抽 象基类,只要正确使用现有的抽象基类,就能获得 99.9% 的好处, 而不用冒着设计不当导致的巨大风险。

下面通过实例讲解白鹅类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值