Перегрузка метода для разных типов аргументов в python
Я пишу препроцессор на 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, как показано выше? Кроме того, предпочтительно "питонический" способ сделать это?
Примечание: может быть несколько " визуализатор" реализации,которые делают утверждения по-разному. Поэтому я не могу переместить код рендеринга в операторы и просто вызвать s.render()
. Это должно быть сделано в классе визуализации.
(я нашел какую-то интересный код "посетитель", но я не уверен, что это действительно то,что я хочу).
4 ответов
будет ли что-то вроде этого работать?
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)
сохранение ссылки на объект класса в качестве ключа в словаре и его значение будет объектом функции, который вы хотите вызвать, сделает ваш код короче и менее подверженным ошибкам. Единственное место, где может произойти ошибка, - это определение словаря. Или одна из ваших внутренних функций, конечно.
Если вы используете Python 3.4 (или готовы установить backport для Python 2.6+), вы можете использовать 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
* код на основе в этом суть.
синтаксис перегрузки, который вы ищете, может быть достигнут с помощью мультиметодный декоратор Гвидо ван Россума.
вот вариант мультиметодного декоратора, который может украсить методы класса (оригинал украшает простые функции). Я назвал вариант 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)), но поскольку похоже, что вам это все равно не нужно, я оставлю код в этой более простой форме.
чтобы добавить некоторые измерения производительности в ответ @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