2011-12-28 7 views
6

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

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

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

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

Update:

Вот некоторые замечания, которые я оставил на David Zaslavsky's answer:

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

[продолжение]. Например, если у меня есть один модуль, который перегружает func, а другой совершенно не связанный модуль, который также перегружает func, эти перегрузки будут сталкиваться, потому что функция отправки ДИКТ глобальна. Этот 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 
+0

Хм ... так что в вашем правлении, что именно вы подразумеваете под 'contatenate.overload' в' first.py'? Как написано, это попытается получить доступ к атрибуту 'overload' функции' concatenate', которая не существует в этом примере. –

+0

@DavidZaslavsky Самая первая перегрузка функции должна быть украшена '@ overload', которая вернет вызываемый объект, который имеет и атрибут' overload'. Все последующие перегрузки должны быть украшены этим уже существующим объектом '@ object.overload', поэтому существует только один dict на объект, а не глобальный dict. (Он работает аналогично стандарту '@ property'.) Я напишу реализацию и сообщу вам, когда я опубликую ее. –

+1

Это будет отход от традиционного синтаксиса перегрузки. Но если это то, что вы хотите, посмотрите на [пакет перегрузки] (http://pypi.python.org/pypi/overload/1.1) (который я собираюсь внести в свой ответ). Он использует этот метод. –

ответ

4

Быстрый ответ: Существует overload package на 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

С помощью нескольких модификаций это очень хорошо подходит для моих целей. Еще одно ограничение, которое я заметил в вашей реализации, поскольку вы используете «func .__ name__» в качестве словарного ключа, вы склонны назвать конфликты между модулями, что не всегда желательно. [cont'd] –

+0

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

+0

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

0

Это не отвечает на ваш вопрос напрямую, но если вы действительно хотите иметь что-то, что ведет себя как перегруженная функция для разных типов и (совершенно справедливо), не хотите использовать 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") 
+0

Это работает для простых примеров, но насколько он масштабируется? –

+0

Ну, если у вас есть много возможностей, вы можете использовать '** kwargs' и сделать это немного более программным. Я использовал его в конструкторе с 17 допустимыми ключевыми словами, и он отлично работал для меня. –

Смежные вопросы