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 witha.__dict__['x']
, thentype(a).__dict__['x']
, and continuing through the base classes oftype(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
How to Turn Off Info Logging in Spark
Importing from a Relative Path in Python
Check What Files Are Open in Python
How to Get System Timezone Setting and Pass It to Pytz.Timezone
Python 3.5 - "Geckodriver Executable Needs to Be in Path"
How to Insert Newlines on Argparse Help Text
Import Multiple Excel Files into Python Pandas and Concatenate Them into One Dataframe
Windows- Pyinstaller Error "Failed to Execute Script " When App Clicked
Intercepting Stdout of a Subprocess While It Is Running
How to Add a Custom Ca Root Certificate to the Ca Store Used by Pip in Windows
Plotting 3D Polygons in Python-Matplotlib
Django Aggregation: Summation of Multiplication of Two Fields
Capture Arbitrary Path in Flask Route
How to Get the Correct Dimensions for a Pygame Rectangle Created from an Image
Different Ways of Clearing Lists