PEP 443 单分派泛型函数 -- Python官方文档译文 [原创]

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 [email protected]

Discussions-To: Python-Dev [email protected]

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)
    • 抽象基类(Abstract Base Classes)
  • 模板的用法(Usage Patterns)
  • 替代方案(Alternative approaches)
  • 致谢(Acknowledgements)
  • 参考文献(References)
  • 版权(Copyright)

摘要(Abstract)



本 PEP 在 functools 标准库模块中提出了一种新机制,以提供一种简单的泛型编程形式,名为单派发(single-dispatch)泛型函数。

泛型函数由多个函数组成,可为不同的类型实现相同的操作。调用期间应选用哪一实现由分派算法确定。如果实现代码根据单个参数的类型做出选择,则被称为单派发

原由和目标(Rationale and Goals)



Python 一直以内置和标准库的形式提供了各种泛型函数,诸如 len()iter()pprint.pprint()copy.copy()operator 模块中的大部分函数。不过,目前情况是:

  1. 开发人员缺少一种简单、直接的方式来新建泛型函数。
  2. 缺少一种将方法添加到现有泛型函数的标准方法,某些方法是用注册函数添加的,另一些方法则需要定义 __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)


  1. http://hg.python.org/features/pep-443/file/tip/Lib/functools.py#l359
  1. PEP 8 在“编程建议”中标明“Python 标准库将不使用函数注解,因为那会将某种注解风格过早确定下来”。

    https://www.python.org/dev/peps/pep-0008

  1. http://bugs.python.org/issue18244
  1. http://www.python.org/dev/peps/pep-3124/
  1. http://peak.telecommunity.com/DevCenter/PEAK_2dRules
  1. http://www.artima.com/weblogs/viewpost.jsp?thread=101605
  1. http://pypi.python.org/pypi/generic
  1. http://gnosis.cx/publish/programming/charming_python_b12.html(译者注:链接已失效)
  1. https://bitbucket.org/pypy/pypy/raw/default/rpython/tool/pairtype.py
  1. http://bugs.python.org/issue5135

版权(Copyright)



本文已在公共领域发布。

原文地址:https://www.cnblogs.com/popapa/p/PEP443.html

时间: 2024-10-12 09:31:34

PEP 443 单分派泛型函数 -- Python官方文档译文 [原创]的相关文章

PEP 3141 数值类型的层次结构 -- Python官方文档译文 [原创]

PEP 3141 -- 数值类型的层次结构(A Type Hierarchy for Numbers) 英文原文:https://www.python.org/dev/peps/pep-3141 采集日期:2020-02-27 PEP: 3141 Title: A Type Hierarchy for Numbers Author: Jeffrey Yasskin [email protected] Status: Final Type: Standards Track Created: 23-

PEP 484 类型提示 -- Python官方文档译文 [原创]

英文原文:https://www.python.org/dev/peps/pep-0484/ 采集日期:2019-12-27 PEP 484 -- 类型提示(Type Hints) PEP: 484 Title: Type Hints Author: Guido van Rossum <guido at python.org>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, ?ukasz Langa <lukasz at pytho

别开心太早,Python 官方文档的翻译差远了

近几天,很多公众号发布了 Python 官方文档的消息.然而,一个特别奇怪的现象就发生了,让人啼笑皆非. Python 文档的中文翻译工作一直是“默默无闻”,几个月前,我还吐槽过这件事<再聊聊Python中文社区的翻译>,当时我们的进度是 10.3%,远远落后于日本和法国,甚至落后于巴西! 这次所谓的中文版,当然是未完成翻译的残品.刚查了下,整体进度是 19.7%. 翻译进度不足20% 有的公众号在发布消息的时候,说明了这不是官宣.不是正式发布版,还指出了中文版的访问地址是隐藏入口.这都是忠于

通读Python官方文档之cgi

cgi 通用网关接口 前驱知识 网关协议学习:CGI.FastCGI.WSGI 简单点说: web服务器接受请求,启动CGI:CGI接受请求,处理,返回给服务器:服务器返回给用户 cgi效率不高,每次都要fork一个新进程出来 WCGI,Python架设的一个桥,连接了服务器和web框架,相当将cgi的连接功能独立了出来,并把处理功能留给了web框架 简介 CGI脚本由HTTP服务器启动,通常用来处理用户通过<FROM>提交的数据. 通常,CGI脚本位于服务器的专门的cgi-bin目录下.HT

python官方文档

Tutorialstart here Library Referencekeep this under your pillow Language Referencedescribes syntax and language elements Python Setup and Usagehow to use Python on different platforms Python HOWTOsin-depth documents on specific topics Extending and E

python官方文档阅读

1.流程控制语句 2.数据结构 原文地址:https://www.cnblogs.com/panlei3707/p/8365967.html

reactor官方文档译文(2)Reactor-core模块

You should never do your asynchronous work alone. — Jon Brisbin 完成Reactor 1后写到 You should never do your asynchronous work alone. — Stephane Maldini 完成Reactor 2后写到 名称解释:back pressure:背压.在交换机在阻止外来数据包发送到堵塞端口的时候可能会发生丢包.而背压就是考验交换机在这个时候避免丢包的能力.很多的交换机当发送或接收

reactor官方文档译文(1)Reactor简介

原文地址:http://projectreactor.io/docs/reference/ Reactor简介 Reactor是一个基础库,用在构建实时数据流应用.要求有容错和低延迟至毫秒.纳秒.皮秒的服务. — PrefaceTL;DR 什么是Reactor? 让我们大致了解一下Reactor.在你使用喜欢的搜索敲入一些关键词如Reactive.spring Reactive.Asynchronous java或者仅仅是"What the heck is Reactor?".简而言之

python附录-re.py模块源码(含re官方文档链接)

re模块 python官方文档链接:https://docs.python.org/zh-cn/3/library/re.html re模块源码 r"""Support for regular expressions (RE). This module provides regular expression matching operations similar to those found in Perl. It supports both 8-bit and Unicod