Python类中的装饰器

220

有没有可能写出这样的代码:

class Test(object):
    def _decorator(self, foo):
        foo()

    @self._decorator
    def bar(self):
        pass

这个失败了:@self 中的 self 是未知的。

我还尝试过:

@Test._decorator(self)

也失败了:测试未知

我想暂时更改装饰器中的一些实例变量,然后运行装饰方法,再将它们改回来。

14个回答

367

这个东西是否能够满足你的需求?

class Test(object):
    def _decorator(foo):
        def magic( self ) :
            print "start magic"
            foo( self )
            print "end magic"
        return magic

    @_decorator
    def bar( self ) :
        print "normal call"

test = Test()

test.bar()

这样避免了通过self调用装饰器,将其隐藏在类命名空间中作为普通方法。

>>> import stackoverflow
>>> test = stackoverflow.Test()
>>> test.bar()
start magic
normal call
end magic
>>> 

编辑回答评论中的问题:

如何在另一个类中使用hidden装饰器

class Test(object):
    def _decorator(foo):
        def magic( self ) :
            print "start magic"
            foo( self )
            print "end magic"
        return magic

    @_decorator
    def bar( self ) :
        print "normal call"

    _decorator = staticmethod( _decorator )

class TestB( Test ):
    @Test._decorator
    def bar( self ):
        print "override bar in"
        super( TestB, self ).bar()
        print "override bar out"

print "Normal:"
test = Test()
test.bar()
print

print "Inherited:"
b = TestB()
b.bar()
print

输出:

Normal:
start magic
normal call
end magic

Inherited:
start magic
override bar in
start magic
normal call
end magic
override bar out
end magic

10
装饰器还是被装饰的函数?注意,当在实例上调用“bar”时,包装“bar”的返回的“magic”函数会接收一个self变量,并且可以在调用“bar”之前和之后(甚至是是否调用“bar”)对实例变量做任何操作。在声明类时不存在实例变量的概念。你想从装饰器内部对类进行某些操作吗?我认为这不是一种惯用的用法。 - Michael Speer
2
谢谢Michael,我现在才看到这正是我想要的。 - hcvst
2
我认为这个解决方案比被接受的答案要好,因为它允许在定义时使用 @ 装饰器语法。如果我必须将装饰器调用放在类的末尾,那么就不清楚函数是否被装饰了。有点奇怪的是不能在装饰器本身上使用 @staticmethod,但至少它能正常工作。 - mgiuca
1
我认为如果我创建Test的继承类,它不起作用。例如: class TestB(Test): @_decorator def foobar(self): print "adsf" 有解决方法吗? - extraeee
2
@AndreiToroplean 我猜你要么必须将你的装饰器移到类外以符合你的工具,要么尝试在你的工具上提交PR,允许在类内作为装饰器使用时缺少 self 参数。祝好运。 - Michael Speer
显示剩余16条评论

65

你想要做的事情是不可能的。举个例子,看看下面的代码是否有效:

class Test(object):

    def _decorator(self, foo):
        foo()

    def bar(self):
        pass
    bar = self._decorator(bar)

当然,这是无效的,因为此时self未定义。对于Test也是如此,因为它只有在类本身被定义后(它正在进行中)才会被定义。我向您展示这段代码片段是因为这就是您的装饰器片段转换成的内容。

因此,正如您所看到的,在装饰器中像那样访问实例其实并不真正可行,因为装饰器是在附加到函数/方法的定义期间应用的,而不是在实例化期间应用的。

如果您需要类级别的访问,请尝试这个:

class Test(object):

    @classmethod
    def _decorator(cls, foo):
        foo()

    def bar(self):
        pass
Test.bar = Test._decorator(Test.bar)

5
建议更新以引用下面更准确的答案。 - Nathan Buesgens
6
好的,你的写作表达了一种不可能的意思,但是你的代码基本上展示了如何实现它。 - Mad Physicist

43
import functools


class Example:

    def wrapper(func):
        @functools.wraps(func)
        def wrap(self, *args, **kwargs):
            print("inside wrap")
            return func(self, *args, **kwargs)
        return wrap

    @wrapper
    def method(self):
        print("METHOD")

    wrapper = staticmethod(wrapper)


e = Example()
e.method()

5
类型错误:'staticmethod'对象不可调用。 - wyx
@wyx 不要调用装饰器。例如,应该是 @foo,而不是 @foo() - docyoda
1
wrapper 的第一个参数不应该是 self 吗? - CpILL
2
@docyoda 这不是问题所在。请查看 https://dev59.com/yVgQ5IYBdhLWcg3w-44d。这个例子中的救命稻草是`wrapper = staticmethod(wrapper)@wrapper下面。如果wrapper = staticmethod(wrapper)先出现(或者使用更常见的@staticmethod装饰器),它确实会产生TypeError`。实际上,我并不确定在这种情况下将其变成静态方法有什么作用。 - Dominick Pastore
@DominickPastore 即使不是静态方法,它也可以正常工作,但大多数现代IDE/编辑器会抱怨将 self 作为方法的第一个参数。假设我们确实想要使 wrapper 方法成为静态方法,我们需要在定义任何其他使用此包装器方法的方法之后这样做,以避免自动转换为实例方法。我建议您使用调试器并在方法定义上设置断点,以更好地理解此内容。还可以查看官方文档了解 staticmethod。 - Martino Nikolovski

17

这是一种从同一类中定义的装饰器内部访问(和使用)self的方法:

class Thing(object):
    def __init__(self, name):
        self.name = name

    def debug_name(function):
        def debug_wrapper(*args):
            self = args[0]
            print 'self.name = ' + self.name
            print 'running function {}()'.format(function.__name__)
            function(*args)
            print 'self.name = ' + self.name
        return debug_wrapper

    @debug_name
    def set_name(self, new_name):
        self.name = new_name

输出结果(在 Python 2.7.10 上测试):

Translated:

Output (tested on Python 2.7.10):

>>> a = Thing('A')
>>> a.name
'A'
>>> a.set_name('B')
self.name = A
running function set_name()
self.name = B
>>> a.name
'B'

上面的例子很傻,但它可行。


10

以下是对Michael Speer答案的扩展,更进一步:

一个实例方法装饰器,它接受参数并作用于带有参数和返回值的函数。

class Test(object):
    "Prints if x == y. Throws an error otherwise."
    def __init__(self, x):
        self.x = x

    def _outer_decorator(y):
        def _decorator(foo):
            def magic(self, *args, **kwargs) :
                print("start magic")
                if self.x == y:
                    return foo(self, *args, **kwargs)
                else:
                    raise ValueError("x ({}) != y ({})".format(self.x, y))
                print("end magic")
            return magic

        return _decorator

    @_outer_decorator(y=3)
    def bar(self, *args, **kwargs) :
        print("normal call")
        print("args: {}".format(args))
        print("kwargs: {}".format(kwargs))

        return 27

然后

In [2]:

    test = Test(3)
    test.bar(
        13,
        'Test',
        q=9,
        lollipop=[1,2,3]
    )
    ​
    start magic
    normal call
    args: (13, 'Test')
    kwargs: {'q': 9, 'lollipop': [1, 2, 3]}
Out[2]:
    27
In [3]:

    test = Test(4)
    test.bar(
        13,
        'Test',
        q=9,
        lollipop=[1,2,3]
    )
    ​
    start magic
    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    <ipython-input-3-576146b3d37e> in <module>()
          4     'Test',
          5     q=9,
    ----> 6     lollipop=[1,2,3]
          7 )

    <ipython-input-1-428f22ac6c9b> in magic(self, *args, **kwargs)
         11                     return foo(self, *args, **kwargs)
         12                 else:
    ---> 13                     raise ValueError("x ({}) != y ({})".format(self.x, y))
         14                 print("end magic")
         15             return magic

    ValueError: x (4) != y (3)

7

我在研究一个非常相似的问题时找到了这个问题。 我的解决方案是将问题分成两个部分。 首先,您需要捕获要与类方法关联的数据。 在这种情况下,handler_for将将Unix命令与处理该命令的输出的处理程序相关联。

class OutputAnalysis(object):
    "analyze the output of diagnostic commands"
    def handler_for(name):
        "decorator to associate a function with a command"
        def wrapper(func):
            func.handler_for = name
            return func
        return wrapper
    # associate mount_p with 'mount_-p.txt'
    @handler_for('mount -p')
    def mount_p(self, slurped):
        pass

现在我们已经将一些数据与每个类方法关联起来,我们需要收集这些数据并将其存储在一个类属性中。
OutputAnalysis.cmd_handler = {}
for value in OutputAnalysis.__dict__.itervalues():
    try:
        OutputAnalysis.cmd_handler[value.handler_for] = value
    except AttributeError:
        pass

7

我在一些调试情况下使用这种装饰器,它允许通过装饰来覆盖类属性,而不必查找调用函数。

class myclass(object):
    def __init__(self):
        self.property = "HELLO"

    @adecorator(property="GOODBYE")
    def method(self):
        print self.property

这里是装饰器代码。
class adecorator (object):
    def __init__ (self, *args, **kwargs):
        # store arguments passed to the decorator
        self.args = args
        self.kwargs = kwargs

    def __call__(self, func):
        def newf(*args, **kwargs):

            #the 'self' for a method function is passed as args[0]
            slf = args[0]

            # replace and store the attributes
            saved = {}
            for k,v in self.kwargs.items():
                if hasattr(slf, k):
                    saved[k] = getattr(slf,k)
                    setattr(slf, k, v)

            # call the method
            ret = func(*args, **kwargs)

            #put things back
            for k,v in saved.items():
                setattr(slf, k, v)

            return ret
        newf.__doc__ = func.__doc__
        return newf 

注意:因为我使用了一个类装饰器,所以即使您不向装饰器类构造函数传递任何参数,您也需要使用带括号的@adecorator()来装饰函数。


7
简单实现的方法就是将修饰器方法放在类外部,这样你仍然可以在类中使用它。
def my_decorator(func):
    #this is the key line. There's the aditional self parameter
    def wrap(self, *args, **kwargs):
        # you can use self here as if you were inside the class
        return func(self, *args, **kwargs)
    return wrap

class Test(object):
    @my_decorator
    def bar(self):
        pass

4
把修饰器放在类外并不能回答这个问题,因为问题是如何把一个修饰器放在类里面。你的方法不能解决一个例子,即当修饰器依赖于一个类属性时。 - weegolo

6

在内部类中声明。 这种解决方案非常可靠且值得推荐。

class Test(object):
    class Decorators(object):
    @staticmethod
    def decorator(foo):
        def magic(self, *args, **kwargs) :
            print("start magic")
            foo(self, *args, **kwargs)
            print("end magic")
        return magic

    @Decorators.decorator
    def bar( self ) :
        print("normal call")

test = Test()

test.bar()

结果:
>>> test = Test()
>>> test.bar()
start magic
normal call
end magic
>>> 

4

相比于对象方法,装饰器似乎更适合修改整个对象的功能(包括函数对象),因为对象方法通常会依赖于实例属性。例如:

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

输出结果为:
>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接