Перегрузка метода для разных типов аргументов в 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, без отвратительной последовательности длиной в километр, если проверка типов, если, как показано выше? Кроме того, предпочтительно "питонический" способ сделать это?

Примечание. Может быть несколько реализаций "Renderer", которые представляют операторы по-разному. Поэтому я не могу переместить код рендеринга в операторы и просто вызвать s.render(). Это должно быть сделано в классе визуализации.

(Я нашел какой-то интересный код "посетителя", но я не уверен, действительно ли это то, что я хочу).

Ответ 1

Что-то вроде этой работы?

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

Если вы используете 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

* Код, основанный на this gist.

Ответ 3

Синтаксис перегрузки, который вы ищете, может быть достигнут с помощью декоратора многоточия 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)), но поскольку это звучит так, как вам все равно, я оставлю код в этой более простой форме.

Ответ 4

Чтобы добавить некоторые измерения производительности в ответ @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

Ответ 5

Альтернативная реализация с 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.

Доступность

Как отметил Дано в другом ответе, это работает в 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() вызывать, это приведет к TypeError - "render требует как минимум 1 позиционный аргумент" (см. Исходный код)

На самом деле, я думаю, что можно использовать ключевое слово аргумент, если есть только один... Если вам это действительно нужно, вы можете сделать что-то похожее на этот ответ, который создаст собственный декоратор с другой оберткой. Это была бы хорошая особенность Python.