Python 装饰器中 self 混淆问题

20 投票
3 回答
8272 浏览
提问于 2025-04-16 14:38

我刚接触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 个回答

1

首先,在下面这一行代码中,你需要明确地把 cacher 对象作为第一个参数传进去:

self.cache[fname] = self.f(self,*args)

在Python中,只有方法会自动添加 self 这个参数。它会把在类里面定义的函数(但不包括像你的 cacher 对象这样的其他可调用对象)转换成方法。为了实现这种行为,我看到有两种方法:

  1. 修改你的装饰器,让它通过闭包返回函数。
  2. 实现描述符协议,自己传递 self 参数,就像在 memoize 装饰器的例子 中那样。
4

闭包通常是更好的选择,因为你不需要去处理描述符协议。保存可变状态在不同调用之间比用类要简单得多,因为你只需要把可变对象放在包含它的作用域里(对于不可变对象,可以通过 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 的值变化,你可能会在不同的实例之间共享缓存的答案,这可能不是你想要的结果。

38

使用描述符协议,可以这样做:

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)

希望这能解释它是如何工作的 :)

撰写回答