PEP 443 – 单分派泛型函数
- 作者:
- Łukasz Langa <lukasz at python.org>
- 讨论地址:
- Python-Dev 邮件列表
- 状态:
- 最终
- 类型:
- 标准轨道
- 创建日期:
- 2013-05-22
- Python 版本:
- 3.4
- 历史记录:
- 2013-05-22, 2013-05-25, 2013-05-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()
属性返回未装饰的函数。这使得装饰器堆叠、腌制以及为每个变体独立创建单元测试成为可能
>>> @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'>
请注意,如果在类定义期间显式地提供了一个或多个 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 提议仅扩展专门标记为泛型的函数的行为。就像基类方法可以被子类覆盖一样,函数也可以被重载以提供给定类型的自定义功能。
通用重载并不等于*任意*重载,因为我们无需期望人们以不可预测的方式随机重新定义现有函数的行为。相反,泛型函数在实际程序中的使用往往遵循非常可预测的模式,并且注册的实现通常在常见情况下易于发现。
如果模块定义了新的泛型操作,它通常也会在同一位置定义对现有类型所需的任何实现。同样,如果模块定义了新类型,那么它通常会在其中定义对它知道或关心的任何泛型函数的实现。因此,大多数注册的实现可以在被重载的函数附近或在为其添加支持的新定义类型附近找到。
只有在很少的情况下,才会在既不包含函数也不包含添加实现的类型 (s) 的模块中注册实现。在没有无能或故意模糊的情况下,那些未在相关类型 (s) 或函数 (s) 附近注册的少数实现,通常不需要在这些实现定义的范围之外进行理解或了解。(除了“支持模块”案例,在最佳实践中建议相应地命名它们。)
如前所述,单分派泛型在整个标准库中非常普遍。一种简洁、标准的实现方法提供了一种将这些自定义实现重构为使用通用实现的途径,同时为用户扩展性打开大门。
替代方案
在 PEP 3124 中,Phillip J. Eby 提议了一种完整的解决方案,该解决方案基于任意规则集(默认实现根据参数类型进行分派)以及接口、适配和方法组合。PEAK-Rules [4] 是 PJE PEP 中描述的概念的参考实现。
这种广泛的方法本质上很复杂,这使得达成共识变得困难。相比之下,本 PEP 侧重于一项易于理解的单一功能。重要的是要注意,这并不排除现在或将来使用其他方法。
在 2005 年 Artima 上的一篇文章中 [5],Guido van Rossum 提出了一种泛型函数实现,该实现根据函数所有参数的类型进行分派。Andrey Popp 的 generic
包(可在 PyPI 上获取 [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
最后修改时间:2023-10-11 12:05:51 GMT