Декоратор для перегрузки в Python - PullRequest
7 голосов
/ 28 декабря 2011

Я знаю, что это не Pythonic, чтобы писать функции, которые заботятся о типе аргументов, но бывают случаи, когда просто невозможно игнорировать типы, потому что они обрабатываются по-разному.

Наличие множества isinstance проверок в вашей функции просто ужасно; есть ли доступный декоратор функций, который разрешает перегрузки функций? Примерно так:

@overload(str)
def func(val):
    print('This is a string')

@overload(int)
def func(val):
    print('This is an int')

Обновление:

Вот некоторые комментарии, которые я оставил Ответ Давида Заславского :

С несколькими модификациями [с], это вполне удовлетворит мои цели. Еще одно ограничение, которое я заметил в вашей реализации, поскольку вы используете func.__name__ в качестве ключа словаря, вы склонны к конфликтам имен между модулями, что не всегда желательно. [Продолжение]

[продолжение] Например, если у меня есть один модуль, который перегружает func, и другой совершенно не связанный модуль, который также перегружает func, эти перегрузки будут конфликтовать, поскольку функция dispatch dict является глобальной. Этот дикт должен быть как-то локальным для модуля. И не только это, оно также должно поддерживать некое «наследование». [Продолжение]

[продолжение] Под «наследованием» я имею в виду следующее: скажем, у меня есть модуль first с некоторыми перегрузками. Затем еще два модуля, которые не связаны, но каждый импортирует first; оба эти модуля добавляют новые перегрузки к уже существующим, которые они только что импортировали. Эти два модуля должны быть в состоянии использовать перегрузки в first, но новые, которые они только что добавили, не должны сталкиваться друг с другом между модулями. (Это на самом деле довольно сложно сделать правильно, теперь, когда я думаю об этом.)

Некоторые из этих проблем могут быть решены путем небольшого изменения синтаксиса декоратора:

first.py

@overload(str, str)
def concatenate(a, b):
    return a + b

@concatenate.overload(int, int)
def concatenate(a, b):
    return str(a) + str(b)

second.py

from first import concatenate

@concatenate.overload(float, str)
def concatenate(a, b):
    return str(a) + b

Ответы [ 2 ]

5 голосов
/ 28 декабря 2011

Быстрый ответ: существует пакет перегрузки в PyPI, который реализует это более надежно, чем то, что я опишу ниже, хотя и использую немного другой синтаксис. Заявлено, что он работает только с Python 3, но похоже, что для его работы с Python 2 потребуются лишь небольшие изменения (если таковые имеются, я не пробовал).


Длинный ответ: В языках, где вы можете перегружать функции, имя функции (буквально или эффективно) дополнено информацией о сигнатуре ее типа, как когда функция определена, так и когда она называется. Когда компилятор или интерпретатор ищет определение функции, он использует как объявленное имя, так и типы параметров, чтобы определить, к какой функции обращаться. Таким образом, логический способ реализовать перегрузку в Python - реализовать оболочку, которая использует как объявленное имя, так и типы параметров для разрешения функции.

Вот простая реализация:

from collections import defaultdict

def determine_types(args, kwargs):
    return tuple([type(a) for a in args]), \
           tuple([(k, type(v)) for k,v in kwargs.iteritems()])

function_table = defaultdict(dict)
def overload(arg_types=(), kwarg_types=()):
    def wrap(func):
        named_func = function_table[func.__name__]
        named_func[arg_types, kwarg_types] = func
        def call_function_by_signature(*args, **kwargs):
            return named_func[determine_types(args, kwargs)](*args, **kwargs)
        return call_function_by_signature
    return wrap

overload должен вызываться с двумя необязательными аргументами: кортеж, представляющий типы всех позиционных аргументов, и кортеж кортежей, представляющий сопоставления типа имени всех аргументов ключевого слова. Вот пример использования:

>>> @overload((str, int))
... def f(a, b):
...     return a * b

>>> @overload((int, int))
... def f(a, b):
...     return a + b

>>> print f('a', 2)
aa
>>> print f(4, 2)
6

>>> @overload((str,), (('foo', int), ('bar', float)))
... def g(a, foo, bar):
...     return foo*a + str(bar)

>>> @overload((str,), (('foo', float), ('bar', float)))
... def g(a, foo, bar):
...     return a + str(foo*bar)

>>> print g('a', foo=7, bar=4.4)
aaaaaaa4.4
>>> print g('b', foo=7., bar=4.4)
b30.8

Недостатки этого включают

  • Фактически он не проверяет, совместима ли функция, к которой применяется декоратор, с аргументами, данными декоратору. Вы могли бы написать

    @overload((str, int))
    def h():
        return 0
    

    и вы получите ошибку при вызове функции.

  • Это не изящно обрабатывает случай, когда не существует перегруженной версии, соответствующей типам передаваемых аргументов (это поможет вызвать более описательную ошибку)

  • Различает именованные и позиционные аргументы, что-то вроде

    g('a', 7, bar=4.4)
    

    не работает.

  • Есть много вложенных скобок, связанных с использованием этого, как в определениях для g.
  • Как уже упоминалось в комментариях, это не касается функций с одинаковыми именами в разных модулях.

Все это может быть исправлено с достаточной тряпкой, я думаю. В частности, проблема конфликтов имен легко решается путем сохранения таблицы диспетчеризации в качестве атрибута функции, возвращаемой декоратором. Но, как я уже сказал, это простой пример, демонстрирующий основы того, как это сделать.

0 голосов
/ 28 декабря 2011

Это не дает прямого ответа на ваш вопрос, но если вы действительно хотите иметь что-то похожее на перегруженную функцию для разных типов и (совершенно правильно) не хотите использовать isinstance, тогда я бы предложил что-то вроде:

def func(int_val=None, str_val=None):
    if sum(x != None for x in (int_val, str_val)) != 1:
        #raise exception - exactly one value should be passed in
    if int_val is not None:
        print('This is an int')
    if str_val is not None:
        print('This is a string')

При использовании намерение очевидно, и даже не требуется, чтобы разные опции имели разные типы:

func(int_val=3)
func(str_val="squirrel")
...