How to Decorate an Instance Method with a Decorator Class

How can I decorate an instance method with a decorator class?

tl;dr

You can fix this problem by making the Timed class a descriptor and returning a partially applied function from __get__ which applies the Test object as one of the arguments, like this

class Timed(object):
def __init__(self, f):
self.func = f

def __call__(self, *args, **kwargs):
print(self)
start = dt.datetime.now()
ret = self.func(*args, **kwargs)
time = dt.datetime.now() - start
ret["time"] = time
return ret

def __get__(self, instance, owner):
from functools import partial
return partial(self.__call__, instance)

The actual problem

Quoting Python documentation for decorator,

The decorator syntax is merely syntactic sugar, the following two function definitions are semantically equivalent:

def f(...):
...
f = staticmethod(f)

@staticmethod
def f(...):
...

So, when you say,

@Timed
def decorated(self, *args, **kwargs):

it is actually

decorated = Timed(decorated)

only the function object is passed to the Timed, the object to which it is actually bound is not passed on along with it. So, when you invoke it like this

ret = self.func(*args, **kwargs)

self.func will refer to the unbound function object and it is invoked with Hello as the first argument. That is why self prints as Hello.


How can I fix this?

Since you have no reference to the Test instance in the Timed, the only way to do this would be to convert Timed as a descriptor class. Quoting the documentation, Invoking descriptors section,

In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol: __get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor.

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting with a.__dict__['x'], then type(a).__dict__['x'], and continuing through the base classes of type(a) excluding metaclasses.

However, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead.

We can make Timed a descriptor, by simply defining a method like this

def __get__(self, instance, owner):
...

Here, self refers to the Timed object itself, instance refers to the actual object on which the attribute lookup is happening and owner refers to the class corresponding to the instance.

Now, when __call__ is invoked on Timed, the __get__ method will be invoked. Now, somehow, we need to pass the first argument as the instance of Test class (even before Hello). So, we create another partially applied function, whose first parameter will be the Test instance, like this

def __get__(self, instance, owner):
from functools import partial
return partial(self.__call__, instance)

Now, self.__call__ is a bound method (bound to Timed instance) and the second parameter to partial is the first argument to the self.__call__ call.

So, all these effectively translate like this

t.call_deco()
self.decorated("Hello", world="World")

Now self.decorated is actually Timed(decorated) (this will be referred as TimedObject from now on) object. Whenever we access it, the __get__ method defined in it will be invoked and it returns a partial function. You can confirm that like this

def call_deco(self):
print(self.decorated)
self.decorated("Hello", world="World")

would print

<functools.partial object at 0x7fecbc59ad60>
...

So,

self.decorated("Hello", world="World")

gets translated to

Timed.__get__(TimedObject, <Test obj>, Test.__class__)("Hello", world="World")

Since we return a partial function,

partial(TimedObject.__call__, <Test obj>)("Hello", world="World"))

which is actually

TimedObject.__call__(<Test obj>, 'Hello', world="World")

So, <Test obj> also becomes a part of *args, and when self.func is invoked, the first argument will be the <Test obj>.

Decorators for Instance Methods

If you decorate method of the class, first argument is always self object (you can access it with args[0]):

import functools

def decorate(method):
@functools.wraps(method)
def wrapper(*args, **kwargs):
print(hasattr(args[0], 'a'))
result = method(*args, **kwargs)
return result
return wrapper

class MyClass(object):
def __init__(self, a=1, b=2):
self.a = a
self.b = b
@decorate
def meth(self):
pass

MyClass().meth()

Prints:

True

Edit:

You can specify also self in your wrapper function (based on comments):

import functools

def decorate(method):
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
print(hasattr(self, 'a'))
result = method(self, *args, **kwargs)
return result
return wrapper

class MyClass(object):
def __init__(self, a=1, b=2):
self.a = a
self.b = b
@decorate
def meth(self):
pass

MyClass().meth()

Prints also:

True

Use an instance method as a decorator within another class

You can use a staticmethod to wrap decorator. The inner func_wrap function of decorator contains an additional parameter in its signature: cls. cls can be used to access the ser attribute of the instance of App, and then the desired methods write and read can be called from cls.ser. Also, note that in your declarations, MySerial.write takes no paramters, but is passed the result of the wrapped function. The code below uses *args to prevent the TypeError which would otherwise be raised:

class MySerial():
def __init__(self):
pass # I have to have an __init__
def write(self, *args):
pass # write to buffer
def read(self):
pass # read to buffer
@staticmethod
def decorator(func):
def func_wrap(cls, *args, **kwargs):
cls.ser.write(func(cls, *args, **kwargs))
return cls.ser.read()
return func_wrap

class App():
def __init__(self):
self.ser = MySerial()
@MySerial.decorator
def myfunc(self):
# 'yummy_bytes' is written to the serial buffer via
# MySerial's decorator method
return 'yummy_bytes'

App().myfunc()

Can a decorator of an instance method access the class?

If you are using Python 2.6 or later you could use a class decorator, perhaps something like this (warning: untested code).

def class_decorator(cls):
for name, method in cls.__dict__.iteritems():
if hasattr(method, "use_class"):
# do something with the method and class
print name, cls
return cls

def method_decorator(view):
# mark the method as something that requires view's class
view.use_class = True
return view

@class_decorator
class ModelA(object):
@method_decorator
def a_method(self):
# do some stuff
pass

The method decorator marks the method as one that is of interest by adding a "use_class" attribute - functions and methods are also objects, so you can attach additional metadata to them.

After the class has been created the class decorator then goes through all the methods and does whatever is needed on the methods that have been marked.

If you want all the methods to be affected then you could leave out the method decorator and just use the class decorator.

Python decorator on instance method

Your decorator has an extra level of indirection which is throwing things off. When you do this:

@paginated_instance_method
def get_attending_users(self, *args, **kwargs):
return User.objects.filter(pk__in=self.attending_list)

You are doing this:

def get_attending_users(self, *args, **kwargs):
return User.objects.filter(pk__in=self.attending_list)
get_attending_users = paginated_instance_method(get_attending_users)

That is what decorators do. Note that paginated_instance_method is called with get_attending_users as its argument. That means that in your decorator, the argument default_page_size is set to the function get_attending_users. Your decorator returns the function wrap, so get_attending_users is set to that wrap function.

Then when you then call Event().get_attending_users() it calls wrap(self), where self is your Event instance. wrap is expecting the argument to be a function, and tries to return a new function wrapping that function. But the argument isn't a function, it's an Event object, so functools.wrap fails when trying to wrap it.

I have a hunch that what you're trying to do is this:

@paginated_instance_method()
def get_attending_users(self, *args, **kwargs):
return User.objects.filter(pk__in=self.attending_list)

That is, you want paginated_instance_method to take an argument. But even if you want to use the default value of that argument, you still have to actually call paginated_instance_method. Otherwise you just pass the method as the argument, which is not what paginated_instance_method is expecting.

The reason it "worked" for a classmethod is that a classmethod takes the class as the first argument, and a class (unlike an instance) does have a __name__ attribute. However, I suspect that if you test it further you'll find it's not really doing what you want it to do, as it's still wrapping the class rather than the method.

Decorating class methods - how to pass the instance to the decorator?

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.

Is it possible for a Python decorator decorating a instance method/classmethod to know the class the function will be bound to?

As jasonharper mentions, the class doesn't exist yet by the time the decorator is called, and so the function it receives is just a regular function (except that its name mentions the class it will be bound to).


For my problem, I ended up doing it attrs-style, using an additional decorator to decorate the class as well.

def include(f: Callable) -> Callable:
"""Add function `f` to SchemaBuilder."""
SchemaBuilder.append(f)
return f

class SchemaBuilder:
records: Dict[Type, Dict[Callable, Any]] = {}
temporary: List[Callable] = []

@classmethod
def append(cls, f: Callable):
"""Temporarily store the method in a list."""
cls.temporary.append(f)

@classmethod
def register(cls, target_cls: Type):
"""Associate all methods stored in the list with `target_cls`.

We rely on the fact that `target_cls` will be instantiated
(and thus this method called) as soon as all of its (immediate)
methods have been created.
"""
cls.records[target_cls] = {k: None for k in cls.temporary}
cls.temporary = []

# In use:

@SchemaBuilder.register # called later
class SomeClass:
@property
@include # called first
def some_property(self): # will be included
pass

@property
def some_other_property(self): # will not be included
pass


Related Topics



Leave a reply



Submit