It's not working with you current design because of how classes work in Python.
When a class is instantiated, the functions on it get bound to the instance -
they become bound methods, so that self
is automatically passed.
You can see it happen:
class A:
def method1(self):
pass
>>> A.method1
<function A.method1 at 0x7f303298ef28>
>>> a_instance = A()
>>> a_instance.method1
<bound method A.method1 of <__main__.A object at 0x7f303a36c518>>
When A is instantiated, method1
is magically transformed from a
function
into a bound method
.
Your decorator replaces method1
- instead of a real function,
it is now an instance of _PrintingArguments
. The magic
that turns functions into bound methods is not applied to random
objects, even if they define __call__
so that they behave like a function. (But that magic can be applied, if your class implements the Descriptor protocol, see ShadowRanger's answer!).
class Decorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class A:
@Decorator
def method1(self):
pass
>>> A.method1
<__main__.Decorator object at 0x7f303a36cbe0>
>>> a_instance = A()
>>> a_instance.method1
<__main__.Decorator object at 0x7f303a36cbe0>
There is no magic. method1
on the instance of A is not a bound method,
it's just a random object with a __call__
method, which will not have
self
passed automatically.
If you want to decorate methods you have to replace the decorated function
with another real function, an arbitrary object with __call__
will not do.
You could adapt your current code to return a real function:
import functools
class _PrintingArguments:
def __init__(self, default_comment, comment_variable):
self.comment_variable = comment_variable
self.default_comment = default_comment
def __call__(self, function):
@functools.wraps(function)
def decorated(*args, **kwargs):
comment = kwargs.pop(self.comment_variable, self.default_comment)
params_str = [repr(arg) for arg in args] + ["{}={}".format(k, repr(v)) for k, v in kwargs.items()]
function_call_log = "{}({})".format(function.__name__, ", ".join(params_str))
print("Function execution - '{}'\n\t{}".format(comment, function_call_log))
function_return = function(*args, **kwargs)
print("\tFunction executed\n")
return function_return
return decorated
def function_log(_function=None, default_comment="No comment.", comment_variable="comment"):
decorator = _PrintingArguments(
default_comment=default_comment,
comment_variable=comment_variable,
)
if _function is None:
return decorator
else:
return decorator(_function)