19

我是 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 实例,它确实没有pl属性。但是我找不到如何解决这个问题。

我考虑过但不能使用的解决方案——我想让装饰器类返回一个函数而不是一个值(如本文的第 2.1 节),以便self在正确的上下文中进行评估,但这是不可能的因为我的装饰器被实现为一个类并使用内置 __call__方法。然后我想不要为我的装饰器使用一个类,这样我就不需要 __call__ 方法,但我不能这样做,因为我需要在装饰器调用之间保持状态(即用于跟踪self.cache属性中的内容) .

问题- 那么,除了使用全局cache字典变量(我没有尝试,但假设会起作用)之外,还有其他方法可以使这个装饰器工作吗?

编辑:这个 SO 问题似乎类似于Decorating python 类方法,我如何将实例传递给装饰器?

4

3 回答 3

36

像这样使用描述符协议

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)

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

于 2011-03-29T08:51:19.243 回答
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,您最终将在实例之间共享缓存的答案,这可能不是您想要的。

于 2011-03-29T13:00:07.563 回答
1

cacher首先,您在以下行中显式传递对象作为第一个参数:

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

Pythonself仅自动将方法添加到参数列表中。它将cacher类命名空间中定义的函数(但不是作为您的对象的其他可调用对象!)转换为方法。为了获得这种行为,我看到了两种方法:

  1. 使用闭包将装饰器更改为返回函数。
  2. 实现描述符协议以自己传递self参数,就像在memoize 装饰器配方中所做的那样。
于 2011-03-29T09:19:38.653 回答