6

在 Python 中,允许用户在不更改函数原始代码的情况下扩展函数可以操作的类型的正确方法是什么?

假设我有一个模块,它的my_module.foo()函数最初是为处理float类型而编写的。现在,我希望相同的函数也能够与mpmath任意精度浮点数一起工作 - 但不改变原始模块中的代码。

在 C++ 中,我会添加一个额外的重载(或者,更有可能是一些带有辅助结构的模板专业化技巧)。我应该如何构造原始my_module.foo()代码,以便用户可以在其中添加自己的自定义挂钩?

我可以想出一些方法来实现这一点,但作为一个新手 Python 程序员,我相信他们中的大多数都会很可怕:)

编辑:感谢到目前为止的所有答案,非常感谢。

我可能应该澄清一个关键要求是能够处理我自己没有定义的类型。例如,如果我试图cos在我的模块中编写一个通用函数,我想调用math.cos内置类型、mpmath.cos类型mpfsympy.cossympy 符号类型等。当然我希望调度逻辑不在我的模块的cos实现中。

4

3 回答 3

7

有两种方法可以做到这一点:

  • 代表团。已经在 Python 中了。最Pythonic。不适用于内置类型。不完全是你要找的。
  • 单次派遣。仍然是PEP。适用于内置类型。正是您正在寻找的东西。

代表团

您通常将责任委托给您正在操作的对象,并且您不会在函数中实现逻辑。

这是一个例子:len。的实现len非常简单:

def len(obj):
    return obj.__len__()

不同的类型 ( str, list, tuple...) 有不同的实现,但它们都使用相同的功能。

现在,如果我想定义我自己的类型,它适用于len,我可以这样做:

class MyTypeOfLength3(object):
    def __len__(self):
        return 3

o = MyTypeOfLength3()
print len(o) 
# 3

在您的情况下,您将实现看起来像len.

(注意:这不是 的实际代码len,但或多或​​少是等价的。)

单次派遣

当然,在某些情况下,这可能不切实际。如果这是您的情况,那么“Single Dispatch”PEP 443可能就是您正在寻找的。

它建议使用一个新的装饰器来完成你正在寻找的东西:

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)
...
>>> @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)

一旦你这样定义了你的函数,你可以调用fun(something),Python 会找到正确的实现(intlist这里),回退到默认实现def fun(...): ...

因此,您只需要装饰您的原始函数,就完成了,您的用户可以添加他们自己的类型。

注意:正如评论中指出的,singledispatch已经在 Python 中实现,它是pkgutil.simplegeneric

于 2013-09-23T10:58:29.970 回答
1

无需等待PEP 443 -通过使用Python 2.6 中添加的抽象基类来实现单调度泛型函数,就可以做你想做的事。这些允许您创建“虚拟”元类并即时向它们添加任意子类,而无需更改现有代码或对其进行猴子修补。然后,您的模块可以使用在此元类中注册的类型来确定要做什么。您(或其他类型的作者)可以根据需要进行注册。

这是说明该概念的示例代码:

import abc

class Trigonometric(object):
    __metaclass__ = abc.ABCMeta
    _registry = {}

    @classmethod
    def register(cls, subclass, cos_func, sin_func):
        cls.__metaclass__.register(cls, subclass)
        if subclass not in cls._registry:  # may or may not want this check...
            cls._registry[subclass] = {'cos': cos_func, 'sin': sin_func}

    @classmethod
    def call_func(cls, func_name, n):
        try:
            return cls._registry[n.__class__][func_name](n)
        except KeyError:
            raise RuntimeError(
                "Either type {} isn't registered or function {}() "
                "isn't known.".format(n.__class__.__name__, func_name))

# module-level functions
def cos(n):
    return Trigonometric.call_func('cos', n)

def sin(n):
    return Trigonometric.call_func('sin', n)

if __name__ == '__main__':
    # avoid hardcoding this module's filename into the source
    import sys
    my_module = sys.modules[__name__]  # replaces import my_module

    # register the built-in float type
    import math
    print 'calling Trigonometric.register(float)'
    Trigonometric.register(float, math.cos, math.sin)

    # register mpmath's arbitrary-precision mpf float type
    from mpmath import mp
    print 'calling Trigonometric.register(mp.mpf)'
    Trigonometric.register(mp.mpf, mp.cos, mp.sin)

    f = 1.0
    print 'isinstance(f, Trigonometric):', isinstance(f, Trigonometric)
    print 'my_module.cos(f):', my_module.cos(f), my_module.sin(f)

    v = mp.mpf(1)
    print 'isinstance(v, Trigonometric):', isinstance(v, Trigonometric)
    print 'my_module.cos(v):', my_module.cos(v), my_module.sin(v)
于 2013-09-23T17:19:59.923 回答
0

这取决于代码和预期结果。通常你不应该隐式指定数据类型。使用鸭子打字

但是如果一个函数只需要浮点数,你可以包装它。有一个最简单的例子:

def bar(data):
    """Execute my_module.foo function with data converted to float"""
    return my_module.foo(float(data))
于 2013-09-23T11:15:17.127 回答