返回介绍

12.1 子类化内置类型很麻烦

发布于 2024-02-05 21:59:47 字数 4488 浏览 0 评论 0 收藏 0

在 Python 2.2 之前,内置类型(如 list 或 dict)不能子类化。在 Python 2.2 之后,内置类型可以子类化了,但是有个重要的注意事项:内置类型(使用 C 语言编写)不会调用用户定义的类覆盖的特殊方法。

PyPy 的文档使用简明扼要的语言描述了这个问题,见于“Differences between PyPy and CPython”中“Subclasses of built-in types”一节:

至于内置类型的子类覆盖的方法会不会隐式调用,CPython 没有制定官方规则。基本上,内置类型的方法不会调用子类覆盖的方法。例如,dict 的子类覆盖的 __getitem__() 方法不会被内置类型的 get() 方法调用。

示例 12-1 说明了这个问题。

示例 12-1 内置类型 dict 的 __init__ 和 __update__ 方法会忽略我们覆盖的 __setitem__ 方法

>>> class DoppelDict(dict):
...   def __setitem__(self, key, value):
...     super().__setitem__(key, [value] * 2)  # ➊
...
>>> dd = DoppelDict(one=1)  # ➋
>>> dd
{'one': 1}
>>> dd['two'] = 2  # ➌
>>> dd
{'one': 1, 'two': [2, 2]}
>>> dd.update(three=3)  # ➍
>>> dd
{'three': 3, 'one': 1, 'two': [2, 2]}

❶ DoppelDict.__setitem__ 方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类。

❷ 继承自 dict 的 __init__ 方法显然忽略了我们覆盖的 __setitem__ 方法:'one' 的值没有重复。

❸ [] 运算符会调用我们覆盖的 __setitem__ 方法,按预期那样工作:'two' 对应的是两个重复的值,即 [2, 2]。

❹ 继承自 dict 的 update 方法也不使用我们覆盖的 __setitem__ 方法:'three' 的值没有重复。

原生类型的这种行为违背了面向对象编程的一个基本原则:始终应该从实例(self)所属的类开始搜索方法,即使在超类实现的类中调用也是如此。在这种糟糕的局面中,__missing__ 方法(参见 3.4.2 节)却能按预期方式工作,不过这只是特例。

不只实例内部的调用有这个问题(self.get() 不调用 self.__getitem__()),内置类型的方法调用的其他类的方法,如果被覆盖了,也不会被调用。示例 12-2 是一个例子,改编自 PyPy 文档中的示例

示例 12-2 dict.update 方法会忽略 AnswerDict.__getitem__ 方法

>>> class AnswerDict(dict):
...   def __getitem__(self, key):  # ➊
...     return 42
...
>>> ad = AnswerDict(a='foo')  # ➋
>>> ad['a']  # ➌
42
>>> d = {}
>>> d.update(ad)  # ➍
>>> d['a']  # ➎
'foo'
>>> d
{'a': 'foo'}

❶ 不管传入什么键,AnswerDict.__getitem__ 方法始终返回 42。

❷ ad 是 AnswerDict 的实例,以 ('a', 'foo') 键值对初始化。

❸ ad['a'] 返回 42,这与预期相符。

❹ d 是 dict 的实例,使用 ad 中的值更新 d。

❺ dict.update 方法忽略了 AnswerDict.__getitem__ 方法。

 直接子类化内置类型(如 dict、list 或 str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承 collections 模块中的类,例如 UserDict、UserList 和 UserString,这些类做了特殊设计,因此易于扩展。

如果不子类化 dict,而是子类化 collections.UserDict,示例 12-1 和示例 12-2 中暴露的问题便迎刃而解了。参见示例 12-3。

示例 12-3 DoppelDict2 和 AnswerDict2 能像预期那样使用,因为它们扩展的是 UserDict,而不是 dict

>>> import collections
>>>
>>> class DoppelDict2(collections.UserDict):
...   def __setitem__(self, key, value):
...     super().__setitem__(key, [value] * 2)
...
>>> dd = DoppelDict2(one=1)
>>> dd
{'one': [1, 1]}
>>> dd['two'] = 2
>>> dd
{'two': [2, 2], 'one': [1, 1]}
>>> dd.update(three=3)
>>> dd
{'two': [2, 2], 'three': [3, 3], 'one': [1, 1]}
>>>
>>> class AnswerDict2(collections.UserDict):
...   def __getitem__(self, key):
...     return 42
...
>>> ad = AnswerDict2(a='foo')
>>> ad['a']
42
>>> d = {}
>>> d.update(ad)
>>> d['a']
42
>>> d
{'a': 42}

为了衡量子类化内置类型所需的额外工作量,我做了个实验,重写了示例 3-8 中的 StrKeyDict 类。原始版继承自 collections.UserDict,而且只实现了三个方法:__missing__、__contains__ 和 __setitem__。在实验中,StrKeyDict 直接子类化 dict,而且也实现了那三个方法,不过根据存储数据的方式稍微做了调整。可是,为了让实验版通过原始版的测试组件,还要实现 __init__、get 和 update 方法,因为继承自 dict 的版本拒绝与覆盖的 __missing__、__contains__ 和 __setitem__ 方法合作。示例 3-8 中那个 UserDict 子类有 16 行代码,而实验的 dict 子类有 37 行代码。2

2如果好奇,实验版在本书代码仓库里的 strkeydict_ dictsub.py 文件中。

综上,本节所述的问题只发生在 C 语言实现的内置类型内部的方法委托上,而且只影响直接继承内置类型的用户自定义类。如果子类化使用 Python 编写的类,如 UserDict 或 MutableMapping,就不会受此影响。3

3顺便说一下,在这方面,PyPy 的行为比 CPython“正确”,不过会导致微小的差异。详情参见“Differences between PyPy and CPython”。

与继承,尤其是多重继承有关的另一个问题是:如果同级别的超类定义了同名属性,Python 如何确定使用哪个?下一节解答。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文