Python 装饰器中 self 混淆问题
我刚接触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
指的是缓存对象,而不是一个Session实例,而Session实例确实没有pl
这个属性。不过我找不到解决这个问题的方法。
我考虑过的解决方案,但不能用 - 我想过让装饰器类返回一个函数,而不是一个值(就像这篇文章的2.1节中那样),这样self
就能在正确的上下文中被评估,但这不行,因为我的装饰器是以类的形式实现的,并且使用了内置的__call__
方法。然后我又想过不使用类来实现装饰器,这样就不需要__call__
方法了,但我不能这样做,因为我需要在装饰器调用之间保持状态(也就是跟踪self.cache
属性中的内容)。
问题 - 除了使用一个全局的cache
字典变量(我没有尝试过,但我想应该可以),还有其他方法可以让这个装饰器正常工作吗?
编辑:这个SO问题似乎和我遇到的情况相似 装饰Python类方法,如何将实例传递给装饰器?
3 个回答
首先,在下面这一行代码中,你需要明确地把 cacher
对象作为第一个参数传进去:
self.cache[fname] = self.f(self,*args)
在Python中,只有方法会自动添加 self
这个参数。它会把在类里面定义的函数(但不包括像你的 cacher
对象这样的其他可调用对象)转换成方法。为了实现这种行为,我看到有两种方法:
- 修改你的装饰器,让它通过闭包返回函数。
- 实现描述符协议,自己传递
self
参数,就像在 memoize 装饰器的例子 中那样。
闭包通常是更好的选择,因为你不需要去处理描述符协议。保存可变状态在不同调用之间比用类要简单得多,因为你只需要把可变对象放在包含它的作用域里(对于不可变对象,可以通过 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
每次方法被调用时都会被调用。
不过,这也提醒我们在使用记忆化技术和实例方法时要小心——如果你没有注意到 self
的值变化,你可能会在不同的实例之间共享缓存的答案,这可能不是你想要的结果。
使用描述符协议,可以这样做:
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)
希望这能解释它是如何工作的 :)