PEP 443 -- 单分派泛型函数(Single-dispatch generic functions)
英文原文:https://www.python.org/dev/peps/pep-0443
采集日期:2020-03-17
PEP: 443
Title: Single-dispatch generic functions
Author: Łukasz Langa lukasz@python.org
Discussions-To: Python-Dev python-dev@python.org
Status: Final
Type: Standards Track
Created: 22-May-2013
Post-History: 22-May-2013, 25-May-2013, 31-May-2013
Replaces: 245, 246, 3124
目录
- 摘要
- 原由和目标(Rationale and Goals)
- 用户 API(User API)
- 关于目前的实现代码(Implementation Notes)
- 模板的用法(Usage Patterns)
- 替代方案(Alternative approaches)
- 致谢(Acknowledgements)
- 参考文献(References)
- 版权(Copyright)
摘要(Abstract)
本 PEP 在 functools
标准库模块中提出了一种新机制,以提供一种简单的泛型编程形式,名为单派发(single-dispatch)泛型函数。
泛型函数由多个函数组成,可为不同的类型实现相同的操作。调用期间应选用哪一实现由分派算法确定。如果实现代码根据单个参数的类型做出选择,则被称为单派发。
原由和目标(Rationale and Goals)
Python 一直以内置和标准库的形式提供了各种泛型函数,诸如 len()
、iter()
、pprint.pprint()
、copy.copy()
和 operator
模块中的大部分函数。不过,目前情况是:
-
开发人员缺少一种简单、直接的方式来新建泛型函数。
-
缺少一种将方法添加到现有泛型函数的标准方法,某些方法是用注册函数添加的,另一些方法则需要定义
__special__
方法,且有可能是以动态替换(monkeypatching)的方式完成。
此外,为了决定该如何处理对象,而由 Python 代码对收到的参数类型进行检查,这种做法目前已经是一种常见的反面典型了(anti-pattern)。
比如,代码可能既要能接受某类型的一个对象,又要能接受该类型对象组成的序列。目前,“浅显的方案”是对类型进行检查,但这种做法十分脆弱且无法扩展。
抽象基类(Abstract Base Class)能让对象的当前行为发现起来更容易一些,但无助于增加新的行为。这样采用现成(already-written)库的开发人员可能就无法修改对象处理方式了,特别是当对象是由第三方创建的时候。
因此,本 PEP 提出了一种统一的 API,用装饰符(decorator)来对动态重载(overload)进行定位。
用户 API(User 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()
属性将返回未经装饰前的函数。这样就能够实现装饰器的堆叠(stack)和序列化(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
基类型做过注册了,这意味着如果找不到更好的实现代码,就会采用 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>
为了确保解释和使用起来都很容易,并与 functools
模块中的现有成员保持一致,故意只提供了这些 API,且必须如此(opinionate)。
关于目前的实现代码(Implementation Notes)
本 PEP 介绍的功能已在 pkgutil
标准库模块中实现为 simplegeneric
。因为该部分实现代码已较为成熟,所以多半是期望能保持不变。实现代码可参考 hg.python.org。
用于分派的类型被设为装饰器的参数。也曾考虑过另一种格式的函数注解,但最后还是拒绝纳入。截至2013年5月,这种用法已经超出了标准库的范畴,使用注解的最佳实践尚存在争议。
根据目前的 pkgutil.simplegeneric
实现代码,遵照在抽象基类上注册虚子类的约定,分派代码的注册过程将不是线程安全的。
抽象基类(Abstract Base Classes)
pkgutil.simplegeneric
的实现代码依赖于多种形式的方法解析顺序(method resolution order,MRO)。@singledispatch
会移除老式类和 Zope ExtensionClass 的特殊处理过程。更重要的是,它引入了对抽象基类(ABC)的支持。
在为 ABC 注册泛型函数的实现代码时,分派算法会切换为 C3 线性化(linearization)的扩展形式,这种形式会在给定参数的 MRO 中加入相关的 ABC。分派算法会在引入 ABC 功能的地方插入 ABC,即 issubclass(cls, abc)
针对类本身返回 True
,而针对其他所有的直接基类则返回 False
。在该类的 MRO 中,给定类的隐含 ABC(或是注册的,或是通过 __len__()
等特殊方法推断出来的)将直接插到最后一个显式列出的 ABC 之后。
最简单形式的线性化就是返回给定类型的 MRO:
>>> _compose_mro(dict, [])
[<class 'dict'>, <class 'object'>]
如果第二个参数包含了给定类型的抽象基类,则基类会按可推算的顺序插入:
>>> _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 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 的早期版本中包含了一种更简单的自定义处理方案,但那产生了很多结果诡异的边界案例。
模板的用法(Usage Patterns)
本 PEP 建议只对特别标记为泛型的函数功能进行扩展。正如基类的方法可被子类覆盖一样,函数也可以被重载,以便为给定类型提供特定功能。
通用重载不等于任意重载,从某种意义上说,没必要期望大家以不可推算的方式随意对已有函数的功能进行重新定义。相反在通常情况下,实际的程序中用到的泛型函数更倾向于按照可推算模式进行,已注册的实现代码也应是非常容易发现的。
如果模块要定义新的泛型操作,则通常还会在同一位置为现有类型实现所有必要的代码。同样,如果模块要定义新的类型,则通常会在模块中为所有已知或相关的泛型函数定义实现代码。如此这般,不论是被重载函数,或是即将加入支持代码的新类型,绝大多数已注册的实现代码都可以就近找到他们。
只有在极少数情况下,才会相关函数和类型之外的模块中注册实现代码。在并非做不到或有意隐匿的情况下,极少数的实现代码不在相关类型或函数附近,他们通常无需理解或知晓定义所在作用域之外的东西。(“支持模块”除外,最佳实践建议对他们作对应性的命名。)
如前所述,单派发泛型已在整个标准库中大量应用。若有一种整洁、标准的实现方案,将为重构这些自定义的实现代码指明一条通用的实现途径,同时为适应用户可扩展性打开了一扇大门。
替代方案(Alternative approaches)
在 PEP 3124 中,Phillip J. Eby 提出了一种成熟的解决方案,支持基于任意规则集的重载(已带根据实参进行分派的默认实现),以及接口(interface)、适配(adaptation)和方法组合(combine)。PEAK 规则对 PJE 在 PEP 中描述的概念给出了参考实现。
这么宏大的方案天生就是复杂的,很难让大家形成共识。相反,本 PEP 仅专注于易于推断的单个功能点。重点是要注意,本文并不排除目前或将来采用其他方法。
在 2005 年关于 Artima 的文章中,Guido van Rossum 提出了一种泛型函数的实现方案,支持依据函数的所有参数类型进行分派。同一方案也被 PyPI 中 Andrey Popp 的 generic
包和 David Mertz 的 gnosis.magic.multimethods
选用。
虽然猛一看似乎很不错,但 Fredrik Lundh 的评论值得同意,即“如果设计 API 时要附带一堆的逻辑,只是为了弄清楚函数应该执行的代码,那可能就该另请高明了”。换句话说,本 PEP 中提出的单个参数方案不仅易于实现,而且清楚地表明更复杂的分派是一种反面典型。这里的单参数分派还有一个优点,就是直接与面向对象编程中熟悉的方法分派机制相对应。唯一的区别就是,自定义的实现代码与数据(面向对象的方法)紧密相关,或是与算法(单分派重载)更靠近。
PyPy 中的 RPython 提供了 extendabletype
,那是一个元类,使得类可以在外部进行扩展。结合 pairtype()
和 pair()
工厂方法,就能提供一种单派发泛型方案。
致谢(Acknowledgements)
除了 Phillip J. Eby 在 PEP 3124 和 PEAK-Rules 中的努力,本文还深受以下内容的影响:Paul Moore 建议将 pkgutil.simplegeneric
发布到 functools
API 中去的原提案、Guido van Rossum 的多重方法文章、与 Raymond Hettinger 关于重写通用 pprint 的多次讨论。非常感谢 Nick Coghlan 鼓励我创建此 PEP 并首先给出反馈。
参考文献(References)
- PEP 8 在“编程建议”中标明“Python 标准库将不使用函数注解,因为那会将某种注解风格过早确定下来”。
(https://www.python.org/dev/peps/pep-0008)
版权(Copyright)
本文已在公共领域发布。