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

Posted

技术标签:

【中文标题】python中不同参数类型的方法重载【英文标题】:Method overloading for different argument type in python 【发布时间】:2014-10-10 05:25:11 【问题描述】:

我正在用 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 真的一点用处都没有。

如果类型检查 ifs 没有可怕的千米长序列,我将如何在 python 中执行此操作,如上所示?另外,最好是“pythonic”的方式来做到这一点?

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

(我找到了一些interesting "visitor" code,但我不确定它是否真的是我想要的东西)。

【问题讨论】:

查看this question 的答案。这个在技术上是重复的,但它收到了一些不同的答案,所以我犹豫要不要举报它。 【参考方案1】:

如果您使用的是 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

*代码基于this gist.

【讨论】:

【参考方案2】:

这样的东西有用吗?

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)

保留对类对象的引用作为字典中的键,并将其值作为您要调用的函数对象,这将使您的代码更短,更不容易出错。这里唯一可能发生错误的地方是字典的定义。或者当然是您的内部职能之一。

【讨论】:

isinstance 为子类以及类本身返回 true。您无法将语义与映射相匹配。 @Joowani 伟大的头脑之类的东西。您唯一应该考虑的是,在我的实现中,字典是静态的,将在类的所有实例中使用。每次使用该函数时都必须实例化您的字典。 @roippi 我从未使用过 isinstance?编辑:哦,我明白你现在的意思了。我没有考虑过,但这似乎不是操作所需要的。 有趣的方法,@roippi 在这里真的不是问题,我总是匹配层次结构的“最深的孩子”,例如。 S_If,而不是 S_BranchingStatement(如果我有的话)。如果出现更好的情况,我会等待一段时间,但这看起来确实是一种很好的技术(不完美,但至少不那么冗长)。 我接受了这一点,因为它最适合我的项目并且最容易理解。但是,一般来说,其他答案可能会更好。【参考方案3】:

您正在寻找的重载语法可以使用Guido van Rossum's multimethod decorator 来实现。

这里是多方法装饰器的一个变体,它可以装饰类方法(原来装饰的是普通函数)。我已将变体命名为 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) 为代价),但是由于听起来您无论如何都不需要它,因此我将以这种更简单的形式保留代码。

【讨论】:

【参考方案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

我更喜欢这个版本而不是带有地图的版本,因为它与使用 isinstance() 的实现具有相同的行为:当您传递 S_SpecialBlock 时,它会将其传递给采用 S_Block 的渲染器。

可用性

正如 dano 在 another answer 中提到的,这适用于 Python 3.4+,而 backport 适用于 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() 的哪个版本,这将导致TypeError - “渲染需要至少1 个位置参数”(参见source code)

实际上,如果只有一个,我认为应该可以使用关键字参数...如果您真的需要,那么您可以执行类似于this answer 的操作,它会创建一个具有不同包装器的自定义装饰器。 这也是 Python 的一个不错的特性。

【讨论】:

【参考方案5】:

为@unutbu 的答案添加一些性能测量:

@multimethod(float)
def foo(bar: float) -> int:
    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

【讨论】:

以上是关于python中不同参数类型的方法重载的主要内容,如果未能解决你的问题,请参考以下文章

什么是方法重载?可以定义两个同名但参数类型不同的方法吗?

重载与重写

方法重载

方法重载

方法的重载设计

请简述重载和重写的区别