Декоратор Python, сам путается
Я новичок в декораторах Python (ничего себе, отличная функция!), и мне нелегко заставить работать следующее, потому что аргумент self
получает вид смешения.
#this is the decorator
class cacher(object):
def __init__(self, f):
self.f = f
self.cache = {}
def __call__(self, *args):
fname = self.f.__name__
if (fname not in self.cache):
self.cache[fname] = self.f(self,*args)
else:
print "using cache"
return self.cache[fname]
class Session(p.Session):
def __init__(self, user, passw):
self.pl = p.Session(user, passw)
@cacher
def get_something(self):
print "get_something called with self = %s "% self
return self.pl.get_something()
s = Session(u,p)
s.get_something()
Когда я запускаю это, я получаю:
get_something called with self = <__main__.cacher object at 0x020870F0>
Traceback:
...
AttributeError: 'cacher' object has no attribute 'pl'
для строки, где я self.cache[fname] = self.f(self,*args)
Проблема. Очевидно, проблема заключается в том, что self
является объектом cacher вместо экземпляра сеанса, который действительно не имеет атрибута pl
. Однако я не могу найти, как это решить.
Решения, которые я рассмотрел, но не могу использовать - я думал о том, чтобы класс декоратора возвращал функцию вместо значения (как в разделе 2.1 этого article), так что self
оценивается в правильном контексте, но это невозможно, поскольку мой декоратор реализован как класс и использует встроенный __call__
. Затем я решил не использовать класс для моего декоратора, так что мне не нужен метод __call__, но я не могу этого сделать, потому что мне нужно поддерживать состояние между вызовами декоратора (т.е. Для отслеживания того, что находится в self.cache
).
Вопрос. Итак, помимо использования глобальной переменной словаря cache
(которую я не пробовал, но предполагаю, что она будет работать), есть ли другой способ заставить этот декоратор работать?
Изменить: этот вопрос SO похож на Украшая методы класса python, как передать экземпляр в декоратор?
Ответы
Ответ 1
Используйте протокол дескриптор следующим образом:
import functools
class cacher(object):
def __init__(self, f):
self.f = f
self.cache = {}
def __call__(self, *args):
fname = self.f.__name__
if (fname not in self.cache):
self.cache[fname] = self.f(self,*args)
else:
print "using cache"
return self.cache[fname]
def __get__(self, instance, instancetype):
"""Implement the descriptor protocol to make decorating instance
method possible.
"""
# Return a partial function with the first argument is the instance
# of the class decorated.
return functools.partial(self.__call__, instance)
Изменить:
Как это работает?
Использование протокола дескриптора в декораторе позволит нам получить доступ к методу, украшенному правильным экземпляром, как self, возможно, какой-то код может помочь лучше:
Теперь, когда мы это сделаем:
class Session(p.Session):
...
@cacher
def get_something(self):
print "get_something called with self = %s "% self
return self.pl.get_something()
эквивалентно:
class Session(p.Session):
...
def get_something(self):
print "get_something called with self = %s "% self
return self.pl.get_something()
get_something = cacher(get_something)
Итак, теперь get_something является экземпляром cacher. поэтому, когда мы будем называть метод get_something, он будет переведен на это (из-за протокола дескриптора):
session = Session()
session.get_something
# <==>
session.get_something.__get__(get_something, session, <type ..>)
# N.B: get_something is an instance of cacher class.
и потому:
session.get_something.__get__(get_something, session, <type ..>)
# return
get_something.__call__(session, ...) # the partial function.
так
session.get_something(*args)
# <==>
get_something.__call__(session, *args)
Надеюсь, это объяснит, как это работает:)
Ответ 2
Закрытие часто является лучшим способом, поскольку вам не нужно гадать с протоколом дескриптора. Сохранение изменяемого состояния через вызовы еще проще, чем с классом, поскольку вы просто вставляете изменяемый объект в область содержимого (ссылки на неизменяемые объекты можно обрабатывать либо с помощью ключевого слова nonlocal
, либо путем их скопления в изменяемом объекте, например, одиночный список).
#this is the decorator
from functools import wraps
def cacher(f):
# No point using a dict, since we only ever cache one value
# If you meant to create cache entries for different arguments
# check the memoise decorator linked in other answers
print("cacher called")
cache = []
@wraps(f)
def wrapped(*args, **kwds):
print ("wrapped called")
if not cache:
print("calculating and caching result")
cache.append(f(*args, **kwds))
return cache[0]
return wrapped
class C:
@cacher
def get_something(self):
print "get_something called with self = %s "% self
C().get_something()
C().get_something()
Если вы не полностью знакомы с тем, как работают замыкания, добавление дополнительных заявлений печати (как я уже выше) может быть иллюстративным. Вы увидите, что cacher
вызывается только как функция определена, но wrapped
вызывается каждый раз, когда вызывается метод.
Это подчеркивает, что вам нужно быть осторожным с методами memoisation и методами экземпляра, хотя - если вы не будете осторожны, чтобы учитывать изменения в значении self
, вы в конечном итоге будете делиться кешированными ответами между экземплярами, что может не то, что вы хотите.
Ответ 3
Сначала вы явно передаете объект cacher
в качестве первого аргумента в следующей строке:
self.cache[fname] = self.f(self,*args)
Python автоматически добавляет self
в список аргументов только для методов. Он преобразует функции (но не другие вызовы как ваш объект cacher
!), Определенные в пространстве имен классов для методов. Чтобы получить такое поведение, я вижу два пути:
- Измените декоратор, чтобы вернуть функцию, используя закрытие.
- Внедрить протокол дескриптора, чтобы передать аргумент
self
самостоятельно, как это сделано в memoize decorator recipe.