PEP 3124 – 重载、通用函数、接口和适配
- 作者:
- Phillip J. Eby <pje at telecommunity.com>
- 讨论至:
- Python-3000 列表
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 要求:
- 3107, 3115, 3119
- 创建日期:
- 2007 年 4 月 28 日
- 发布历史:
- 2007 年 4 月 30 日
- 取代:
- 245, 246
推迟
参见 https://mail.python.org/pipermail/python-3000/2007-July/008784.html。
摘要
本 PEP 提出了一个新的标准库模块 overloading,旨在提供通用编程功能,包括动态重载(即通用函数)、接口、适配、方法组合(类似于 CLOS 和 AspectJ),以及简单形式的面向切面编程 (AOP)。
提议的 API 也可扩展;也就是说,库开发者将能够实现他们自己的专用接口类型、通用函数分派器、方法组合算法等,并且这些扩展将由提议的 API 作为一等公民对待。
该 API 将完全用纯 Python 实现,不包含 C 语言,但可能依赖于 CPython 特定的功能,例如 sys._getframe 和函数的 func_code 属性。预计 Jython 和 IronPython 等将有其他方法来实现类似功能(可能使用 Java 或 C#)。
基本原理和目标
Python 一直提供各种内置和标准库的通用函数,例如 len()、iter()、pprint.pprint() 以及 operator 模块中的大多数函数。然而,目前它
- 没有为开发者提供创建新通用函数的简单或直接方法,
- 没有标准的方法将方法添加到现有通用函数(即,有些通过注册函数添加,另一些则需要定义
__special__方法,可能通过猴子补丁实现),以及 - 不允许基于多个参数类型进行分派(除了算术运算符的有限形式,其中“右侧”(
__r*__) 方法可用于执行双参数分派。
此外,目前 Python 代码中常见的反模式是检查接收参数的类型,以便决定如何处理这些对象。例如,代码可能希望接受某种类型的对象,或者该类型对象的序列。
目前,“显而易见”的做法是通过类型检查,但这既脆弱又无法扩展。使用已编写库的开发者可能无法更改其对象被此类代码处理的方式,特别是如果他们使用的对象是由第三方创建的。
因此,本 PEP 提出一个标准库模块,利用装饰器和参数注解(PEP 3107)来解决这些以及相关问题。主要提供的功能有:
- 一种动态重载机制,类似于 Java 和 C++ 等语言中的静态重载,但包括 CLOS 和 AspectJ 中可选的方法组合功能。
- 一个受 Haskell 类型类启发(但更具动态性,且没有任何静态类型检查)的简单“接口和适配”库,带有扩展 API,允许注册用户定义的接口类型,例如 PyProtocols 和 Zope 中的接口类型。
- 一个简单的“切面”实现,便于创建有状态适配器和执行其他有状态 AOP。
这些功能将以一种能够创建和使用扩展实现的方式提供。例如,库应该能够为通用函数定义新的分派标准,以及定义新的接口类型,并用它们替代预定义的功能。例如,应该能够使用 zope.interface 接口对象来指定函数参数的所需类型,只要 zope.interface 包正确注册了自身(或者第三方完成了注册)。
通过这种方式,提议的 API 仅仅提供了一种统一的方式来访问其范围内的功能,而不是规定一个单一的实现用于所有库、框架和应用程序。
用户 API
重载 API 将作为一个名为 overloading 的模块实现,提供以下功能:
重载/通用函数
使用 @overload 装饰器可以定义函数的备用实现,这些实现通过参数类型进行专门化。本地命名空间中必须已经存在同名函数。现有函数会被装饰器原地修改以添加新的实现,并且装饰器会返回修改后的函数。因此,以下代码
from overloading import overload
from collections import Iterable
def flatten(ob):
"""Flatten an object to its component iterables"""
yield ob
@overload
def flatten(ob: Iterable):
for o in ob:
for ob in flatten(o):
yield ob
@overload
def flatten(ob: basestring):
yield ob
创建了一个单一的 flatten() 函数,其实现大致相当于
def flatten(ob):
if isinstance(ob, basestring) or not isinstance(ob, Iterable):
yield ob
else:
for o in ob:
for ob in flatten(o):
yield ob
除了通过添加更多重载,通过重载定义的 flatten() 函数仍然可以扩展,而硬编码版本则无法扩展。
例如,如果有人想用一个不继承 basestring 的字符串类型来使用 flatten(),那么第二种实现将无法满足。然而,对于重载实现,他们可以这样写
@overload
def flatten(ob: MyString):
yield ob
或者这样(以避免复制实现)
from overloading import RuleSet
RuleSet(flatten).copy_rules((basestring,), (MyString,))
(另请注意,尽管 PEP 3119 提出抽象基类如 Iterable 应该能够允许像 MyString 这样的类声称是其子类,但这种声明是 全局的,贯穿整个应用程序。相比之下,添加特定的重载或复制规则是特定于单个函数的,因此不太可能产生意想不到的副作用。)
@overload 对比 @when
@overload 装饰器是更通用的 @when 装饰器的常用简写。它允许您省略要重载的函数的名称,但代价是要求目标函数必须在本地命名空间中。它也不支持除了通过参数注解指定的条件之外,添加其他条件。以下函数定义具有相同的效果,除了名称绑定副作用(将在下面描述)
from overloading import when
@overload
def flatten(ob: basestring):
yield ob
@when(flatten)
def flatten(ob: basestring):
yield ob
@when(flatten)
def flatten_basestring(ob: basestring):
yield ob
@when(flatten, (basestring,))
def flatten_basestring(ob):
yield ob
上面的第一个定义会将 flatten 绑定到它之前绑定的任何内容。如果 flatten 已经绑定到 when 装饰器的第一个参数,那么第二个定义也会做同样的事情。如果 flatten 未绑定或绑定到其他内容,它将重新绑定到给定的函数定义。上面最后两个定义将始终将 flatten_basestring 绑定到给定的函数定义。
使用这种方法,您可以给方法一个描述性名称(在回溯中通常很有用!)并在以后重用该方法。
除非另有规定,所有 overloading 装饰器都具有与 @when 相同的签名和绑定规则。它们接受一个函数和一个可选的“谓词”对象。
默认的谓词实现是一个类型元组,与重载函数的参数进行位置匹配。但是,可以使用扩展 API创建并注册任意数量的其他类型的谓词,然后这些谓词将与 @when 以及本模块创建的其他装饰器(如 @before、@after 和 @around)一起使用。
方法组合与重写
当一个重载函数被调用时,使用与调用参数 最具体匹配 的签名所对应的实现。如果没有实现匹配,则会引发 NoApplicableMethods 错误。如果有多个实现匹配,但没有一个签名比其他签名更具体,则会引发 AmbiguousMethods 错误。
例如,如果 foo() 函数被调用时带有两个整数参数,则下面这对实现是模糊的,因为两个签名都适用,但没有任何一个签名比另一个更 具体 (即,它们彼此之间不隐含)
def foo(bar:int, baz:object):
pass
@overload
def foo(bar:object, baz:int):
pass
相比之下,以下这对实现永远不会模糊,因为一个签名总是隐含另一个签名;int/int 签名比 object/object 签名更具体
def foo(bar:object, baz:object):
pass
@overload
def foo(bar:int, baz:int):
pass
签名 S1 隐含另一个签名 S2,如果 S1 适用,则 S2 也适用。签名 S1 比另一个签名 S2“更具体”,如果 S1 隐含 S2,但 S2 不隐含 S1。
尽管上述示例都使用具体或抽象类型作为参数注解,但并没有要求注解必须如此。它们也可以是“接口”对象(在接口和适配一节中讨论),包括用户定义的接口类型。(它们也可以是其他通过扩展 API适当注册了类型的对象。)
执行“下一个”方法
如果重载函数的第一个参数名为 __proceed__,它将被传递一个表示下一个最具体方法的可调用对象。例如,这段代码
def foo(bar:object, baz:object):
print "got objects!"
@overload
def foo(__proceed__, bar:int, baz:int):
print "got integers!"
return __proceed__(bar, baz)
将打印“got integers!”,然后是“got objects!”。
如果没有下一个最具体的方法,__proceed__ 将被绑定到 NoApplicableMethods 实例。当调用时,将引发一个新的 NoApplicableMethods 实例,其中包含传递给第一个实例的参数。
类似地,如果下一个最具体方法之间在优先级上存在歧义,__proceed__ 将被绑定到 AmbiguousMethods 实例,如果调用,它将引发一个新的实例。
因此,方法可以检查 __proceed__ 是否为错误实例,或者直接调用它。NoApplicableMethods 和 AmbiguousMethods 错误类有一个共同的基类 DispatchError,所以 isinstance(__proceed__, overloading.DispatchError) 足以识别 __proceed__ 是否可以安全调用。
(实现说明:使用像 __proceed__ 这样的魔术参数名可能可以被一个魔术函数取代,该函数将被调用以获取下一个方法。然而,魔术函数会降低性能,并且可能在非 CPython 平台上更难实现。但是,通过魔术参数名进行方法链式调用可以在任何支持从函数创建绑定方法的 Python 平台上高效实现——只需递归地绑定每个要链式调用的函数,使用以下函数或错误作为绑定方法的 im_self。)
“之前”和“之后”方法
除了上面所示的简单下一个方法链式调用之外,有时还需要其他方法组合方式。例如,“观察者模式”有时可以通过向函数添加额外的方法来实现,这些方法在正常实现之前或之后执行。
为了支持这些用例,overloading 模块将提供 @before、@after 和 @around 装饰器,它们大致对应于 Common Lisp 对象系统 (CLOS) 中的相同类型方法,或 AspectJ 中相应的“advice”类型。
与 @when 类似,所有这些装饰器都必须传递要重载的函数,并且可以选择接受一个谓词
from overloading import before, after
def begin_transaction(db):
print "Beginning the actual transaction"
@before(begin_transaction)
def check_single_access(db: SingletonDB):
if db.inuse:
raise TransactionError("Database already in use")
@after(begin_transaction)
def start_logging(db: LoggableDB):
db.set_log_level(VERBOSE)
@before 和 @after 方法在主函数体之前或之后调用,并且 永远不会被视为模糊的。也就是说,拥有多个具有相同或重叠签名的“before”或“after”方法不会导致任何错误。模糊性通过方法添加到目标函数的顺序来解决。
“之前”方法按照最具体方法优先的顺序调用,具有模糊性的方法按照它们添加的顺序执行。所有“之前”方法都在函数的任何“主要”方法(即正常的 @overload 方法)执行之前调用。
“之后”方法以 相反 的顺序调用,在所有函数“主要”方法执行完毕后。也就是说,它们以最不具体方法优先的顺序执行,具有模糊性的方法以它们添加顺序的相反顺序执行。
“之前”和“之后”方法的返回值会被忽略,任何方法(主要或其他)抛出的未捕获异常都会立即终止分派过程。“之前”和“之后”方法不能有 __proceed__ 参数,因为它们不负责调用任何其他方法。它们只是作为在主要方法之前或之后的一种通知而被调用。
因此,“之前”和“之后”方法可以用于检查或建立前置条件(例如,如果条件不满足则引发错误)或确保后置条件,而无需重复任何现有功能。
“环绕”方法
@around 装饰器将方法声明为“环绕”方法。“环绕”方法与主要方法非常相似,只是最不具体的“环绕”方法比最具体的“之前”方法具有更高的优先级。
然而,与“before”和“after”方法不同,“Around”方法 负责 调用它们的 __proceed__ 参数,以继续调用过程。“Around”方法通常用于转换输入参数或返回值,或者用特殊的错误处理或 try/finally 条件来包装特定情况,例如
from overloading import around
@around(commit_transaction)
def lock_while_committing(__proceed__, db: SingletonDB):
with db.global_lock:
return __proceed__(db)
它们也可以通过 不 调用 __proceed__ 函数来替换特定情况下的正常处理。
给“环绕”方法的 __proceed__ 将是下一个适用的“环绕”方法,一个 DispatchError 实例,或者一个合成方法对象,它将调用所有“之前”方法,然后是主要方法链,然后是所有“之后”方法,并返回主要方法链的结果。
因此,与普通方法一样,可以检查 __proceed__ 是否为 DispatchError 实例,或者直接调用它。“环绕”方法应返回 __proceed__ 返回的值,除非它希望修改或替换整个函数的不同返回值。
自定义组合
上述装饰器(@overload、@when、@before、@after 和 @around)共同实现了 CLOS 中所谓的“标准方法组合”——方法组合中最常见的模式。
然而,有时应用程序或库可能需要更复杂的方法组合类型。例如,如果您希望有“折扣”方法,返回一个百分比折扣,并从主要方法返回的值中扣除,您可能会这样写
from overloading import always_overrides, merge_by_default
from overloading import Around, Before, After, Method, MethodList
class Discount(MethodList):
"""Apply return values as discounts"""
def __call__(self, *args, **kw):
retval = self.tail(*args, **kw)
for sig, body in self.sorted():
retval -= retval * body(*args, **kw)
return retval
# merge discounts by priority
merge_by_default(Discount)
# discounts have precedence over before/after/primary methods
always_overrides(Discount, Before)
always_overrides(Discount, After)
always_overrides(Discount, Method)
# but not over "around" methods
always_overrides(Around, Discount)
# Make a decorator called "discount" that works just like the
# standard decorators...
discount = Discount.make_decorator('discount')
# and now let's use it...
def price(product):
return product.list_price
@discount(price)
def ten_percent_off_shoes(product: Shoe)
return Decimal('0.1')
类似的技术可用于实现各种 CLOS 风格的方法限定符和组合规则。创建自定义方法组合对象及其相应装饰器的过程将在扩展 API部分中详细描述。
顺便说一下,请注意所示的 @discount 装饰器将与通过其他代码定义的任何新谓词正确工作。例如,如果 zope.interface 将其接口类型注册为作为参数注解正确工作,您将能够基于其接口类型指定折扣,而不仅仅是类或 overloading 定义的接口类型。
类似地,如果像 RuleDispatch 或 PEAK-Rules 这样的库注册了适当的谓词实现和分派引擎,那么也可以将这些谓词用于折扣,例如
from somewhere import Pred # some predicate implementation
@discount(
price,
Pred("isinstance(product,Shoe) and"
" product.material.name=='Blue Suede'")
)
def forty_off_blue_suede_shoes(product):
return Decimal('0.4')
定义自定义谓词类型和分派引擎的过程也在扩展 API部分中详细描述。
类内部重载
当上述所有装饰器直接在类体中调用时,它们都具有一个特殊的附加行为:被装饰函数的第一个参数(如果存在,不包括 __proceed__)将被视为具有与其定义所在类相等的注解。
也就是说,这段代码
class And(object):
# ...
@when(get_conjuncts)
def __conjuncts(self):
return self.conjuncts
产生与此相同的效果(除了私有方法的存在)
class And(object):
# ...
@when(get_conjuncts)
def get_conjuncts_of_and(ob: And):
return ob.conjuncts
这种行为既是在定义大量方法时的便利增强,也是在子类中安全区分多参数重载的必要条件。例如,考虑以下代码
class A(object):
def foo(self, ob):
print "got an object"
@overload
def foo(__proceed__, self, ob:Iterable):
print "it's iterable!"
return __proceed__(self, ob)
class B(A):
foo = A.foo # foo must be defined in local namespace
@overload
def foo(__proceed__, self, ob:Iterable):
print "B got an iterable!"
return __proceed__(self, ob)
由于隐式类规则,调用 B().foo([]) 将依次打印“B got an iterable!”、“it’s iterable!”和“got an object”,而 A().foo([]) 只会打印 A 中定义的消息。
反之,如果没有隐式类规则,这两个“Iterable”方法的适用条件将完全相同,因此调用 A().foo([]) 或 B().foo([]) 都会导致 AmbiguousMethods 错误。
目前,如何在 Python 3.0 中以最佳方式实现此规则仍是一个悬而未决的问题。在 Python 2.x 中,类的元类直到类体结束时才被选择,这意味着装饰器可以插入自定义元类来执行此类处理。(例如,RuleDispatch 就是这样实现隐式类规则的。)
然而,PEP 3115 要求在类体执行 之前 确定类的元类,这使得此技术无法再用于类装饰。
本文撰写时,关于此问题的讨论仍在进行中。
接口和适配
overloading 模块提供了一个简单的接口和适配实现。以下示例定义了一个 IStack 接口,并声明 list 对象支持它
from overloading import abstract, Interface
class IStack(Interface):
@abstract
def push(self, ob)
"""Push 'ob' onto the stack"""
@abstract
def pop(self):
"""Pop a value and return it"""
when(IStack.push, (list, object))(list.append)
when(IStack.pop, (list,))(list.pop)
mylist = []
mystack = IStack(mylist)
mystack.push(42)
assert mystack.pop()==42
Interface 类是一种“通用适配器”。它接受一个参数:一个要适配的对象。然后,它将其所有方法绑定到目标对象,而不是自身。因此,调用 mystack.push(42) 等同于调用 IStack.push(mylist, 42)。
@abstract 装饰器将函数标记为抽象:即,没有实现。如果调用 @abstract 函数,它会引发 NoApplicableMethods。要使其可执行,必须使用前面描述的技术添加重载方法。(也就是说,方法可以使用 @when、@before、@after、@around 或任何自定义方法组合装饰器添加。)
在上面的例子中,当参数为列表和任意对象时,list.append 方法被添加为 IStack.push() 的方法。因此,IStack.push(mylist, 42) 被转换为 list.append(mylist, 42),从而实现了所需的操作。
抽象方法和具体方法
顺便说一下,请注意 @abstract 装饰器不限于在接口定义中使用;它可以在任何您希望创建最初没有方法的“空”通用函数的地方使用。特别是,它不需要在类内部使用。
另请注意,接口方法不必是抽象的;例如,可以编写如下接口:
class IWriteMapping(Interface):
@abstract
def __setitem__(self, key, value):
"""This has to be implemented"""
def update(self, other:IReadMapping):
for k, v in IReadMapping(other).items():
self[k] = v
只要为某种类型定义了 __setitem__,上述接口将提供一个可用的 update() 实现。然而,如果某些特定类型(或一对类型)有更高效的方式来处理 update() 操作,仍然可以为这种情况注册适当的重载。
子类化和重组
接口可以被子类化
class ISizedStack(IStack):
@abstract
def __len__(self):
"""Return the number of items on the stack"""
# define __len__ support for ISizedStack
when(ISizedStack.__len__, (list,))(list.__len__)
或通过组合现有接口的函数来组装
class Sizable(Interface):
__len__ = ISizedStack.__len__
# list now implements Sizable as well as ISizedStack, without
# making any new declarations!
如果接口中定义的任何方法在运行时作用于该类的实例时,都不能保证引发 NoApplicableMethods 错误,那么在特定时间点,该类可以被认为是“适配”该接口的。
然而,在正常使用中,是“求饶比请求更容易”。也就是说,通过将对象适配到接口(例如 IStack(mylist))或直接调用接口方法(例如 IStack.push(mylist, 42))来使用对象上的接口比尝试弄清楚对象是否可适配(或直接实现)接口更容易。
在类中实现接口
可以使用 declare_implementation() 函数声明一个类直接实现一个接口
from overloading import declare_implementation
class Stack(object):
def __init__(self):
self.data = []
def push(self, ob):
self.data.append(ob)
def pop(self):
return self.data.pop()
declare_implementation(IStack, Stack)
上述 declare_implementation() 调用大致等同于以下步骤
when(IStack.push, (Stack,object))(lambda self, ob: self.push(ob))
when(IStack.pop, (Stack,))(lambda self, ob: self.pop())
也就是说,在 Stack 的任何子类的实例上调用 IStack.push() 或 IStack.pop(),将简单地委托给其实际的 push() 或 pop() 方法。
为了效率起见,当 s 是 Stack 的实例时,调用 IStack(s) 可能返回 s 而不是 IStack 适配器。(请注意,当 x 已经是 IStack 适配器时,调用 IStack(x) 将始终返回未更改的 x;这是在已知被适配对象 直接 实现接口而无需适配的情况下允许的额外优化。)
为了方便,在类头中声明实现可能很有用,例如
class Stack(metaclass=Implementer, implements=IStack):
...
而不是在套件末尾调用 declare_implementation()。
接口作为类型说明符
Interface 子类可以用作参数注解,以指示重载可接受的对象类型,例如
@overload
def traverse(g: IGraph, s: IStack):
g = IGraph(g)
s = IStack(s)
# etc....
然而,请注意,仅使用接口作为类型说明符,实际参数不会以任何方式更改或适配。您必须显式地将对象强制转换为适当的接口,如上所示。
然而,请注意,其他接口使用模式是可能的。例如,其他接口实现可能不支持适配,或者可能要求函数参数已经适配到指定的接口。因此,将接口用作类型说明符的具体语义取决于您实际使用的接口对象。
然而,对于本 PEP 定义的接口对象,其语义如上所述。如果 I1 的继承层次结构中的描述符集合是 I2 的继承层次结构中描述符的真超集,则接口 I1 被认为比另一个接口 I2“更具体”。
因此,例如,ISizedStack 比 ISizable 和 ISizedStack 都更具体,而与这些接口之间的继承关系无关。这纯粹是关于这些接口中包含哪些操作的问题——并且操作的 名称 并不重要。
接口(至少是 overloading 提供的接口)总是被认为不如具体类具体。其他接口实现可以自行决定它们的特异性规则,包括接口之间以及接口与类之间。
接口中的非方法属性
实现 Interface 实际上以相同的方式处理所有属性和方法(即描述符):它们的 __get__(如果存在,还有 __set__ 和 __delete__)方法会以包装(适配)对象作为“self”来调用。对于函数,这会产生一个绑定方法,将通用函数链接到包装对象。
对于非函数属性,最简单的方法是使用内置的 property,以及相应的 fget、fset 和 fdel 属性来指定
class ILength(Interface):
@property
@abstract
def length(self):
"""Read-only length attribute"""
# ILength(aList).length == list.__len__(aList)
when(ILength.length.fget, (list,))(list.__len__)
或者,可以将 _get_foo() 和 _set_foo() 等方法定义为接口的一部分,并根据这些方法定义属性,但这对于用户在创建直接实现接口的类时正确实现会有些困难,因为他们需要匹配所有单独的方法名称,而不仅仅是属性或特性的名称。
切面
上述适配系统假定适配器是“无状态的”,也就是说,适配器除了被适配对象的属性或状态之外,没有其他属性或状态。这遵循了 Haskell 的“类型类/实例”模型,以及“纯”(即,可传递组合的)适配器概念。
然而,有时为了提供某个接口的完整实现,需要某种额外的状态。
当然,一种可能性是将被适配对象打补丁,添加“私有”属性。但这容易发生命名冲突,并使初始化过程复杂化(因为任何使用这些属性的代码都必须检查它们是否存在并在必要时进行初始化)。它也不适用于没有 __dict__ 属性的对象。
因此,提供了 Aspect 类,以便轻松地将额外信息附加到以下对象:
- 具有
__dict__属性(因此切面实例可以存储在其中,以切面类为键), - 支持弱引用(因此切面实例可以使用全局但线程安全的弱引用字典进行管理),或者
- 实现或可以适配到
overloading.IAspectOwner接口(技术上,#1 或 #2 暗示了这一点)。
子类化 Aspect 会创建一个适配器类,其状态与被适配对象的生命周期绑定。
例如,假设您想统计 Target 实例上某个特定方法被调用的次数(一个经典的 AOP 示例)。您可能会这样做:
from overloading import Aspect
class Count(Aspect):
count = 0
@after(Target.some_method)
def count_after_call(self:Target, *args, **kw):
Count(self).count += 1
上述代码将记录 Target.some_method() 成功调用在 Target 实例上的次数(即,它不会计算错误,除非这些错误发生在更具体的“之后”方法中)。其他代码可以使用 Count(someTarget).count 访问计数。
Aspect 实例当然可以拥有 __init__ 方法,来初始化任何数据结构。它们可以使用 __slots__ 或基于字典的属性进行存储。
尽管此功能与 AspectJ 等功能齐全的 AOP 工具相比相当原始,但希望构建切入点库或其他 AspectJ 类似功能的人当然可以使用 Aspect 对象和方法组合装饰器作为基础,构建更具表达力的 AOP 工具。
- XXX 详细说明完整的切面 API,包括键、N 对 1 切面、手动
- 切面实例的附加/分离/删除,以及
IAspectOwner接口。
扩展 API
待办事项:解释所有这些是如何工作的
implies(o1, o2)
declare_implementation(iface, class)
predicate_signatures(ob)
parse_rule(ruleset, body, predicate, actiontype, localdict, globaldict)
combine_actions(a1, a2)
rules_for(f)
规则对象
ActionDef 对象
RuleSet 对象
方法对象
MethodList 对象
IAspectOwner
重载使用模式
在 Python-3000 列表上的讨论中,允许任意函数重载的提议功能引起了一些争议,一些人担心这会使程序更难以理解。
这个论点的主要观点是,如果函数可以随时在程序的任何地方更改,就不能依赖它的行为。尽管原则上这可以通过猴子补丁或代码替换来实现,但这被认为是一种不良实践。
然而,提供对任何函数进行重载的支持(或者说,该论点认为),就隐含地认可了此类更改是一种可接受的做法。
这个论点在理论上似乎有道理,但在实践中几乎完全没有意义,原因有二。
首先,人们通常不会是反常的,在一个地方定义一个函数做一件事,然后又草率地在另一个地方定义它做相反的事!扩展一个 没有 被特别泛化过的函数行为的主要原因是:
- 添加原始函数作者未考虑的特殊情况,例如对附加类型的支持。
- 接收到操作通知,以便在原始操作执行之前、之后或两者兼有,执行一些相关的操作。这可以包括添加日志、计时或追踪等通用操作,以及应用程序特定的行为。
然而,这些添加重载的原因都并不意味着改变现有函数的预期默认或总体行为。就像基类方法可以出于这两个相同的原因被子类覆盖一样,函数也可以被重载以提供此类增强。
换句话说,通用重载并不等同于 任意 重载,因为我们不需要期望人们以不合逻辑或不可预测的方式随意重新定义现有函数的行为。如果他们这样做,那将不比任何其他编写不合逻辑或不可预测代码的方式更糟!
然而,为了区分不良实践和良好实践,或许有必要进一步阐明定义重载的良好实践 是 什么。这让我们想到通用函数不一定会使程序更难理解的第二个原因:实际程序中的重载模式往往遵循非常可预测的模式。(在 Python 和没有 非 通用函数的语言中都是如此。)
如果一个模块正在定义一个新的通用操作,它通常也会在同一位置为现有类型定义任何所需的重载。同样,如果一个模块正在定义一种新类型,那么它通常会在该处为它所知或关心的任何通用函数定义重载。
因此,绝大多数重载都可以在被重载函数或新定义类型附近找到,因为重载是为该新类型添加支持的。因此,在常见情况下,重载具有高度可发现性,因为您要么在查看函数,要么在查看类型,或两者兼而有之。
只有在极少数情况下,一个模块中会存在既不包含函数也不包含重载所添加类型的重载。例如,如果第三方在某个库的类型和另一个库的通用函数之间创建了支持桥梁。然而,在这种情况下,最佳实践建议明确宣传这一点,特别是通过模块名称。
例如,PyProtocols 通过使用名为 protocols.twisted_support 和 protocols.zope_support 的模块,为 Zope 接口和传统的 Twisted 接口定义了此类桥接支持。(这些桥接是通过接口适配器而不是通用函数完成的,但基本原理是相同的。)
简而言之,在通用重载存在的情况下,理解程序并不需要变得更加困难,因为绝大多数重载将位于函数附近,或者位于传递给该函数类型的定义附近。
而且,在没有无能或故意晦涩意图的情况下,少数不与相关类型或函数相邻的重载,通常不需要在定义这些重载的范围之外被理解或知晓。(除了“支持模块”的情况,在这种情况下,最佳实践建议相应地命名它们。)
实现说明
本 PEP 中描述的大多数功能已经在 PEAK-Rules 框架的开发版本中实现。特别是,基本的重载和方法组合框架(减去 @overload 装饰器)已经存在。撰写本文时,peak.rules.core 中所有这些功能的实现有 656 行 Python 代码。
peak.rules.core 目前依赖于 DecoratorTools 和 BytecodeAssembler 模块,但这两个依赖都可以替换,因为 DecoratorTools 主要用于 Python 2.3 兼容性并实现结构类型(在更高版本的 Python 中可以用命名元组完成)。在合理努力下,BytecodeAssembler 的使用可以通过“exec”或“compile”变通方法替换。(如果函数对象的 func_closure 属性可写,则更容易做到这一点。)
Interface 类以前曾被原型化,但目前未包含在 PEAK-Rules 中。
“隐式类规则”以前已在 RuleDispatch 库中实现。然而,它依赖于目前在 PEP 3115 中已取消的 __metaclass__ 钩子。
我目前不知道如何让 @overload 在类体中与 classmethod 和 staticmethod 很好地协同工作。然而,目前还不清楚是否需要这样。
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3124.rst
最后修改: 2025-02-01 08:59:27 GMT