ts.
ts.

Reputation: 10709

How to add a decorator to inherited method in Python without copying whole method?

I've a class that inherits decorated properties from its parent. I want to add one more decorator (@thedecorator1("yyy")), but without overriding the whole method and existing decorators. (Order of decorators is important to some extent, thedecorator1 should be always between property.setter and thedecorator2)

Is that possible in Python 3 ?

from functools import wraps

class thedecorator1:
    def __init__ (self, idx):
        self.idx = idx
    def __call__ (self, func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            print("thedecorator1", self.idx, args)
            return func(*args, **kwargs)
        return wrapped

def thedecorator2(func):
    def _thedecorator2(self, *args, **kwargs):
        print("thedecorator2",args)
        return func(self, *args, **kwargs)
    return _thedecorator2

class SomeClass:
    """That's te base class"""
    _some_property = None

    @property
    @thedecorator2
    def someproperty(self):
        return self._some_property

    @someproperty.setter
    @thedecorator1("xxx")
    @thedecorator2
    def someproperty(self, value):
        self._some_property = value  

class AnotherClass(SomeClass):
    """That's what works, but I need to copy everything everytime"""

    _some_property = None

    @property
    @thedecorator2
    def someproperty(self):
        return self._some_property

    @someproperty.setter
    @thedecorator1("yyy")
    @thedecorator1("xxx")
    @thedecorator2
    def someproperty(self, value):
        self._someproperty = value

class YetAnotherClass(SomeClass):
    """That's what I think I need"""

    dosomethingsmart(target = someproperty, decorator =  thedecorator1("yyy"), after=someproperty.setter)

Upvotes: 1

Views: 1653

Answers (2)

BrenBarn
BrenBarn

Reputation: 251383

There is no way to do this, because a decorator does not "know" about any other decorators applied before or after it. A decorator is not extra information attached to a function; rather, when you use the decorator, it replaces the original function with the decorated version. It's not like you have a function with a list of individual decorators; you just get the end result of applying all the decorators, squashed into one object, and you can't later peek inside and know which decorators are in effect. So when you write this:

class SomeClass:
    """That's te base class"""
    _some_property = None

    @property
    @thedecorator2
    def someproperty(self):
        return self._some_property

Now SomeClass.someproperty is a property object. That property object wraps thedecorator2, but propertydoesn't know anything about thedecorator2; it just wraps the object its given. You defined a method called someproperty, but the decorators wrap it and "bury" the original function; there is no general way to "unwind" the series of decorators to get access to the original function or to the partially-decorated intermediate functions (e.g., the result of applying thedecorator2 but not property).

Later when you define AnotherClass, you say you want to "add a decorator". But to what? The only thing you have access to is SomeClass.someproperty. The original function you defined is not available; it is "buried" under the decorators, and you can't get it back out.

Now, for some decorators, you may be able to get the original function back out. For instance, a propertystores its getter function in its fget attribute. But how (or if!) a decorator stores the original function is up to that decorator. Your thedecorator2 decorator, for instance, does not expose func in any way. So if AnotherClass knows exactly which decorators were applied originally, it might be able to unwind them using specific knowledge about each one. But this still requires you to know which decorators were applied initially, so it doesn't save you anything in terms of not having to duplicate that information in AnotherClass.

The bottom line is that decorators do not "save" the original thing they decorate; they just squash it into whatever they produce. There is no general way to "peel off" the decorators of a function that has been decorated, or even to know which decorators have been applied. Hence you can't have something like insert_new_decorator(somefunction, after=somedecorator), because there is no way to peel back to the layers of decoration to know where somedecorator was in the stack.

Decorators aren't like clothes, where you can take off an outer jacket put a different shirt on underneath, and then put the original jacket back on. Rather, imagine something like a strawberry dipped in chocolate, then dipped in whipped cream, and then drizzled with icing. You can't "unwrap" the cream and the icing and to put a different chocolate coating on underneath. The entire thing is melded into one object whose layers can't be pulled apart.

Upvotes: 2

Robᵩ
Robᵩ

Reputation: 168626

Consider this program:

from functools import wraps

def dec1(fn):
    @wraps(fn)
    def wrap(*args, **kw):
        print "dec1", fn.__name__
        return fn(*args, **kw)
    return wrap

def dec2(fn):
    @wraps(fn)
    def wrap(*args, **kw):
        print "dec2", fn.__name__
        return fn(*args, **kw)
    return wrap

class C(object):
    prop = None
    @property
    def foo(self):
        return self.prop

    @foo.setter
    @dec1
    def foo(self, x):
        self.prop = x

class D(C):
    foo = property(
        C.foo.fget,
        dec2(C.foo.fset),
        C.foo.fdel)

print '.'
c=C()
c.foo = 7
print '.'
d = D()
d.foo = 8
print '.'

D.foo is a property that inherits the get and delete functions from C.foo, but wraps the set function.

An alternative definition for D is:

class D(C):
    foo = C.foo.setter(dec2(C.foo.fset))

In either case, the output of the program is:

.
dec1 foo
.
dec2 foo
dec1 foo
.

Note that the assignment to c.foo invokes one wrapper while the assignment to d.foo invokes both.

Upvotes: 0

Related Questions