pbhowmick
pbhowmick

Reputation: 1113

Python decorators and side-effects

Recently I stumbled upon a slight difference in how a Python decorator behaves depending on whether functools wraps is used or not.

Without functools, this would be an example (trivial) decorator that just takes a single argument and prints it before calling the wrapped function

def order(arg):
    def wrapper(func):
        print(f"order: {arg}")
        return func

    return wrapper


@order("one")
@order("two")
def inc(x):
    return x + 1


print(inc(7))
#Output:
# order: two
# order: one
# 8

With functools.wraps the code would be

from functools import wraps


def order(arg):
    def outer_wrapper(func):
        @wraps(func)
        def inner_wrapper(*args, **kwargs):
            print(f"order: {arg}")
            return func(*args, **kwargs)

        return inner_wrapper

    return outer_wrapper


@order("one")
@order("two")
def inc(x):
    return x + 1


print(inc(7))
#Output:
# order: one
# order: two
# 8

Subtle difference but the order of side-effects (in this case the print statement) is reversed and unless the engineer using the decorator digs into the implementation detail of the decorator, there is no way of knowing how it will show up. Is there a way for the decorator's implementor can avoid this ambiguity?

Upvotes: 2

Views: 244

Answers (1)

Blckknght
Blckknght

Reputation: 104712

It might help you understand what is going on if you added some additional print statements in your code. Your two versions of the decorator are currently printing out at very different times, but that's obscured a bit because of the limited number of print statements. With a few more, you'll understand things better. Here's an updated version of the second version of your code.

def order(arg):
    print(f"order: {arg}")
    def outer_wrapper(func):
        print(f"outer_wrapper: {arg}")
        @wraps(func)
        def inner_wrapper(*args, **kwargs):
            print(f"inner_wrapper: {arg}")
            result = func(*args, **kwargs)
            print(f"inner_wrapper, after function call: {arg}")
            return result

        return inner_wrapper

    return outer_wrapper


@order("one")
@order("two")
def inc(x):
    return x + 1

print("after function definition")

print(inc(7))

The output will be:

order: one
order: two
outer_wrapper: two
outer_wrapper: one
after function definition
inner_wrapper: one
inner_wrapper: two
inner_wrapper, after function call: two
inner_wrapper, after function call: one
8

The first version of your code only prints out at the same time this code prints the outer_wrapper messages, and the second version of your code only prints out the inner_wrapper messages (the ones before the function call, not the ones after). Note that some of the messages print out when the function is defined (and the decorators are applied) and others print out later, when the function is called (and will print again if you call the function additional times).

Upvotes: 1

Related Questions