我在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中执行此操作,如果类型检查ifs没有可怕的公里长序列,如上所示?此外,最好是" pythonic"这样做的方法?
注意:可以有多个"渲染器"实现,以不同的方式呈现语句。因此,我无法将渲染代码移动到语句中,只需调用s.render()
即可。它必须在渲染器类中完成。
(我发现了一些interesting "visitor" code,但我不确定它是否真的是我想要的东西。
答案 0 :(得分:13)
如果你正在使用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.
的代码答案 1 :(得分:12)
这样的事情会起作用吗?
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)
将类对象的引用保持为字典中的键,并将其值作为要调用的函数对象,这将使您的代码更短,更不容易出错。这里唯一可能出现错误的地方是字典的定义。或者当然是你的一个内部职能。
答案 2 :(得分:12)
您正在寻找的重载语法可以使用Guido van Rossum's multimethod decorator来实现。
这是multimethod装饰器的一个变体,它可以装饰类方法(原始装饰普通函数)。我已将变体命名为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)为代价)但是因为听起来你不需要这个,我将把代码留在这个更简单的形式。
答案 3 :(得分:0)
要在@unutbu的答案中添加一些性能指标:
@multimethod(str)
def foo(bar: str) -> int:
return 'string: {}'.format(bar)
@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
答案 4 :(得分:0)
使用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+中有效,并且在Python 2.6+中有backport。
如果您使用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-“渲染至少需要一个位置参数”(cf {{ 3}})
实际上,我认为如果只有一个参数,应该可以使用关键字参数。如果您确实需要,则可以执行类似于source code的操作,它创建了一个具有不同外观的自定义装饰器包装纸。 这也是Python的一个不错的功能。