PEP 443 – 单分派泛型函数
- 作者:
- Łukasz Langa <lukasz at python.org>
- 讨论至:
- Python-Dev 列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2013年5月22日
- Python 版本:
- 3.4
- 发布历史:
- 2013年5月22日,2013年5月25日,2013年5月31日
- 取代:
- 245, 246, 3124
摘要
本 PEP 提出了 functools 标准库模块中的一种新机制,提供了一种称为单分派泛型函数的简单泛型编程形式。
泛型函数由多个为不同类型实现相同操作的函数组成。在调用期间应使用哪个实现由分派算法确定。当实现是根据单个参数的类型选择时,这称为单分派。
基本原理和目标
Python 一直提供各种内置和标准库泛型函数,例如 len()、iter()、pprint.pprint()、copy.copy() 以及 operator 模块中的大多数函数。然而,它目前
- 没有简单或直接的方式让开发者创建新的泛型函数,
- 没有标准的方式将方法添加到现有泛型函数(即,有些使用注册函数添加,有些需要定义
__special__方法,可能通过猴子补丁)。
此外,目前 Python 代码中常见的反模式是检查接收到的参数类型,以便决定如何处理这些对象。
例如,代码可能希望接受某种类型的对象,或者该类型对象的一个序列。目前,“显而易见”的做法是通过类型检查,但这很脆弱且不易扩展。
抽象基类使发现现有行为变得更容易,但无助于添加新行为。使用已编写库的开发者可能无法改变他们的对象被此类代码处理的方式,特别是如果他们使用的对象是由第三方创建的。
因此,本 PEP 提出了一个统一的 API,以使用装饰器解决动态重载问题。
用户 API
要定义一个泛型函数,请使用 @singledispatch 装饰器对其进行装饰。请注意,分派是根据第一个参数的类型进行的。相应地创建您的函数
>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
... if verbose:
... print("Let me just say,", end=" ")
... print(arg)
要向函数添加重载实现,请使用泛型函数的 register() 属性。这是一个装饰器,它接受一个类型参数并装饰一个为该类型实现操作的函数
>>> @fun.register(int)
... def _(arg, verbose=False):
... if verbose:
... print("Strength in numbers, eh?", end=" ")
... print(arg)
...
>>> @fun.register(list)
... def _(arg, verbose=False):
... if verbose:
... print("Enumerate this:")
... for i, elem in enumerate(arg):
... print(i, elem)
为了能够注册 lambda 表达式和预先存在的函数,register() 属性可以以函数形式使用
>>> def nothing(arg, verbose=False):
... print("Nothing.")
...
>>> fun.register(type(None), nothing)
register() 属性返回未装饰的函数。这使得装饰器堆叠、pickle 以及为每个变体独立创建单元测试成为可能
>>> @fun.register(float)
... @fun.register(Decimal)
... def fun_num(arg, verbose=False):
... if verbose:
... print("Half of your number:", end=" ")
... print(arg / 2)
...
>>> fun_num is fun
False
调用时,泛型函数会根据第一个参数的类型进行分派
>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615
当没有针对特定类型注册的实现时,其方法解析顺序用于查找更通用的实现。用 @singledispatch 装饰的原始函数是为基础 object 类型注册的,这意味着如果没有找到更好的实现,就会使用它。
要检查泛型函数将为给定类型选择哪个实现,请使用 dispatch() 属性
>>> fun.dispatch(float)
<function fun_num at 0x104319058>
>>> fun.dispatch(dict) # note: default implementation
<function fun at 0x103fe0000>
要访问所有已注册的实现,请使用只读的 registry 属性
>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
<class 'decimal.Decimal'>, <class 'list'>,
<class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>
所提议的 API 故意是有限且有主见的,以确保它易于解释和使用,并与 functools 模块中的现有成员保持一致。
实现说明
本 PEP 中描述的功能已在 pkgutil 标准库模块中作为 simplegeneric 实现。由于此实现已成熟,目标是将其基本原封不动地迁移。参考实现可在 hg.python.org 上找到 [1]。
分派类型指定为装饰器参数。曾考虑过使用函数注解的替代形式,但其包含已被拒绝。截至 2013 年 5 月,这种使用模式超出了标准库的范围 [2],并且关于注解用法的最佳实践仍在争论中。
基于当前的 pkgutil.simplegeneric 实现,并遵循在抽象基类上注册虚拟子类的约定,分派注册表将不是线程安全的。
抽象基类
pkgutil.simplegeneric 的实现依赖于多种形式的方法解析顺序(MRO)。@singledispatch 移除了对旧式类和 Zope 的 ExtensionClasses 的特殊处理。更重要的是,它引入了对抽象基类(ABC)的支持。
当一个泛型函数实现为 ABC 注册时,分派算法会切换到 C3 线性化的扩展形式,其中包括所提供参数的 MRO 中的相关 ABC。该算法在引入其功能的地方插入 ABC,即 issubclass(cls, abc) 对于类本身返回 True,但对于其所有直接基类返回 False。给定类的隐式 ABC(无论是注册的还是从特殊方法(如 __len__())的存在推断出的)会直接插入到该类 MRO 中明确列出的最后一个 ABC 之后。
在其最基本的形式中,此线性化返回给定类型的 MRO
>>> _compose_mro(dict, [])
[<class 'dict'>, <class 'object'>]
当第二个参数包含指定类型为其子类的 ABC 时,它们将按可预测的顺序插入
>>> _compose_mro(dict, [Sized, MutableMapping, str,
... Sequence, Iterable])
[<class 'dict'>, <class 'collections.abc.MutableMapping'>,
<class 'collections.abc.Mapping'>, <class 'collections.abc.Sized'>,
<class 'collections.abc.Iterable'>, <class 'collections.abc.Container'>,
<class 'object'>]
虽然这种操作模式明显较慢,但所有分派决策都会被缓存。当在泛型函数上注册新实现时,或者当用户代码在 ABC 上调用 register() 以隐式地子类化它时,缓存会失效。在后一种情况下,可能会出现分派模糊的情况,例如
>>> from collections.abc import Iterable, Container
>>> class P:
... pass
>>> Iterable.register(P)
<class '__main__.P'>
>>> Container.register(P)
<class '__main__.P'>
面对模糊不清的情况,@singledispatch 拒绝猜测的诱惑
>>> @singledispatch
... def g(arg):
... return "base"
...
>>> g.register(Iterable, lambda arg: "iterable")
<function <lambda> at 0x108b49110>
>>> g.register(Container, lambda arg: "container")
<function <lambda> at 0x108b491c8>
>>> g(P())
Traceback (most recent call last):
...
RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'>
or <class 'collections.abc.Iterable'>
请注意,如果在类定义期间显式提供了MRO中的一个或多个ABC作为基类,则不会引发此异常。在这种情况下,分派将按照MRO顺序进行
>>> class Ten(Iterable, Container):
... def __iter__(self):
... for i in range(10):
... yield i
... def __contains__(self, value):
... return value in range(10)
...
>>> g(Ten())
'iterable'
当从特殊方法(如 __len__() 或 __contains__())的存在推断出对 ABC 进行子类化时,也会出现类似的冲突
>>> class Q:
... def __contains__(self, value):
... return False
...
>>> issubclass(Q, Container)
True
>>> Iterable.register(Q)
>>> g(Q())
Traceback (most recent call last):
...
RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'>
or <class 'collections.abc.Iterable'>
该 PEP 的早期版本包含一个更简单但产生了一些令人惊讶的边缘情况的自定义方法 [3]。
使用模式
本 PEP 提议仅扩展明确标记为泛型的函数的行为。就像基类方法可以被子类覆盖一样,函数也可以被重载以提供给定类型的自定义功能。
通用重载不等于*任意*重载,因为我们不需要期望人们以不可预测的方式随意重新定义现有函数的行为。相反,实际程序中的泛型函数使用往往遵循非常可预测的模式,并且在常见情况下,注册的实现是高度可发现的。
如果一个模块正在定义一个新的泛型操作,它通常也会在同一位置为现有类型定义任何所需的实现。同样,如果一个模块正在定义一个新类型,那么它通常也会在那里为它知道或关心的任何泛型函数定义实现。因此,绝大多数注册的实现都可以在被重载的函数旁边,或者在新定义的类型旁边找到,该实现是为该类型添加支持的。
只有在极少数情况下,一个模块中会注册既不包含函数也不包含添加实现所针对的类型(或多种类型)的实现。在没有能力不足或故意模糊不清的情况下,少数未在相关类型或函数旁边注册的实现,通常不需要在定义这些实现的范围之外被理解或知晓。(除了“支持模块”的情况,最佳实践建议相应地命名它们。)
如前所述,单分派泛型在整个标准库中已经非常普遍。一种清晰、标准化的实现方式为重构那些自定义实现以使用通用实现提供了前进的道路,同时为用户可扩展性开辟了空间。
替代方法
在 PEP 3124 中,Phillip J. Eby 提出了一个完整的解决方案,包括基于任意规则集(默认实现基于参数类型分派)的重载,以及接口、适配和方法组合。PEAK-Rules [4] 是 PJE PEP 中描述概念的参考实现。
这种广泛的方法本质上是复杂的,这使得达成共识变得困难。相比之下,本 PEP 专注于一个易于理解的单一功能。重要的是要注意,这并不排除现在或将来使用其他方法。
在 2005 年 Artima 上的一篇文章中 [5],Guido van Rossum 提出了一种泛型函数实现,该实现根据函数所有参数的类型进行分派。Andrey Popp 在 PyPI 上提供的 generic 包 [6] 以及 David Mertz 的 gnosis.magic.multimethods [7] 采用了相同的方法。
虽然这乍一看似乎是可取的,但我同意 Fredrik Lundh 的评论:“如果你设计的 API 有一页又一页的逻辑只是为了弄清楚一个函数应该执行什么代码,你可能应该把 API 设计交给其他人”。换句话说,本 PEP 中提出的单参数方法不仅更容易实现,而且清楚地表明在更复杂的状态上进行分派是一种反模式。它还具有与面向对象编程中熟悉的方法分派机制直接对应的优点。唯一的区别在于自定义实现是更紧密地与数据(面向对象方法)相关联还是与算法(单分派重载)相关联。
PyPy 的 RPython 提供了 extendabletype [8],这是一个允许类外部扩展的元类。结合 pairtype() 和 pair() 工厂,这提供了一种单分派泛型的形式。
致谢
除了 Phillip J. Eby 在 PEP 3124 和 PEAK-Rules 方面的工作外,影响还包括 Paul Moore 最初的问题 [9],该问题提议将 pkgutil.simplegeneric 作为 functools API 的一部分公开;Guido van Rossum 关于多重方法的文章 [5];以及与 Raymond Hettinger 关于通用 pprint 重写进行的讨论。非常感谢 Alyssa Coghlan 鼓励我创建此 PEP 并提供初步反馈。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0443.rst