Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 模块中的大多数函数。但是,它目前

  1. 没有提供简单或直接的方法供开发人员创建新的泛型函数,
  2. 没有提供标准方法为现有泛型函数添加方法(即,有些方法使用注册函数添加,其他方法需要定义 __special__ 方法,可能通过猴子补丁),并且
  3. 不允许根据多个参数类型进行分派(除了算术运算符的有限形式,其中“右端”(__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() 函数可以通过添加更多重载来扩展,而硬编码版本无法扩展。

例如,如果有人想使用 flatten() 和一个不继承 basestring 的字符串类型,那么第二个实现将无法满足需求。但是,使用重载实现,他们可以编写以下代码

@overload
def flatten(ob: MyString):
    yield ob

或以下代码(以避免复制实现)

from overloading import RuleSet
RuleSet(flatten).copy_rules((basestring,), (MyString,))

(还要注意,尽管PEP 3119 提出应该允许像 Iterable 这样的抽象基类允许像 MyString 这样的类声明子类关系,但这种声明是全局的,遍布整个应用程序。相比之下,添加特定重载或复制规则特定于单个函数,因此不太可能产生不希望的副作用。)

@overload vs. @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 绑定到它之前绑定的任何内容。第二个也将执行相同的操作,如果它已绑定到 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,但 S2 不暗示 S1,则签名 S1 比另一个签名 S2“更具体”。

尽管以上示例都使用具体类型或抽象类型作为参数注释,但没有要求注释必须是此类类型。它们也可以是“接口”对象(在接口和适配部分中讨论),包括用户定义的接口类型。(它们也可以是通过扩展 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__ 是否为错误实例,或者简单地调用它。 NoApplicableMethodsAmbiguousMethods 错误类有一个共同的 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”方法不会导致任何错误。歧义是使用将方法添加到目标函数的顺序来解决的。

“Before”方法首先调用最具体的那个方法,模棱两可的方法按添加顺序执行。所有“before”方法都在函数的任何“primary”方法(即正常的 @overload 方法)执行之前调用。

“After”方法以**相反**的顺序调用,在函数的所有“primary”方法执行之后。也就是说,它们首先执行最不具体的那个方法,模棱两可的方法按添加顺序的反序执行。

“before”和“after”方法的返回值将被忽略,任何由**任何**方法(primary或其他)引发的未捕获异常都会立即结束分派过程。“Before”和“after”方法不能具有 __proceed__ 参数,因为它们不负责调用任何其他方法。它们只是在primary方法之前或之后作为通知被调用。

因此,“before”和“after”方法可以用来检查或建立先决条件(例如,如果条件不满足则引发错误)或确保后置条件,而无需复制任何现有功能。

“环绕”方法

@around 装饰器将方法声明为“around”方法。“Around”方法非常类似于primary方法,除了最不具体的“around”方法比最具体的“before”方法具有更高的优先级。

但是,与“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__ 函数。

传递给“around”方法的 __proceed__ 将是下一个适用的“around”方法、一个 DispatchError 实例,或者一个合成方法对象,该对象将调用所有“before”方法,然后调用primary方法链,然后调用所有“after”方法,并返回primary方法链的结果。

因此,就像普通方法一样,可以检查 __proceed__ 是否为 DispatchError,或者简单地调用它。“around”方法应该返回 __proceed__ 返回的值,除非它希望修改或替换它,以作为整个函数的不同返回值。

自定义组合

上面描述的装饰器(@overload@when@before@after@around)共同实现了 CLOS 中所谓的“标准方法组合”——组合方法中最常用的模式。

但是,有时应用程序或库可能需要更复杂类型的方法组合。例如,如果您希望拥有返回折扣百分比的“discount”方法,以从primary方法返回的值中减去,您可以编写如下代码:

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() 方法。

为了提高效率,调用 IStack(s)(其中 sStack 的实例),**可能**会返回 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 “更具体”。

因此,例如,ISizedStackISizableISizedStack 都更具体,而不管这些接口之间的继承关系如何。这纯粹是一个关于哪些操作包含在这些接口中的问题——并且操作的 *名称* 不重要。

接口(至少是 overloading 提供的接口)始终被认为比具体类不那么具体。其他接口实现可以自行决定其具体性规则,包括接口和接口之间,以及接口和类之间。

接口中的非方法属性

Interface 实现实际上以相同的方式处理所有属性和方法(即描述符):它们的 __get__(以及 __set____delete__,如果存在)方法被调用,并将包装的(适配的)对象作为“self”。对于函数,这将创建将泛型函数链接到包装对象的绑定方法。

对于非函数属性,使用内置的 property 和相应的 fgetfsetfdel 属性来指定它们可能最容易。

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 类,以便轻松地将额外信息附加到以下任一对象

  1. 具有 __dict__ 属性(因此方面实例可以存储在其中,以方面类为键),
  2. 支持弱引用(因此方面实例可以使用全局但线程安全的弱引用字典进行管理),或者
  3. 实现或可以适配到 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 的实例上成功调用的次数(即,除非错误发生在更具体的“after”方法中,否则它不会计算错误)。其他代码然后可以使用 Count(someTarget).count 访问计数。

Aspect 实例当然可以具有 __init__ 方法,以初始化任何数据结构。它们可以使用 __slots__ 或基于字典的属性进行存储。

虽然与 AspectJ 这样的功能齐全的 AOP 工具相比,此功能相当原始,但希望构建切点库或其他类似 AspectJ 的功能的人员当然可以使用 Aspect 对象和方法组合装饰器作为构建更具表现力的 AOP 工具的基础。

XXX 指定完整的方面 API,包括键、N 对 1 方面、手动
附加/分离/删除方面实例,以及 IAspectOwner 接口。

扩展 API

TODO:解释所有这些是如何工作的

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 定义了用于处理 Zope 接口和旧版 Twisted 接口的这种桥接支持,使用名为protocols.twisted_supportprotocols.zope_support的模块。(这些桥接是使用接口适配器而不是泛型函数完成的,但基本原理相同。)

简而言之,在存在泛型重载的情况下,理解程序并不一定更困难,因为绝大多数重载要么与函数相邻,要么与传递给该函数的类型的定义相邻。

并且,在没有不称职或故意模糊的情况下,那些与相关类型或函数不相邻的少数重载通常不需要在定义这些重载的范围之外理解或了解。(除了“支持模块”的情况,最佳实践建议相应地命名它们。)

实现说明

本 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__钩子。

我目前不知道如何在类体中使@overloadclassmethodstaticmethod很好地配合使用。但是,目前还不清楚它是否需要这样做。


来源:https://github.com/python/peps/blob/main/peps/pep-3124.rst

上次修改时间:2023-09-09 17:39:29 GMT