Decorating a class method after @property

2019-02-06 22:06发布

I want to wrap every method of various objects except __init__ using a decorator.

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)()

However, the wrapping of a property instance results is equivalent to this:

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

When the desired result is something equivalent to this:

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

It seems the attributes of a property object are read-only preventing modifying the function after property has wrapped it. I'm not too comfortable with the level of hackery required already and I'd rather not delve into the property object anyway.

The only other solution I can see is to generate a metaclass on the fly which I was hoping to avoid. Am I missing something obvious?

3条回答
Evening l夕情丶
2楼-- · 2019-02-06 22:35

You could introduce "lazy" decorators, which are applied after your own decorator, for example:

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)

..and then use @lazy_property instead of @property. (Warning: untested code, but I hope you get the idea...)

查看更多
做个烂人
3楼-- · 2019-02-06 22:37

After a bit of try-and-error, I came up with the following solution. First, create a helper class that will emulate a decorated descriptor:

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)

Then, if you see a property, wrap it in it:

# 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))

This works like this:

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

There! No metaclasses :)

查看更多
欢心
4楼-- · 2019-02-06 22:40

There are a few other issues in this sample, but to atain to question, all you have to do is, when you are wrapping a property

When you are wrapping a property, wrap its __get__ method instead:

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)()

That is the simpelst modification to your code that does the job. I'd however change it to dinamically a subclass of the classs it is wrapping, in order to avoid re-writing its attributes. You can create a subclass programtically by simply caling type with the name, a tuple withe the bases, and a dict as parameters.

edit - changing code to subclass wrapped class

Actually, subclassing the given class requires almost no modification on the given code, but for the type call I indicated. I just tested it here - change your WrappedObject class to:

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)
查看更多
登录 后发表回答