2016-05-30 4 views
1

Я хочу создать декоратор «кеш» для методов класса, который регистрирует во внутреннем свойстве класса результат метода, позволяющего избежать его вычисления несколько раз (и я не хочу использовать простое свойство, вычисленное в __init__, потому что я не уверен, что его вычислить даже раз все время).Python decorator обрабатывает аргументы по умолчанию для декорированной функции

Первая идея заключается в том, чтобы создать декоратор «кэш», похожее на это:

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    def wrapped(obj): 
     if not hasattr(obj, name) or getattr(obj, name) is None: 
      print "Computing..." 
      setattr(obj, name, func(obj)) 
     else: 
      print "Already computed!" 
     return getattr(obj, name) 
    return wrapped 

class Test: 
    @cache 
    def hello(self): 
     return 1000 ** 5 

Все работает отлично:

In [121]: t = Test() 

In [122]: hasattr(t, '_hello') 
Out[122]: False 

In [123]: t.hello() 
Computing... 
Out[123]: 1000000000000000 

In [124]: t.hello() 
Already computed! 
Out[124]: 1000000000000000 

In [125]: hasattr(t, '_hello') 
Out[125]: True 

Теперь давайте скажем, что я хочу сделать то же самое, но когда метод можно вызвать с аргументами (ключевое слово и/или нет). Конечно, теперь мы будем хранить результаты не в отдельных свойствах (какими были бы имена? ...), а в словаре, ключи которого состоят из * args и ** kwargs. Давайте делать это с кортежами:

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    def wrapped(obj, *args, **kwargs): 
     if not hasattr(obj, name) or getattr(obj, name) is None: 
      setattr(obj, name, {}) 
     o = getattr(obj, name) 
     a = args + tuple(kwargs.items()) 
     if not a in o: 
      print "Computing..." 
      o[a] = func(obj, *args, **kwargs) 
     else: 
      print "Already computed!" 
     return o[a] 
    return wrapped 

class Test: 
    @cache 
    def hello(self, *args, **kwargs): 
     return 1000 * sum(args) * sum(kwargs.values()) 

In [137]: t = Test() 

In [138]: hasattr(t, '_hello') 
Out[138]: False 

In [139]: t.hello() 
Computing... 
Out[139]: 0 

In [140]: hasattr(t, '_hello') 
Out[140]: True 

In [141]: t.hello(3) 
Computing... 
Out[141]: 0 

In [142]: t.hello(p=3) 
Computing... 
Out[142]: 0 

In [143]: t.hello(4, y=23) 
Computing... 
Out[143]: 92000 

In [144]: t._hello 
Out[144]: {(): 0, (3,): 0, (4, ('y', 23)): 92000, (('p', 3),): 0} 

Благодаря тому, что метод items превращает словарь в кортеже, не принимая во внимание на порядок в словаре, он отлично работает, если keyworded аргументы не называются в те же заказы:

In [146]: t.hello(2, a=23,b=34) 
Computing... 
Out[146]: 114000 

In [147]: t.hello(2, b=34, a=23) 
Already computed! 
Out[147]: 114000 

Вот моя проблема: если метод имеет аргументы по умолчанию, то он больше не работает:

class Test: 
    @cache 
    def hello(self, a=5): 
     return 1000 * a 

Теперь не приста K больше:

In [155]: t = Test() 

In [156]: t.hello() 
Computing... 
Out[156]: 5000 

In [157]: t.hello(a=5) 
Computing... 
Out[157]: 5000 

In [158]: t.hello(5) 
Computing... 
Out[158]: 5000 

In [159]: t._hello 
Out[159]: {(): 5000, (5,): 5000, (('a', 5),): 5000} 

Результат вычисляется в 3 раза, потому что аргументы не дают один и тот же путь (даже если они являются «же» аргумент!).

Кто-нибудь знает, как я мог поймать значения «по умолчанию», заданные функции, внутри декоратора?

Спасибо

+0

Знаете ли вы [functools.lru_cache] (https://docs.python.org/3/library/functools.html#functools.lru_cache)? –

+0

@Jonas Wielicki Это не доступно на Python ниже 3.2. Он использует Python 2.x –

+0

@VadimShkaberda вправо, я не заметил, что он был добавлен в 3.2. –

ответ

1

Там могут быть различные решения в зависимости от того, насколько сложным будет структура Аргументы функции. Я предпочитаю добавить внутреннюю функцию в hello. Если вы не хотите, чтобы изменить имя кэша, дать ему такое же имя вашей внешней функции есть:

class Test: 
    def hello(self, a=5): 
     @cache 
     def hello(self, a): 
      return 1000 * a 
     return hello(self, a) 

t = Test() 
t.hello() 
t.hello(a=5) 
t.hello(5) 
t._hello 

Out[111]: Computing... 
Already computed! 
Already computed! 
{(5,): 5000} 

Другой подход заключается в добавлении проверки переменных по умолчанию в декоратора, например:

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    def wrapped(obj, *args, **kwargs): 
     if not hasattr(obj, name) or getattr(obj, name) is None: 
      setattr(obj, name, {}) 
     o = getattr(obj, name) 
     a = args + tuple(kwargs.items()) 
     if func.func_defaults: # checking if func have default variable 
      for k in kwargs.keys(): 
       if k in func.func_code.co_varnames and kwargs[k] == func.func_defaults[0]: 
        a =() 
      if args: 
       if args[0] == func.func_defaults[0]: 
        a =() 
     if not a in o: 
      print "Computing..." 
      o[a] = func(obj, *args, **kwargs) 
     else: 
      print "Already computed!" 
     return o[a] 
    return wrapped 

class Test: 
    @cache 
    def hello(self, a=5): 
     return 1000 * a 

t = Test() 
t.hello() 
t.hello(a=5) 
t.hello(5) 
t._hello 

Out[112]: Computing... 
Already computed! 
Already computed! 
{(): 5000} 

Если у вас есть, например, 2 переменные по умолчанию, первый код (с внутренней функцией) все равно будет работать, тогда как второй будет нуждаться в изменениях в «правилах проверки переменных по умолчанию».

+0

Спасибо за быстрый ответ! Мне нравится в обоих направлениях, но я думаю, что я выберу второй, который больше соответствует тому, что мне нужно! – Edouardb

1

Если вы используете достаточно недавнюю версию Python, вы можете использовать inspect.signature, чтобы получить объект Signature, который полностью инкапсулирует информацию о аргументах функции. Затем вы можете вызвать его метод bind с аргументами, передаваемыми вашей оболочкой, чтобы получить объект BoundArguments.Вызов метода apply_defaults на BoundArguments заполнить любые недостающие аргументы, которые имеют значение по умолчанию, а также изучить arguments упорядоченный словарь, чтобы увидеть однозначный список параметров для функции и их значений для этого вызова:

import inspect 

def cache(func): 
    name = "_{:s}".format(func.__name__) 
    sig = inspect.signature(func) 
    def wrapped(obj, *args, **kwargs): 
     cache_dict = getattr(obj, name, None) 
     if cache_dict is None: 
      cache_dict = {} 
      setattr(obj, name, cache_dict)  
     bound_args = sig.bind(obj, *args, **kwargs) 
     bound_args.apply_defaults() 
     cache_key = tuple(bound_args.arguments.values()) 
     if not cache_key in cache_dict: 
      print("Computing...") 
      cache_dict[cache_key] = func(obj, *args, **kwargs) 
     else: 
      print("Already computed!") 
     return cache_dict[cache_key] 
    return wrapped 

Примечания что я переименовал переменные a и o, чтобы иметь более значимые имена. Я также изменил способ создания словаря кеша на объекте. Меньше getattr и setattr звонит так!

Функция inspect.signature была добавлена ​​в Python 3.3, но метод apply_defaults на объектах BoundArguments является новым в Python 3.5. Существует базовая функциональность для старых версий Python on PyPi, но она не включает в себя apply_defaults. Я собираюсь сообщить об этом в качестве проблемы на backport's github tracker.

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