Decorating class methods - how to pass the instance to the decorator?
Asked Answered
G

3

80

This is Python 2.5, and it's GAE too, not that it matters.

I have the following code. I'm decorating the foo() method in bar, using the dec_check class as a decorator.

class dec_check(object):

  def __init__(self, f):
    self.func = f

  def __call__(self):
    print 'In dec_check.__init__()'
    self.func()

class bar(object):

  @dec_check
  def foo(self):
    print 'In bar.foo()'

b = bar()
b.foo()

When executing this I was hoping to see:

In dec_check.__init__()
In bar.foo()

But I'm getting TypeError: foo() takes exactly 1 argument (0 given) as .foo(), being an object method, takes self as an argument. I'm guessing problem is that the instance of bar doesn't actually exist when I'm executing the decorator code.

So how do I pass an instance of bar to the decorator class?

Guinna answered 2/3, 2010 at 18:38 Comment(0)
D
102

You need to make the decorator into a descriptor -- either by ensuring its (meta)class has a __get__ method, or, way simpler, by using a decorator function instead of a decorator class (since functions are already descriptors). E.g.:

def dec_check(f):
  def deco(self):
    print 'In deco'
    f(self)
  return deco

class bar(object):
  @dec_check
  def foo(self):
    print 'in bar.foo'

b = bar()
b.foo()

this prints

In deco
in bar.foo

as desired.

Disquietude answered 2/3, 2010 at 18:47 Comment(5)
Change the f(self) line to return f(self) to pass the return of foo back to the caller.Dirkdirks
Broken link to the descriptor page.Automatize
@Apteryx: I linked the Python docs on descriptors; dry, but covers them well enough.Cadmar
How would one pass some args to the decorator with this implementation?Dead
@Dead please see stackoverflow.com/questions/739654. We have more or less a textbook about decorators on Stack Overflow.Bedplate
R
64

Alex's answer suffices when a function is sufficient. However, when you need a class you can make it work by adding the following method to the decorator class.

def __get__(self, obj, objtype):
    """Support instance methods."""
    import functools
    return functools.partial(self.__call__, obj)

To understand this you need to understand the descriptor protocol. The descriptor protocol is the mechanism for binding a thing to an instance. It consists of __get__, __set__ and __delete__, which are called when the thing is got, set or deleted from the instances dictionary.

In this case when the thing is got from the instance we are binding the first argument of its __call__ method to the instance, using partial. This is done automatically for member functions when the class is constructed, but for a synthetic member function like this we need to do it explicitly.

Ravens answered 21/7, 2010 at 4:28 Comment(6)
@Gilbert The descriptor protocol is the mechanism for binding a thing to an instance. It consists of __ get __, __ set __ and __ delete __, which are called when the thing is got, set or deleted from the instances dictionary. In this case when the thing is got from the instance we are binding the first argument of it's __ call __ method to the instance, using partial. This is done automatically for member functions when the class is constructed, but for a synthetic member function like this we need to do it explicitly.Ravens
So what about if the decorator takes arguments?Loran
python docs have a Descriptor HowTo Guide sectionSisk
More detailed explanation with code: #5470456Lawtun
A better way to implement __get__ is to use types.MethodType to make an actual bound (or in some cases on Py2, unbound) method object. On Py2, you'd make the __get__ body return types.MethodType(self, instance, instancetype); on Py3, you'd first test for None on instance to avoid binding (if instance is None: return self), and otherwise, you'd return types.MethodType(self, instance). Full example here. Put the import outside the __get__ though; import (even cached) is relatively expensive for simple attribute lookup.Cadmar
I think your answer should be the one validated because it is more in relation with the context (Using a decorator as a class). Thanks sir!Hallette
T
12

If you want to write the decorator as a class you can do:

from functools import update_wrapper, partial

class MyDecorator(object):
    def __init__(self, func):
        update_wrapper(self, func)
        self.func = func

    def __get__(self, obj, objtype):
        """Support instance methods."""
        return partial(self.__call__, obj)

    def __call__(self, obj, *args, **kwargs):
        print('Logic here')
        return self.func(obj, *args, **kwargs)

my_decorator = MyDecorator

class MyClass(object):
     @my_decorator
     def my_method(self):
         pass
Telefilm answered 27/7, 2017 at 21:57 Comment(6)
How would one pass arguments to this decorator? I tried adding as extra argument to the __init__ method, but then I don't really know what to pass in for func when instantiating the decorator: @my_decorator(func, given_arg).Dead
@Dead Were you able to find the answer to this?Bick
@Bick honestly I don't remember. :) Will update this if I do.Dead
@Dead No problem I actually came up with a solution by adding in the constructor self.func = decorator()(self.func) instead of adding a decorator.Bick
this one should be the best answer!!!Poon
Arguments are in args and kwargs in __call__ methodLame

© 2022 - 2024 — McMap. All rights reserved.