在@property后装饰类方法

9 投票
3 回答
6989 浏览
提问于 2025-04-16 11:23

我想用一个装饰器来包装各种对象的每个方法,除了__init__这个方法。

class MyObject(object):

    def method(self):
        print "method called on %s" % str(self)

    @property
    def result(self):
        return "Some derived property"

def my_decorator(func):
    def _wrapped(*args, **kwargs):
        print "Calling decorated function %s" % func
        return func(*args, **kwargs)
    return _wrapped


class WrappedObject(object):

    def __init__(self, cls):
        for attr, item in cls.__dict__.items():
            if attr != '__init__' and (callable(item) or isinstance(item, property)):
                setattr(cls, attr, my_decorator(item))
        self._cls = cls

    def __call__(self, *args, **kwargs):
        return self._cls(*args, **kwargs)

inst = WrappedObject(MyObject)()

不过,包装一个属性实例的结果其实是这样的:

@my_decorator
@property
def result(self):
    return "Some derived property"

而我想要的结果是这样的:

@property
@my_decorator
def result(self):
    return "Some derived property"

看起来,属性对象的属性是只读的,这意味着在属性被包装后,不能再修改这个函数。我对这种黑科技的做法有点不太舒服,而且我也不想深入了解属性对象。

我能想到的唯一其他解决方案是动态生成一个 metaclass,但我本来是希望能避免这样做的。我是不是漏掉了什么明显的东西?

3 个回答

0

你可以引入一种叫“懒惰”的装饰器,这种装饰器是在你自己的装饰器之后使用的,比如:

class Lazy(object):
    def __init__(self, decorator):
        self.decorator = decorator
    def __call__(self, method):
        self.method = method
        return self

def my_decorator(func):
    def _wrapped(*args, **kwargs):
        print "Calling decorated function %s" % func
        return func(*args, **kwargs)
    if isinstance(func, Lazy):
        lazy = func
        func = lazy.method
        return lazy.decorator(_wrapped)
    return _wrapped

lazy_property = Lazy(property)

然后你可以用 @lazy_property 来代替 @property。 (注意:这段代码没有经过测试,但我希望你能明白这个意思...)

2

经过一番尝试和错误,我想出了以下解决方案。首先,创建一个辅助类,这个类可以模拟一个装饰器描述符:

class DecoratedDescriptor(object):

    def __init__(self, descriptor, decorator):
        self.funcs = {}
        for attrname in '__get__', '__set__', '__delete__':
            self.funcs[attrname] = decorator(getattr(descriptor, attrname))

    def __get__(self, *args, **kwargs):
        return self.funcs['__get__'](*args, **kwargs)

    def __set__(self, *args, **kwargs):
        return self.funcs['__set__'](*args, **kwargs)

    def __delete__(self, *args, **kwargs):
        return self.funcs['__delete__'](*args, **kwargs)

然后,如果你看到一个属性,就把它包裹在这个类里:

# Fragment of your WrappedObject.__init__ method:
if attr != '__init__' and callable(item):
    setattr(cls, attr, my_decorator(item))
elif isinstance(item, property):
    setattr(cls, attr, DecoratedDescriptor(item, my_decorator))

它的工作原理是这样的:

>>> inst = WrappedObject(MyObject)()
>>> print inst.result
Calling decorated function <method-wrapper '__get__' of property object at 0x00BB6930>
Some derived property

好了!没有使用元类 :)

6
class MyObject(object):

    def method(self):
        print "method called on %s" % str(self)

    @property
    def result(self):
        return "Some derived property"

    def common(self, a=None):
        print self

def my_decorator(func):
    def _wrapped(*args, **kwargs):
        print "Calling decorated function %s" % func
        return func(*args, **kwargs)
    return _wrapped


class WrappedObject(object):

    def __init__(self, cls):
        for attr, item in cls.__dict__.items():
            if attr != '__init__' and callable(item):
                setattr(cls, attr, my_decorator(item))
            elif  isinstance(item, property):
                new_property = property(my_decorator(item.__get__), item.__set__, item.__delattr__)
                setattr(cls, attr, new_property)
        self._cls = cls

    def __call__(self, *args, **kwargs):
        return self._cls(*args, **kwargs)

inst = WrappedObject(MyObject)()

这个例子里还有一些其他问题,不过为了回答问题,你只需要在包装一个属性的时候,包装它的 __get__ 方法就可以了。

也就是说,这样修改你的代码是最简单的办法,能够解决问题。不过,我建议你把它改成动态创建一个它所包装的类的子类,这样就不用重新写它的属性了。你可以通过调用 type 函数,传入类名、一个包含基类的元组和一个字典来程序化地创建一个子类。

编辑 - 修改代码以创建被包装类的子类

实际上,给定类的子类几乎不需要对现有代码做什么修改,除了我提到的 type 调用。我刚刚在这里测试过了 - 把你的 WrappedObject 类改成:

class WrappedObject(object):

    def __init__(self, cls):
        dct = cls.__dict__.copy()
        for attr, item in dct.items():
            if attr != '__init__' and callable(item):
                dct[attr] =  my_decorator(item)
            elif  isinstance(item, property):
                new_property = property(my_decorator(item.__get__), item.__set__, item.__delattr__)
                dct[attr] = new_property
        self._cls = type("wrapped_" + cls.__name__, (cls,), dct)

    def __call__(self, *args, **kwargs):
        return self._cls(*args, **kwargs)

撰写回答