views:

179

answers:

4

Is there a clean way to have a decorator call an instance method on a class only at the time an instance of the class is instantiated?

class C:
    def instance_method(self):
      print('Method called')

    def decorator(f):
        print('Locals in decorator %s  ' % locals())
        def wrap(f):
            print('Locals in wrapper   %s' % locals())
            self.instance_method()
            return f
        return wrap

    @decorator
    def function(self):
      pass

c = C()
c.function()

I know this doesn't work because self is undefined at the point decorator is called (since it isn't called as an instance method as there is no available reference to the class). I then came up with this solution:

class C:
    def instance_method(self):
      print('Method called')

    def decorator():
        print('Locals in decorator %s  ' % locals())
        def wrap(f):
            def wrapped_f(*args):
                print('Locals in wrapper   %s' % locals())
                args[0].instance_method()
                return f
            return wrapped_f
        return wrap

    @decorator()
    def function(self):
      pass

c = C()
c.function()

This uses the fact that I know the first argument to any instance method will be self. The problem with the way this wrapper is defined is that the instance method is called every time the function is executed, which I don't want. I then came up with the following slight modification which works:

class C:
    def instance_method(self):
      print('Method called')
def decorator(called=[]):
    print('Locals in decorator %s  ' % locals())
    def wrap(f):
        def wrapped_f(*args):
            print('Locals in wrapper   %s' % locals())
            if f.__name__ not in called:
                called.append(f.__name__)
                args[0].instance_method()
            return f
        return wrapped_f
    return wrap

@decorator()
def function(self):
  pass

c = C()
c.function()
c.function()

Now the function only gets called once, but I don't like the fact that this check has to happen every time the function gets called. I'm guessing there's no way around it, but if anyone has any suggestions, I'd love to hear them! Thanks :)

A: 

I think you're asking something fundamentally impossible. The decorator will be created at the same time as the class, but the instance method doesn't exist until the instance does, which is later. So the decorator can't handle instance-specific functionality.

Another way to think about this is that the decorator is a functor: it transforms functions into other functions. But it doesn't say anything about the arguments of these functions; it works at a higher level than that. So invoking an instance method on an argument of function is not something that should be done by a decorator; it's something that should be done by function.

The way to get around this is necessarily hackish. Your method looks OK, as hacks go.

katrielalex
Yeah, I'm aware of how decorators are defined. I was just curious if anyone had experienced this issue before and perhaps came up with a better hack than I.
Michael Mior
A: 

It can be achieved by using callables as decorators.

class ADecorator(object):
    func = None
    def __new__(cls, func):
        dec = object.__new__(cls)
        dec.__init__(func)
        def wrapper(*args, **kw):
            return dec(*args, **kw)
        return wrapper

    def __init__(self, func, *args, **kw):
        self.func = func
        self.act  = self.do_first

    def do_rest(self, *args, **kw):
        pass

    def do_first(self, *args, **kw):
        args[0].a()
        self.act = self.do_rest

    def __call__(self, *args, **kw):
        return self.act(*args, **kw)

class A(object):
    def a(self):
        print "Original A.a()"

    @ADecorator
    def function(self):
        pass


a = A()
a.function()
a.function()
Daniel Kluev
This works, although an unnecessary function call is still made each time the decorated function is executed (to `do_rest`).
Michael Mior
I dont think its possible to eliminate both checks and calls in general case. However, if you know method name, you can re-bind it to do-nothing function. And method name could be acquired from the `func` object our decorator receives.So you could do args[0].__setattr__(self.func.__name__, do_nothing) in do_first().Obviously you would need to declare some do_nothing somewhere or use some builtin for that. However this is even more 'hackish' and time it saves not worth possible side-effects.
Daniel Kluev
A: 

How should multiple instances of the class C behave? Should instance_method only be called once, no matter which instance calls function? Or should each instance call instance_method once?

Your called=[] default argument makes the decorator remember that something with string name function has been called. What if decorator is used on two different classes which both have a method named function? Then

c=C()
d=D()
c.function()
d.function()

will only call c.instance_method and prevent d.instance_method from getting called. Strange, and probably not what you want.

Below, I use self._instance_method_called to record if self.instance_method has been called. This makes each instance of C call instance_method at most once.

If you want instance_method to be called at most once regardless of which instance of C calls function, then simply define _instance_method_called as a class attribute instead of an instance attribute.

def decorator():
    print('Locals in decorator %s  ' % locals())
    def wrap(f):
        def wrapped(self,*args):
            print('Locals in wrapper   %s' % locals())            
            if not self._instance_method_called:
                self.instance_method()
                self._instance_method_called=True
            return f
        return wrapped
    return wrap

class C:
    def __init__(self):
        self._instance_method_called=False
    def instance_method(self): print('Method called')
    @decorator()
    def function(self):
      pass

c = C()
# Locals in decorator {}  
c.function()
# Locals in wrapper   {'self': <__main__.C instance at 0xb76f1aec>, 'args': (), 'f': <function function at 0xb76eed14>}
# Method called
c.function()
# Locals in wrapper   {'self': <__main__.C instance at 0xb76f1aec>, 'args': (), 'f': <function function at 0xb76eed14>}

d = C()
d.function()
# Locals in wrapper   {'self': <__main__.C instance at 0xb76f1bcc>, 'args': (), 'f': <function function at 0xb76eed14>}
# Method called
d.function()
# Locals in wrapper   {'self': <__main__.C instance at 0xb76f1bcc>, 'args': (), 'f': <function function at 0xb76eed14>}

Edit: To get rid of the if statement:

def decorator():
    print('Locals in decorator %s  ' % locals())
    def wrap(f):
        def rest(self,*args):
            print('Locals in wrapper   %s' % locals())
            return f
        def first(self,*args):
            print('Locals in wrapper   %s' % locals())            
            self.instance_method()
            setattr(self.__class__,f.func_name,rest)
            return f
        return first
    return wrap

class C:
    def instance_method(self): print('Method called')
    @decorator()
    def function(self):
      pass
unutbu
True. Doesn't answer my question however.
Michael Mior
Oh, you mean about removing the `if` statement? Well, I know a way if you want `instance_method` to be called only once no matter what instance calls `c.function()`. I can't think of a way to do this without an `if` statement if you want the instances of `C` to behave individually.
unutbu
I've edited my post to show what I mean.
unutbu
>I can't think of a way to do this without an if statement if you want the instances of C to behave individually -- rebinding other method to the instance using `f.__name__` partially achieves that (except cases when method is named differently after being decorated, or being referenced outside of the object before being called)
Daniel Kluev
A: 

I came up with this as a possible alternative solution. I like it because there is only one call that happens when the function is defined, and one when the class is instantiated. The only downside is a tiny bit of extra memory consumption for the function attribute.

from types import FunctionType

class C:
    def __init__(self):
        for name,f in C.__dict__.iteritems():
            if type(f) == FunctionType and hasattr(f, 'setup'):
                  self.instance_method()

    def instance_method(self):
      print('Method called')

    def decorator(f):
        setattr(f, 'setup', True)
        return f

    @decorator
    def function(self):
      pass

c = C()
c.function()
c.function()
Michael Mior
Apologies to those who took the time to craft responses. Much appreciated. However, I think the solution I finally came up with suits my needs best.
Michael Mior