Search code examples
pythonpython-2.7python-decoratorsmethod-declarationclass-decorator

Passing 'self' parameter during methods decorating in Python


I want to create decorator that shows which parameters were passed to function and methods. I have already written the code for functions, but methods are giving me a headaches.

This is function decorator that works as intended:

from functools import update_wrapper


class _PrintingArguments:
    def __init__(self, function, default_comment, comment_variable):
        self.function = function
        self.comment_variable = comment_variable
        self.default_comment = default_comment
        update_wrapper(wrapped=function, wrapper=self)

    def __call__(self, *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(self.function.__name__, ", ".join(params_str))
        print("Function execution - '{}'\n\t{}".format(comment, function_call_log))
        function_return = self.function(*args, **kwargs)
        print("\tFunction executed\n")
        return function_return


def function_log(_function=None, default_comment="No comment.", comment_variable="comment"):
    if _function is None:
        def decorator(func):
            return _PrintingArguments(function=func, default_comment=default_comment, comment_variable=comment_variable)
        return decorator
    else:
        return _PrintingArguments(function=_function, default_comment=default_comment, comment_variable=comment_variable)

# example use:
@function_log
def a(*args, **kwargs):
    pass


@function_log(default_comment="Hello World!", comment_variable="comment2")
def b(*args, **kwargs):
    pass


a(0, x=1, y=2)
a(0, x=1, y=2, comment="Custom comment!")

b("a", "b", "c", asd="something")
b("a", "b", "c", asd="something", comment2="Custom comment for b!")

Output of the code execution:

Function execution - 'No comment.'
    a(0, y=2, x=1)
    Function executed

Function execution - 'Custom comment!'
    a(0, y=2, x=1)
    Function executed

Function execution - 'Hello World!'
    b('a', 'b', 'c', asd='something')
    Function executed

Function execution - 'Custom comment for b!'
    b('a', 'b', 'c', asd='something')
    Function executed



I have tried the exactly same decorator for methods:

class A:
    def __init__(self):
        pass

    @function_log
    def method1(self, *args, **kwargs):
        print("\tself = {}".format(self))

    @function_log(default_comment="Something", comment_variable="comment2")
    def method2(self, *args, **kwargs):
        print("\tself = {}".format(self))

a_obj = A()

a_obj.method1(0, 1, p1="abc", p2="xyz")
a_obj.method1(0, 1, p1="abc", p2="xyz", comment="My comment")

a_obj.method2("a", "b", p1="abc", p2="xyz")
a_obj.method2("a", "b", p1="abc", p2="xyz", comment="My comment 2")

The output is:

Function execution - 'No comment.'
    method1(0, 1, p2='xyz', p1='abc')
    self = 0
    Function executed

Function execution - 'My comment'
    method1(0, 1, p2='xyz', p1='abc')
    self = 0
    Function executed

Function execution - 'Something'
    method2('a', 'b', p2='xyz', p1='abc')
    self = a
    Function executed

Function execution - 'Something'
    method2('a', 'b', comment='My comment 2', p2='xyz', p1='abc')
    self = a
    Function executed

Parameter 'self' is not passed by my decorator to the method.
I want to write second decorator 'method_log' that would work pretty similar as 'function_log'. For code:

class A:
    def __init__(self):
        pass

    @method_log
    def method1(self, *args, **kwargs):
        print("\tself = {}".format(self))

    @fmethod_log(default_comment="Something", comment_variable="comment2")
    def method2(self, *args, **kwargs):
        print("\tself = {}".format(self))

a_obj = A()

a_obj.method1(0, 1, p1="abc", p2="xyz")
a_obj.method1(0, 1, p1="abc", p2="xyz", comment="My comment")

a_obj.method2("a", "b", p1="abc", p2="xyz")
a_obj.method2("a", "b", p1="abc", p2="xyz", comment="My comment 2")

I want the output:

Method execution - 'No comment.'
    method1(<__main__.A instance at ...>, 0, 1, p2='xyz', p1='abc')
    self = <__main__.A instance at ...> #
    Function executed

Method execution - 'My comment'
    method1(<__main__.A instance at ...>, 0, 1, p2='xyz', p1='abc')
    self = <__main__.A instance at ...>
    Function executed

Method execution - 'Something'
    method2(<__main__.A instance at ...>, 'a', 'b', p2='xyz', p1='abc')
    self = <__main__.A instance at ...>
    Function executed

Method execution - 'Something'
    method2(<__main__.A instance at ...>, 'a', 'b', comment='My comment 2', p2='xyz', p1='abc')
    self = <__main__.A instance at ...>
    Function executed

Solution

  • 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)