【问题标题】:Decorator for overloading in PythonPython中重载的装饰器
【发布时间】:2012-01-29 02:13:24
【问题描述】:

我知道编写关心参数类型的函数不是 Pythonic,但在某些情况下根本不可能忽略类型,因为它们的处理方式不同。

在你的函数中有一堆 isinstance 检查只是丑陋的;是否有任何可用的函数装饰器可以启用函数重载?像这样的:

@overload(str)
def func(val):
    print('This is a string')

@overload(int)
def func(val):
    print('This is an int')

更新:

这是我在David Zaslavsky's answer留下的一些cmets:

经过一些修改[s],这将非常适合我的目的。我在您的实现中注意到的另一个限制,因为您使用 func.__name__ 作为字典键,您很容易在模块之间发生名称冲突,这并不总是可取的。 [继续]

[cont.] 例如,如果我有一个模块重载func,而另一个完全不相关的模块也重载func,这些重载将发生冲突,因为函数 dispatch dict 是全局的。该 dict 应该以某种方式对模块进行本地化。不仅如此,它还应该支持某种“继承”。 [继续]

[继续] '继承'我的意思是:假设我有一个模块first 有一些重载。然后是另外两个不相关但每个导入first 的模块;这两个模块都为它们刚刚导入的现有模块添加了新的重载。这两个模块应该能够使用first 中的重载,但是它们刚刚添加的新模块不应在模块之间相互冲突。 (现在想想,这实际上很难做到。)

其中一些问题可以通过稍微改变装饰器语法来解决:

first.py

@overload(str, str)
def concatenate(a, b):
    return a + b

@concatenate.overload(int, int)
def concatenate(a, b):
    return str(a) + str(b)

second.py

from first import concatenate

@concatenate.overload(float, str)
def concatenate(a, b):
    return str(a) + b

【问题讨论】:

  • 嗯...所以在您的编辑中,contatenate.overload 中的 first.py 到底是什么意思?如所写,这将尝试访问 concatenate 函数的 overload 属性,本示例中不存在该属性。
  • @DavidZaslavsky 函数的第一个重载应该用@overload 修饰,这将返回一个具有overload 属性的可调用对象。所有后续的重载都应该用这个已经存在的对象@object.overload 装饰,所以每个对象只有一个字典,而不是全局字典。 (它的工作方式类似于标准的@property。)我将编写一个实现并在发布时通知您。
  • 这将背离传统的重载语法。但是,如果这就是您想要的,请查看overload package(我将在我的答案中进行编辑)。它基本上使用了这种方法。

标签: python overloading decorator


【解决方案1】:

从 Python 3.4 开始,functools 模块现在支持 @singledispatch 装饰器。它的工作原理是这样的:

from functools import singledispatch


@singledispatch
def func(val):
    raise NotImplementedError


@func.register
def _(val: str):
    print('This is a string')


@func.register
def _(val: int):
    print('This is an int')

用法

func("test") --> "This is a string"
func(1) --> "This is an int"
func(None) --> NotImplementedError

【讨论】:

    【解决方案2】:

    快速回答: PyPI 上有一个overload package,它比我在下面描述的更稳健地实现了这一点,尽管使用的语法略有不同。它被声明为仅适用于 Python 3,但看起来只需要稍作修改(如果有的话,我还没有尝试过)就可以使其适用于 Python 2。


    长答案:在可以重载函数的语言中,函数的名称(无论是字面上还是有效地)都通过有关其类型签名的信息进行扩充,无论是在定义函数时还是在定义函数时叫做。当编译器或解释器查找函数定义时,它使用声明的名称和参数类型来解析要访问的函数。所以在 Python 中实现重载的逻辑方法是实现一个包装器,它使用声明的名称和参数类型来解析函数。

    这是一个简单的实现:

    from collections import defaultdict
    
    def determine_types(args, kwargs):
        return tuple([type(a) for a in args]), \
               tuple([(k, type(v)) for k,v in kwargs.iteritems()])
    
    function_table = defaultdict(dict)
    def overload(arg_types=(), kwarg_types=()):
        def wrap(func):
            named_func = function_table[func.__name__]
            named_func[arg_types, kwarg_types] = func
            def call_function_by_signature(*args, **kwargs):
                return named_func[determine_types(args, kwargs)](*args, **kwargs)
            return call_function_by_signature
        return wrap
    

    overload 应该使用两个可选参数调用,一个元组表示所有位置参数的类型,一个元组表示所有关键字参数的名称类型映射。这是一个使用示例:

    >>> @overload((str, int))
    ... def f(a, b):
    ...     return a * b
    
    >>> @overload((int, int))
    ... def f(a, b):
    ...     return a + b
    
    >>> print f('a', 2)
    aa
    >>> print f(4, 2)
    6
    
    >>> @overload((str,), (('foo', int), ('bar', float)))
    ... def g(a, foo, bar):
    ...     return foo*a + str(bar)
    
    >>> @overload((str,), (('foo', float), ('bar', float)))
    ... def g(a, foo, bar):
    ...     return a + str(foo*bar)
    
    >>> print g('a', foo=7, bar=4.4)
    aaaaaaa4.4
    >>> print g('b', foo=7., bar=4.4)
    b30.8
    

    这样的缺点包括

    • 它实际上并没有检查装饰器所应用的函数是否与提供给装饰器的参数兼容。你可以写

      @overload((str, int))
      def h():
          return 0
      

      当函数被调用时你会得到一个错误。

    • 它不能优雅地处理不存在与传递的参数类型相对应的重载版本的情况(这将有助于引发更具描述性的错误)

    • 它区分命名参数和位置参数,所以类似于

      g('a', 7, bar=4.4)
      

      没用。

    • 在使用它时涉及到很多嵌套括号,如g 的定义。
    • 如 cmets 中所述,这不涉及不同模块中具有相同名称的函数。

    我认为,所有这些都可以通过足够的摆弄来弥补。特别是,通过将调度表存储为从装饰器返回的函数的属性,可以轻松解决名称冲突问题。但正如我所说,这只是一个简单的示例,用于演示如何操作的基础知识。

    【讨论】:

    • 经过一些修改,这将非常适合我的目的。我在您的实现中注意到的另一个限制,因为您使用 func.__name__ 作为字典键,您很容易在模块之间发生名称冲突,这并不总是可取的。 [续]
    • [cont.] 例如,如果我有一个模块重载func,而另一个完全不相关 模块也重载func,这些重载会发生冲突,因为函数 dispatch dict 是全局的。该 dict 应该以某种方式对模块进行本地化。不仅如此,它还应该支持某种“继承”。 [续]
    • [cont.] 通过“继承”我的意思是:假设我有一个模块 first 有一些重载。然后是另外两个不相关的模块,但每个模块都导入first;这两个模块都为它们刚刚导入的现有模块添加了新的重载。这两个模块应该能够使用first 中的重载,但是它们刚刚添加的新模块不应在模块之间相互冲突。 (现在想想,这实际上很难做到。)
    • 快速回答:嘿,谢谢,我不知道已经有这个实现了。
    • @PaulManta:实际上,直到一个小时前我也没有 ;-) 虽然我可能一开始就应该考虑查找它。
    【解决方案3】:

    这并不能直接回答您的问题,但是如果您真的想要一些行为类似于不同类型的重载函数并且(非常正确)不想使用 isinstance,那么我建议您这样做:

    def func(int_val=None, str_val=None):
        if sum(x != None for x in (int_val, str_val)) != 1:
            #raise exception - exactly one value should be passed in
        if int_val is not None:
            print('This is an int')
        if str_val is not None:
            print('This is a string')
    

    在使用中意图很明显,甚至不需要不同的选项有不同的类型:

    func(int_val=3)
    func(str_val="squirrel")
    

    【讨论】:

    • 好吧,如果您有很多可能性,那么您可以改用**kwargs 并使其更具程序性。我已经在具有 17 个允许关键字的构造函数中使用了它,它对我来说效果很好。
    猜你喜欢
    • 2013-06-07
    • 2012-02-09
    • 1970-01-01
    • 2012-04-28
    • 1970-01-01
    • 1970-01-01
    • 2013-07-19
    • 2012-07-15
    相关资源
    最近更新 更多