如何使用 singledispatchmethod 重载 __new__,并且为什么它不能按预期工作?

0 投票
1 回答
52 浏览
提问于 2025-04-14 17:08

我想让我的 __new__ 方法在某些情况下表现得不一样,所以我想把它分成几个重载函数,使用 singledispatchmethod。但是这样做不行,重载的函数根本没有被调用。这是什么原因呢?

from functools import singledispatchmethod

class Foo:
     @singledispatchmethod
     def __new__(cls, arg1, **kwargs):
         return "default call"

     @__new__.register(int)
     def _(cls, arg1, **kwargs):
         return "called with int " + str(arg1)

print(Foo("hi"))
# default call
print(Foo(1))
# default call

我还尝试用 singledispatch 来实验,但也没有成功。

1 个回答

3

你可能会觉得 singledispatchmethod 只是一个版本的 singledispatch,它是根据第二个参数来分发的,而不是第一个,但其实并不是这样。

实际上,它是作为一个类来写的,这个类实现了 描述符协议,用来定制属性的访问。当你访问一个被 singledispatchmethod 装饰的方法时,属性访问会返回一个闭包对象,这个闭包对象会根据传给它的第一个参数来进行分发。

举个例子,如果有一个名为 Example 的类,它有一个叫 sdmsingledispatchmethod,那么 Example().sdm(1) 会根据数字 1 的类型来分发,但 Example.sdm(Example(), 1) 会根据 Example() 的类型来分发!


__new__ 不是一个普通的方法。它应该是一个静态方法,或者至少在访问时表现得像一个静态方法。(通常情况下,type.__new__ 会自动把 __new__ 方法转换为静态方法,但这只在 __new__ 是一个普通的 Python 函数对象时才会发生,而如前所述,singledispatchmethod 是作为一个自定义类实现的。)

具体来说,当你执行 Foo(1) 时,得到的 __new__ 调用就像是 Foo.__new__(Foo, 1)。它会在 Foo 上获取 __new__ 属性,然后用 Foo1 作为参数调用它找到的内容。

由于 singledispatchmethod 的分发方式,它是根据 Foo 的类型来分发,而不是根据 1 的类型。


通常,如果你想要 singledispatchmethod 有类似静态方法的行为,获取这种行为的方法是利用 singledispatchmethod 的一个特性,它可以包装其他装饰器,像这样:

# This doesn't do what you need.

@functools.singledispatchmethod
@staticmethod
def __new__(cls, arg1, **kwargs):
    ...

@__new__.register(int)
@staticmethod
def _(cls, arg1, **kwargs):
    ...

不过,这并不能解决我们要根据哪个参数进行分发的问题。

相反,你可以把 __new__ 写成一个普通的方法,然后委托给一个 singledispatch 辅助函数,并重新排列辅助函数的参数,把你想要分发的参数放在前面:

import functools

class Foo:
    def __new__(cls, arg1, **kwargs):
        return _new_helper(arg1, cls, **kwargs)

@functools.singledispatch
def _new_helper(arg1, cls, **kwargs):
    return "default call"

@_new_helper.register(int)
def _(arg1, cls, **kwargs):
    return "called with int " + str(arg1)

或者,当然,你也可以完全放弃 singledispatch 的用法,自己手动处理分发:

import functools

class Foo:
    def __new__(cls, arg1, **kwargs):
        if isinstance(arg1, int):
            return "called with int " + str(arg1)
        else:
            return "default call"

撰写回答