pythonclassdecoratorpython-decoratorskeyword-argument

Python decorator classes with kwargs move function object


First I would like to say that I am still a python aprentice, so I might be missing something obvious here, but after some research on stack overflow and some google articles I could not find exactly the answer.

So my problem is that, in python, when you declare a decorator class, depending on if you call it with kwargs present or not, your func object ends up on the init method or the call method. I understand that the cause of the problem is that when the decorator does not have kwargs it is not called when decorating another function but only initiated, but when kwargs are present it is both initiated and called. But that behaviour looks odd to me, maybe it is just the way python works. There are some workarounds like the one mentioned here but they are just that, workarounds. And as our friend Raymond Hettinger always says: "There must be a better way!"

To exhibit the problem I've written a small script that shows how, the decorator with kargs is called before the script starts, while the decorator without kwargs is called only when the function itself is called. This leads to the function object being parsed to the __ call __ method, for the decorator with kwargs, while it is passed to the __ init __ method, for the decorator without kwargs.

Here is the code to see what I mean:

class Decorator1:
    def __init__(self, *a, **kw):
        print("\nInitializing Decorator1 Class")
        print("init 1 arguments = " + str(a))
        print("init 1 karguments = " + str(kw))
        self.func = a[0]

    def __call__(self, *a, **kw):
        print("\nCalling Decorator1 Class")
        print("call 1 arguments = " + str(a))
        print("call 1 karguments = " + str(kw))
        return self.func()


class Decorator2:
    def __init__(self, *a, **kw):
        print("\nInitializing Decorator2 Class")
        print("init 2 arguments = " + str(a))
        print("init 2 karguments = " + str(kw))

    def __call__(self, *a, **kw):
        print("\nCalling Decorator2 Class")
        print("call 2 arguments = " + str(a))
        print("call 2 karguments = " + str(kw))
        return a[0]


@Decorator1
def my_function_1(height=2):
    print("This is my_function_1")
    return True


@Decorator2(test=3)
def my_function_2(height=2):
    print("This is my_function_2")
    return True


print("\n-------------------- Script Starts ----------------------------")
print("\n### Calling my_function_1 ###")
my_function_1(height=1)
print("\n### Calling my_function_2 ###\n")
my_function_2(height=2)
print("-------------------- Script Ends ------------------------------")

And here's the bash output:

Initializing Decorator1 Class
init 1 arguments = (<function my_function_1 at 0x000001D575367F70>,)
init 1 karguments = {}

Initializing Decorator2 Class
init 2 arguments = ()
init 2 karguments = {'test': 3}

Calling Decorator2 Class
call 2 arguments = (<function my_function_2 at 0x000001D5753791F0>,)
call 2 karguments = {}

-------------------- Script Starts ----------------------------

### Calling my_function_1 ###

Calling Decorator1 Class
call 1 arguments = ()
call 1 karguments = {'height': 1}
my function 1

### Calling my_function_2 ###

my function 2
-------------------- Script Ends ------------------------------

Edit:

To sum up the problem is: Is there a simple way to call the same decorator with and without kwargs? I mean is there a way that I can apply my decorator like this (@Decorator1(kwarg1 = 3)) for some functions while also being able to apply it like this (@Decorator1) for others?

Sorry for the long post, I hope you help me find a better way :D


Solution

  • Your use of Decorator1 causes the function being decorated to be passed to Decorator1.__init__ as an argument.

    Your use of Decorator2 causes an instance of Decorator2 to be created, and then the function being decorated is passed as an argument to Decorator2.__call__.

    Decorator syntax is a shortcut for defining a function, then

    1. calling the decorator on that function
    2. and binding the result of the decorator call to the original name of the function.

    That is, the following are very different:

    # Decorator1 is called on my_function_1
    # Equivalent to my_function_1 = Decorator1(my_function_1)
    @Decorator1
    def my_function_1(height=2):
        print('This is my_function_1')
        return True
    
    # An instance of Decorator1 is called on my_function_1.
    # Equivalent to my_function_1 = Decorator1()(my_function_1)
    @Decorator1()
    def my_function_1(height=2):
        print('This is my_function_1')
        return True
    

    In this sense, decorator syntax differs from class statement syntax, where

    class Foo:
        ...
    

    and

    class Foo():
        ...
    

    are identical.

    Effectively, the string following @ has always been an expression that evaluates to a callable object. Starting in Python 3.9, it can literally be any such expression, rather than being restricted to a dotted name or a dotted-name call as in earlier versions.