Python中不同参数类型的方法重载

25 投票
5 回答
22151 浏览
提问于 2025-04-18 17:31

我正在用Python写一个预处理器,其中一部分是处理抽象语法树(AST)。

里面有一个叫做render()的方法,负责把各种语句转换成源代码。

现在我的代码是这样的(简化版):

def render(self, s):
    """ Render a statement by type. """

    # code block (used in structures)
    if isinstance(s, S_Block):
        # delegate to private method that does the work
        return self._render_block(s)

    # empty statement
    if isinstance(s, S_Empty):
        return self._render_empty(s)

    # a function declaration
    if isinstance(s, S_Function):
        return self._render_function(s)

    # ...

如你所见,这样写起来很繁琐,容易出错,而且代码也很长(我还有很多其他类型的语句)。

理想的解决方案应该是这样的(用Java的语法):

String render(S_Block s)
{
    // render block
}

String render(S_Empty s)
{
    // render empty statement
}

String render(S_Function s)
{
    // render function statement
}

// ...

当然,Python不能这样做,因为它是动态类型的。当我查找如何模拟方法重载时,所有的答案都说“你在Python中不想这样做”。我想在某些情况下确实是这样,但在这里kwargs根本没什么用。

我该如何在Python中做到这一点,而不需要像上面那样冗长的类型检查?另外,最好是用一种“Pythonic”的方式来实现。

注意:可能会有多个“Renderer”实现,它们以不同的方式渲染语句。因此,我不能把渲染代码移动到语句中,只是调用s.render()。这必须在渲染器类中完成。

(我找到了一些有趣的“访问者”代码,但我不确定这是否真的是我想要的东西)。

5 个回答

2

为了给@unutbu的回答增加一些性能测量的内容:

@multimethod(float)
def foo(bar: float) -> str:
    return 'float: {}'.format(bar)

def foo_simple(bar):
    return 'string: {}'.format(bar)

import time

string_type = "test"
iterations = 10000000

start_time1 = time.time()
for i in range(iterations):
    foo(string_type)
end_time1 = time.time() - start_time1


start_time2 = time.time()
for i in range(iterations):
    foo_simple(string_type)
end_time2 = time.time() - start_time2

print("multimethod: " + str(end_time1))
print("standard: " + str(end_time2))

返回结果:

> multimethod: 16.846999883651733
> standard:     4.509999990463257
4

这里有一个不同的实现方式,使用了 functools.singledispatch,并且用到了在 PEP-443 中定义的装饰器:

from functools import singledispatch

class S_Unknown: pass
class S_Block: pass
class S_Empty: pass
class S_Function: pass
class S_SpecialBlock(S_Block): pass

@singledispatch
def render(s, **kwargs):
  print('Rendering an unknown type')

@render.register(S_Block)
def _(s, **kwargs):
  print('Rendering an S_Block')

@render.register(S_Empty)
def _(s, **kwargs):
  print('Rendering an S_Empty')

@render.register(S_Function)
def _(s, **kwargs):
  print('Rendering an S_Function')

if __name__ == '__main__':
  for t in [S_Unknown, S_Block, S_Empty, S_Function, S_SpecialBlock]:
    print(f'Passing an {t.__name__}')
    render(t())

这个实现的输出是:

Passing an S_Unknown
Rendering an unknown type
Passing an S_Block
Rendering an S_Block
Passing an S_Empty
Rendering an S_Empty
Passing an S_Function
Rendering an S_Function
Passing an S_SpecialBlock
Rendering an S_Block

我觉得这个版本比用 map 的那个更好,因为它的行为和使用 isinstance() 的实现是一样的:当你传入一个 S_SpecialBlock 时,它会把这个对象传给处理 S_Block 的渲染器。

可用性

正如 dano 在 另一个回答 中提到的,这个方法在 Python 3.4 及以上版本中有效,并且有一个 回溯版本 可以在 Python 2.6 及以上版本中使用。

如果你使用的是 Python 3.7 及以上版本,register() 属性支持使用类型注解:

@render.register
def _(s: S_Block, **kwargs):
  print('Rendering an S_Block')

注意

我看到的一个问题是,你必须把 s 作为位置参数传入,这意味着你不能这样写 render(s=S_Block())

因为 single_dispatch 是通过第一个参数的类型来决定调用哪个版本的 render(),所以这样会导致一个类型错误 - “render 至少需要一个位置参数”(参考 源代码

其实,我觉得如果只有一个参数的话,使用关键字参数应该是可以的……如果你真的需要这样做,可以参考 这个回答,它创建了一个带有不同包装器的自定义装饰器。这也是 Python 的一个不错的功能。

15

你想要的重载语法可以通过Guido van Rossum的多方法装饰器来实现。

这里有一个多方法装饰器的变体,它可以装饰类的方法(原来的装饰器只能装饰普通函数)。我把这个变体命名为multidispatch,以便和原来的区分开来:

import functools

def multidispatch(*types):
    def register(function):
        name = function.__name__
        mm = multidispatch.registry.get(name)
        if mm is None:
            @functools.wraps(function)
            def wrapper(self, *args):
                types = tuple(arg.__class__ for arg in args) 
                function = wrapper.typemap.get(types)
                if function is None:
                    raise TypeError("no match")
                return function(self, *args)
            wrapper.typemap = {}
            mm = multidispatch.registry[name] = wrapper
        if types in mm.typemap:
            raise TypeError("duplicate registration")
        mm.typemap[types] = function
        return mm
    return register
multidispatch.registry = {}

它可以这样使用:

class Foo(object):
    @multidispatch(str)
    def render(self, s):
        print('string: {}'.format(s))
    @multidispatch(float)
    def render(self, s):
        print('float: {}'.format(s))
    @multidispatch(float, int)
    def render(self, s, t):
        print('float, int: {}, {}'.format(s, t))

foo = Foo()
foo.render('text')
# string: text
foo.render(1.234)
# float: 1.234
foo.render(1.234, 2)
# float, int: 1.234, 2

上面的示例代码展示了如何根据Foo.render方法参数的类型进行重载。

这段代码是根据参数的确切类型来匹配的,而不是检查isinstance关系。虽然可以修改代码来处理这种情况(这样查找的时间复杂度会变成O(n),而不是O(1)),但听起来你并不需要这种复杂性,所以我就把代码保留在这个简单的形式。

22

如果你在使用Python 3.4(或者愿意为Python 2.6及以上版本安装一个叫做backport的工具),你可以使用functools.singledispatch来实现这个功能*:

from functools import singledispatch

class S_Block(object): pass
class S_Empty(object): pass
class S_Function(object): pass


class Test(object):
    def __init__(self):
        self.render = singledispatch(self.render)
        self.render.register(S_Block, self._render_block)
        self.render.register(S_Empty, self._render_empty)
        self.render.register(S_Function, self._render_function)

    def render(self, s):
        raise TypeError("This type isn't supported: {}".format(type(s)))

    def _render_block(self, s):
        print("render block")

    def _render_empty(self, s):
        print("render empty")

    def _render_function(self, s):
        print("render function")


if __name__ == "__main__":
    t = Test()
    b = S_Block()
    f = S_Function()
    e = S_Empty()
    t.render(b)
    t.render(f)
    t.render(e)

输出结果:

render block
render function
render empty

*这段代码是基于这个链接的内容。

20

这样做可行吗?

self.map = {
            S_Block : self._render_block,
            S_Empty : self._render_empty,
            S_Function: self._render_function
}
def render(self, s):
    return self.map[type(s)](s)

在字典里把一个类对象当作键,然后把你想调用的函数对象作为值,这样可以让你的代码更简洁,也更不容易出错。唯一可能出错的地方就是在定义这个字典的时候,或者当然还有你内部的某个函数。

撰写回答