Python装饰器,自我混淆[重复]

Posted

技术标签:

【中文标题】Python装饰器,自我混淆[重复]【英文标题】:Python decorator, self is mixed up [duplicate] 【发布时间】:2011-07-25 02:33:03 【问题描述】:

我是 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 属性。但是我找不到如何解决这个问题。

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

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

编辑:这个 SO 问题似乎很相似 Decorating python class methods, how do I pass the instance to the decorator?

【问题讨论】:

您知道这样Session 的所有实例都将共享同一个缓存吗? 是的,我还没有完成代码,但我大致想到了在某个地方提供一个额外的会话参数来保持单独的缓存。 【参考方案1】:

像这样使用descriptor protocol:

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)

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

【讨论】:

这确实做到了,尽管我仍然必须围绕您提供的链接中的描述来了解究竟发生了什么。我猜self(在f(self, *args) 中)是通过自动__get__ 调用解决的。但这不是在所有其他情况下为self 提供错误的解决方案,例如self.cache[fname] 甚至 self.f? @Rabarberski:我刚刚编辑了我的答案以包含解释,希望它会有用:) 实现__get__ 的更好方法是使用types.MethodType 来创建实际绑定(或在某些情况下在Py2 上,未绑定)方法对象。在 Py2 上,您将创建 __get__ 主体 return types.MethodType(self, instance, instancetype);在 Py3 上,您首先要在 instance 上测试 None 以避免绑定 (if instance is None: return self),否则,您将 return types.MethodType(self, instance)。完整示例here.【参考方案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是在每次调用方法时被调用。

这确实强调了您需要注意记忆技术和实例方法 - 如果您不小心考虑 self 值的变化,您最终将在实例之间共享缓存的答案,这可能不是你想要的。

【讨论】:

嗯,我不明白。调用之间不会保留缓存变量内容,不是吗?我查过wraps,但我认为我遭受信息过载的困扰,partial() 的意义和有用性(来自先前的答案)正在慢慢渗透。最后,dict 是必要的,因为您希望为每个应用 cacher 的函数使用不同的缓存(键),不是吗? 否,因为缓存是在调用cacher 时创建的,这只发生在函数定义时。 wrapped 是同时创建的,每次调用 wrapped 都会看到相同的原始 cache 定义。 wraps() 只是将__name____doc__ 和其他细节从f 复制到wrapped 的助手,因此装饰函数看起来更像原始函数。 这个关于装饰器一般如何工作的解释也可能会有所帮助:***.com/questions/5481739/…【参考方案3】:

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

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

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

    使用闭包将装饰器更改为返回函数。 实现描述符协议以自己传递 self 参数,就像在 memoize decorator recipe 中所做的那样。

【讨论】:

我认为解决方案 (1) 不是我在帖子中给出的论点的选择。或者我错了?解决方案(2)似乎与奇点的答案相同。感谢您提供 memoize 链接! 我看到的唯一论点是在调用之间保持状态。当然,可以通过装饰器函数的可变参数[s]。

以上是关于Python装饰器,自我混淆[重复]的主要内容,如果未能解决你的问题,请参考以下文章

Python装饰器访问自我[重复]

Python进阶精华-编写装饰器为被包装的函数添加参数

python属性装饰器[重复]

“@”装饰器(在 Python 中)[重复]

Python装饰器的高级用法(翻译)

python装饰器参数[重复]