David Berger
David Berger

Reputation: 12803

Subclassing method decorators in python

I am having trouble thinking of a way that's good python and consistent with oop principles as I've been taught to figure out how to create a family of related method decorators in python.

The mutually inconsistent goals seem to be that I want to be able to access both decorator attributes AND attributes of the instance on which the decorated method is bound. Here's what I mean:

from functools import wraps

class AbstractDecorator(object):
    """
    This seems like the more natural way, but won't work
    because the instance to which the wrapped function
    is attached will never be in scope.
    """
    def __new__(cls,f,*args,**kwargs):
        return wraps(f)(object.__new__(cls,*args,**kwargs))

    def __init__(decorator_self, f):
        decorator_self.f = f
        decorator_self.punctuation = "..."

    def __call__(decorator_self, *args, **kwargs):
        decorator_self.very_important_prep()
        return decorator_self.f(decorator_self, *args, **kwargs)

class SillyDecorator(AbstractDecorator):
    def very_important_prep(decorator_self):
        print "My apartment was infested with koalas%s"%(decorator_self.punctuation)

class UsefulObject(object):
    def __init__(useful_object_self, noun):
        useful_object_self.noun = noun

    @SillyDecorator
    def red(useful_object_self):
        print "red %s"%(useful_object_self.noun)

if __name__ == "__main__":
    u = UsefulObject("balloons")
    u.red()

which of course produces

My apartment was infested with koalas...
AttributeError: 'SillyDecorator' object has no attribute 'noun'

Note that of course there is always a way to get this to work. A factory with enough arguments, for example, will let me attach methods to some created instance of SillyDecorator, but I was kind of wondering whether there is a reasonable way to do this with inheritance.

Upvotes: 4

Views: 570

Answers (2)

miku
miku

Reputation: 188054

Adapted from http://metapython.blogspot.de/2010/11/python-instance-methods-how-are-they.html. Note that this variant sets attributes on the target instance, hence, without checks, it is possible to overwrite target instance attributes. The code below does not contain any checks for this case.

Also note that this example sets the punctuation attribute explicitly; a more general class could auto-discover it's attributes.

from types import MethodType

class AbstractDecorator(object):
    """Designed to work as function or method decorator """
    def __init__(self, function):
        self.func = function
        self.punctuation = '...'
    def __call__(self, *args, **kw):
        self.setup()
        return self.func(*args, **kw)
    def __get__(self, instance, owner):
        # TODO: protect against 'overwrites'
        setattr(instance, 'punctuation', self.punctuation) 
        return MethodType(self, instance, owner)

class SillyDecorator(AbstractDecorator):
    def setup(self):
        print('[setup] silly init %s' % self.punctuation)

class UsefulObject(object):
    def __init__(self, noun='cat'):
        self.noun = noun

    @SillyDecorator
    def d(self): 
        print('Hello %s %s' % (self.noun, self.punctuation))

obj = UsefulObject()
obj.d()

# [setup] silly init ...
# Hello cat ...

Upvotes: 2

BrenBarn
BrenBarn

Reputation: 251398

@miku got the key idea of using the descriptor protocol. Here is a refinement that keeps the decorator object separate from the "useful object" -- it doesn't store the decorator info on the underlying object.

class AbstractDecorator(object):
    """
    This seems like the more natural way, but won't work
    because the instance to which the wrapped function
    is attached will never be in scope.
    """
    def __new__(cls,f,*args,**kwargs):
        return wraps(f)(object.__new__(cls,*args,**kwargs))

    def __init__(decorator_self, f):
        decorator_self.f = f
        decorator_self.punctuation = "..."

    def __call__(decorator_self, obj_self, *args, **kwargs):
        decorator_self.very_important_prep()
        return decorator_self.f(obj_self, *args, **kwargs)

    def __get__(decorator_self, obj_self, objtype):
        return functools.partial(decorator_self.__call__, obj_self)      

class SillyDecorator(AbstractDecorator):
    def very_important_prep(decorator_self):
        print "My apartment was infested with koalas%s"%(decorator_self.punctuation)

class UsefulObject(object):
    def __init__(useful_object_self, noun):
        useful_object_self.noun = noun

    @SillyDecorator
    def red(useful_object_self):
        print "red %s"%(useful_object_self.noun)

>>> u = UsefulObject("balloons")
... u.red()
My apartment was infested with koalas...
red balloons

The descriptor protocol is the key here, since it is the thing that gives you access to both the decorated method and the object on which it is bound. Inside __get__, you can extract the useful object identity (obj_self) and pass it on to the __call__ method.

Note that it's important to use functools.partial (or some such mechanism) rather than simply storing obj_self as an attribute of decorator_self. Since the decorated method is on the class, only one instance of SillyDecorator exists. You can't use this SillyDecorator instance to store useful-object-instance-specific information --- that would lead to strange errors if you created multiple UsefulObjects and accessed their decorated methods without immediately calling them.

It's worth pointing out, though, that there may be an easier way. In your example, you're only storing a small amount of information in the decorator, and you don't need to change it later. If that's the case, it might be simpler to just use a decorator-maker function: a function that takes an argument (or arguments) and returns a decorator, whose behavior can then depend on those arguments. Here's an example:

def decoMaker(msg):
    def deco(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print msg
            return func(*args, **kwargs)
        return wrapper
    return deco

class UsefulObject(object):
    def __init__(useful_object_self, noun):
        useful_object_self.noun = noun

    @decoMaker('koalas...')
    def red(useful_object_self):
        print "red %s"%(useful_object_self.noun)

>>> u = UsefulObject("balloons")
... u.red()
koalas...
red balloons

You can use the decoMaker ahead of time to make a decorator to reuse later, if you don't want to retype the message every time you make the decorator:

sillyDecorator = decoMaker("Some really long message about koalas that you don't want to type over and over")

class UsefulObject(object):
    def __init__(useful_object_self, noun):
        useful_object_self.noun = noun

    @sillyDecorator
    def red(useful_object_self):
        print "red %s"%(useful_object_self.noun)

>>> u = UsefulObject("balloons")
... u.red()
Some really long message about koalas that you don't want to type over and over
red balloons

You can see that this is much less verbose than writing a whole class inheritance tree for different kinds of decoratorts. Unless you're writing super-complicated decorators that store all sorts of internal state (which is likely to get confusing anyway), this decorator-maker approach might be an easier way to go.

Upvotes: 2

Related Questions