I have some questions regarding what happens inside decorators in Python.
- 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...
- 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.