Home > other >  How does Python decorator work under the hood?
How does Python decorator work under the hood?

Time:03-06

I have some questions regarding what happens inside decorators in Python.

  1. Consider code, which saves sum in log file:
def logger(func):
    def wrapped(*args, **kwargs):
        result = func(*args, **kwargs)
        with open('log.txt', 'w') as f:
            f.write(str(result))
            
        return result
    return wrapped        
        
@logger
def summator(numlist):
    return sum(numlist)

print(f"Summator: {summator([1, 2, 3, 4, 5])}")

How does it work when I run summator([1, 2, 3, 4, 5, 6])? I suppose the logger to send the summator([1, 2, 3, 4, 5, 6]) function instance inside wrapped, where the summator function is executed and it's result is someway modified. But def logger(func) is strange for me: does it mean, that it takes func with func arguments attached? I see that logger itself doesn't take * args, ** kwargs, only wrapped does...

  1. Consider similar code:
def logger(filename):
    def decorator(func):
        def wrapped(*args, **kwargs):
            result = func(*args, **kwargs)
            with open(filename, 'w') as f:
                f.write(str(result))
            
            return result
        return wrapped
    return decorator

        
@logger('new_log.txt')
def summator(numlist):
    return sum(numlist)

How does decorator get func? It is wrapped by logger, which accepts only a filename.

CodePudding user response:

This is my simple explanation of how decorators work.

You are going to decorate a function, here summator and basically add some functionalities to it without touching the body of it.

The expression:

@logger
def summator(numlist):
    return sum(numlist)

is equivalent to:

def summator(numlist):
    return sum(numlist)

summator = logger(summator)

@ is just a syntactic sugar.

What happened?
Look at logger function. What does it do ? it takes a function and returns another function (here named wrapped). Not the result of that function, just the function itself.

Then you assign this returned function from calling logger(summator) to a symbol called summator. From now on, whenever you call summator you are calling the returned function from the logger. You are calling the wrapped.

Inside wrapped there is a func. What is it ? isn't it the original summator function which you passed to the logger in the previous paragraph? YES.

Q: How wrapped function can access it? it is not its local variable!
A: Because wrapped is a closure and have access to the enclosing scope's local variables. read more here

So wrapped calls func, stores the result, write it to the file then returns it.

note: we normally define our wrapped function with this signature : def wrapped(*args, **kwargs): because we want to be free to pass all parameters(both positional and keyword arguments) down to the func function. At the end of the day func and original summator are the same object. In your case you could write it like:

def logger(func):
    def wrapped(lst):       # <--------
        result = func(lst)  # <--------
        with open('log.txt', 'w') as f:
            f.write(str(result))

        return result
    return wrapped

You second question is referring to what is known as decorator factory. It's a function which returns the actual decorator. Q: When do we use it? A: When we want to have additional parameters from the decorator factory to the decorator or it's inner function. (Here you passed filename as a parameter to the decorator factory but used it inside the wrapped function.)

The story is the same, it is just another layer above our decorator. The equivalent of that syntactic sugar would be:

def summator(numlist):
    return sum(numlist)

summator = logger('new_log.txt')(summator)

# In two steps, it would be:
# decorator = logger('new_log.txt')
# summator = decorator(summator)

note: In the first question, name logger was the actual decorator(Because it takes the summator function for decorating) But in second question it is not. It is now a decorator factory.

The way wrapped can access filename is the same way it can access func as I mentioned previously.

Read more about it here.

  • Related