如何将functools.singledispatch与实例方法一起使用?

62 投票
3 回答
29585 浏览
提问于 2025-04-18 12:18

Python 3.4 新增了一个功能,可以用静态方法来定义函数重载。简单来说,这就是文档中的一个例子:

from functools import singledispatch


class TestClass(object):
    @singledispatch
    def test_method(arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    @test_method.register(int)
    def _(arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    @test_method.register(list)
    def _(arg):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, elem)

if __name__ == '__main__':
    TestClass.test_method(55555)
    TestClass.test_method([33, 22, 11])

在最基本的情况下,singledispatch 的实现依赖于第一个参数来识别类型,这就让它在扩展到实例方法时变得有点棘手。

有没有人能给点建议,如何使用(或者说是临时拼凑)这个功能,让它能在实例方法中也能正常工作?

3 个回答

6

在Python 3.8中,functools模块引入了一种新的装饰器@singledispatchmethod,可以让实例方法支持函数重载。

根据官方文档的说明:

方法的选择是基于第一个非self或非cls参数的类型。

这意味着,紧跟在self后面的那个参数的类型,会决定调用哪个版本的方法。可以参考下面的示例。

from functools import singledispatchmethod


class Cooking:
    @singledispatchmethod
    def cook(self, arg):
        return f"I'm cooking {arg} eggs."

    @cook.register
    def _(self, arg: int):
        return f"I'm cooking {arg} eggs."

    @cook.register
    def _(self, arg: bool):
        return f"Am I cooking eggs? {arg}"

f = Cooking()
print(f.cook('many'))
# I'm cooking many eggs.
print(f.cook(50))
# I'm cooking 50 eggs.
print(f.cook(True))
# Am I cooking eggs? True

14

装饰器其实就是一个包装器,它把被包装的函数当作参数,然后返回另一个函数。

正如被接受的答案中所说,singledispatch 返回一个 wrapper,这个包装器把第一个参数当作注册的类型 - 在实例方法中是 self

在那个答案中提到的情况,你可以写另一个包装器来修改装饰器的行为。但这种“黑客”式的解决方案并不总是最佳选择。

和其他函数一样,你可以直接调用这个包装器,并把参数传给它。如果这种方法重载在一个包中很少使用,我觉得这样做更简单、更直接,也更容易理解。

from functools import singledispatch

class TestClass(object):

    def __init__(self):
        self.test_method = singledispatch(self.test_method)
        self.test_method.register(int, self._test_method_int)
        self.test_method.register(list, self._test_method_list)

    def test_method(self, arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    def _test_method_int(self, arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    def _test_method_list(self, arg):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, elem)


if __name__ == '__main__':
    test = TestClass()
    test.test_method(55555)
    test.test_method([33, 22, 11])

还有一个模块,叫做 multipledispatch(虽然不是标准库,但在 Anaconda 中包含,并且没有任何非标准的依赖),顾名思义,它允许多方法。

除了 Dispatcher 对象外,它还提供了一个 dispatch 装饰器,这个装饰器让用户不需要关心这些对象的创建和管理。

这个 dispatch 装饰器使用函数的名称来选择合适的 Dispatcher 对象,并将新的签名/函数添加到其中。当它遇到一个新的函数名时,会创建一个新的 Dispatcher 对象,并将名称和 Dispatcher 的配对存储在命名空间中,以备将来使用。

例如:

from types import LambdaType
from multipledispatch import dispatch

class TestClass(object):

    @dispatch(object)
    def test_method(self, arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    @dispatch(int, float)
    def test_method(self, arg, arg2):
        print("Strength in numbers, eh?", end=" ")
        print(arg + arg2)

    @dispatch((list, tuple), LambdaType, type)
    def test_method(self, arg, arg2, arg3):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, arg3(arg2(elem)))


if __name__ == '__main__':

    test = TestClass()
    test.test_method(55555, 9.5)
    test.test_method([33, 22, 11], lambda x: x*2, float)
94

更新: 从 Python 3.8 开始,functools.singledispatchmethod 允许对方法、类方法、抽象方法和静态方法进行单一分发。

对于旧版本的 Python,请查看本答案的其余部分。

查看 singledispatch 的源代码,我们可以看到这个装饰器返回了一个函数 wrapper(),它根据 args[0] 的类型选择要调用的函数……

    def wrapper(*args, **kw):
        return dispatch(args[0].__class__)(*args, **kw)

……这对于普通函数来说没问题,但对于实例方法就不太适用了,因为实例方法的第一个参数总是 self

不过,我们可以写一个新的装饰器 methdispatch,它依赖于 singledispatch 来处理复杂的部分,但返回一个包装函数,根据 args[1] 的类型选择要调用的注册函数:

from functools import singledispatch, update_wrapper

def methdispatch(func):
    dispatcher = singledispatch(func)
    def wrapper(*args, **kw):
        return dispatcher.dispatch(args[1].__class__)(*args, **kw)
    wrapper.register = dispatcher.register
    update_wrapper(wrapper, func)
    return wrapper

下面是这个装饰器使用的一个简单例子:

class Patchwork(object):

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    @methdispatch
    def get(self, arg):
        return getattr(self, arg, None)

    @get.register(list)
    def _(self, arg):
        return [self.get(x) for x in arg]

注意,装饰过的 get() 方法和注册给 list 的方法都有一个初始的 self 参数,和往常一样。

测试 Patchwork 类:

>>> pw = Patchwork(a=1, b=2, c=3)
>>> pw.get("b")
2
>>> pw.get(["a", "c"])
[1, 3]

撰写回答