使用户可以扩展功能的正确方法

时间:2013-09-23 10:52:07

标签: python mpmath

允许用户在不改变函数原始代码的情况下扩展函数可以运行的类型的正确方法是什么?

假设我有一个my_module.foo()函数的模块,该模块最初编写为适用于float类型。现在我希望同样的函数能够使用mpmath任意精度浮点数,但不改变原始模块中的代码。

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

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

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

我应该澄清一个关键要求是能够应对我自己没有定义的类型。例如,如果我尝试在我的模块中编写通用cos函数,我想在内置类型上调用math.cosmpmath.cos类型调用mpf,{{1}当然,我希望调度逻辑不在我的模块的sympy.cos实现中。

3 个答案:

答案 0 :(得分:7)

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

  • 即可。已经在Python中了。大多数Pythonic。不适用于内置类型。不完全是你想要的。
  • 单一调度。还是一个PEP。适用于内置类型。正是你要找的东西。

您通常将责任委托给您正在执行的对象,并且您没有在您的函数中实现逻辑。

以下是一个示例:lenlen的实施是非常明确的:

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

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

现在,如果我想定义自己的类型,可以使用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

答案 1 :(得分:1)

可以通过使用在Python 2.6中添加的PEP 443 - Single-dispatch generic functions来等待abstract base classes来实现您想要的功能。这些允许您创建"虚拟"元类,并在不改变现有代码或猴子修补的情况下动态添加任意子类。然后,您的模块可以使用在此元类中注册的类型来确定要执行的操作。您(或其他类型的作者)可以根据需要注册它们。

以下是说明这一概念的示例代码:

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)

答案 2 :(得分:0)

这取决于代码和预期结果。通常,您不应隐式指定数据类型。使用Duck Typing

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

def bar(data):
    """Execute my_module.foo function with data converted to float"""
    return my_module.foo(float(data))