Python Decorators
On the subject of Decorators. Right now my brain is feeling overwhelmed and I wonder if I bit off more than I could chew. Not sure exactly which is best to start with but I’ll first go for the how and follow up with some of the why.
Also this article assumes some knowledge of decorators already. It’s not intended to be a full course on how they work or the motivation behind them (though I attempt to shed light on both).
How?
How do these work? What do they do?
Basically decorators create a simpler way to work with the idea that in Python we can pass a function around just like any other object. Such that we can pass a function to another like this:
def outerFunction(func):
# do something with func()
def innerFunction():
pass
outerFunction(innerFunction())
See? Passed a function to a function, which is valid. Well, it turns out that a decorator is basically just a simpler way to pull that off. This basically achieves the same (though this is overly simplistic):
def outerFunction(func):
# do something with func()
@outerFunction
def innerFunction():
pass
innerFunction()
As long as we defined the innerFunction and ‘decorated’ its definition with @outerFunction
then we’re essentially doing the same thing as in the earlier example.
There is a bit more to it though. Here’s what boilerplate that I’m going to talk about looks like, which I’d like to dive into:
import functools
def outerFunction(func):
@functools.wraps(func)
def outerFunction_wrapper(*args,**kwargs):
print('before running func')
wrapped = func(*args,**kwargs)
print('after running func')
print('we can do anything else in here')
return wrapped
return outerFunction_wrapper
@outerFunction
def innerFunction():
print('running innerFunction')
innerFunction()
What this allows us to do is to modify in some way the workings of the original innerFunction
. In this case all it would do is add some printed lines around the one that innerFunction
already prints. I’m avoiding going into the full detail here because many have come before me to do that. I do want to highlight some points however.
functools?
Yes, functools. This part is interesting. The basic reason behind which one would want to include this is because otherwise the innerFunction loses its identity. Observe:
In [4]: import functools
...:
...: def outerFunction(func):
...: @functools.wraps(func)
...: def outerFunction_wrapper(*args,**kwargs):
...: #print('before running func')
...: wrapped = func(*args,**kwargs)
...: #print('after running func')
...: #print('we can do anything else in here')
...: return wrapped
...: return outerFunction_wrapper
...:
...: @outerFunction
...: def innerFunction():
...: print('running innerFunction')
...: print(f'Function name: {innerFunction.__name__}')
...:
...:
...: innerFunction()
running innerFunction
Function name: innerFunction
In the above, we print out the name of the function, and get what would be expected (the name of the function we’re asking about). But below you’ll see that without @functools.wraps(func)
, innerFunction
has the name of the outerFunction_wrapper
, and would include the rest of its properties as well. Which may not be desired.
In [5]: import functools
...:
...: def outerFunction(func):
...: # @functools.wraps(func)
...: def outerFunction_wrapper(*args,**kwargs):
...: #print('before running func')
...: wrapped = func(*args,**kwargs)
...: #print('after running func')
...: #print('we can do anything else in here')
...: return wrapped
...: return outerFunction_wrapper
...:
...: @outerFunction
...: def innerFunction():
...: print('running innerFunction')
...: print(f'Function name: {innerFunction.__name__}')
...:
...:
...: innerFunction()
running innerFunction
Function name: outerFunction_wrapper
So we include functools and decorate our _wrapper
with @functools.wraps(func)
so that we can take in the properties from func
, store them temporarily and reassign them to our _wrapper
function.
outerFunction_wrapper?
Yes! And this honestly is one where I get a tad confused. Basically, this is a function that represents innerFunction
for purposes of the wrapper. It can do whatever it wants with the values passed originally to innerFunction and return whatever it wants, including any functions, even the original func
one that had been passed in. What confuses me is how in the world it actually gets access to the args passed to innerFunction
. But that may be an answer I’ll just have to keep seeking and later note something down about it.
Either way, an example:
In [12]: import functools
...:
...: def outerFunction(func):
...: @functools.wraps(func)
...: def outerFunction_wrapper(number):
...: print(f'sneakily multiplying {number} by 3')
...: value = number * 3
...: return value
...: return outerFunction_wrapper
...:
...: @outerFunction
...: def innerFunction(number):
...: return number
...:
...:
...: innerFunction(2)
when we pass innerFunction
into outerFunction
we need to be able to do something with it, modify it, etc (that’s why we’re decorating in the first place). That’s where this _wrapper
comes in. the _wrapper()
accepts *args, **kwargs
(anything, or we could specify if we wanted) as input that was passed in when innerFunction('something')
was called. In the case above, we see that all innerFunction
is supposed to be doing is returning whatever was passed to it. Kinda boring and excessive. But when decorated with @outerFunction
it becomes something new and returns a multiple of 3 and whatever was passed to it.
Why?
So why do we care about decorating? I suppose that for myself the only real reason I want to understand them at this stage are:
- So I understand what’s going on under the hood when reading someone else’s code that uses them
- Perhaps so I can create a
@debug
wrapper that suits my own needs for debugging purposes. May be helpful. I tend to clutter my code during dev withprint()
statements that echo out what’s going on and things get hairy and confusing after awhile
I’ve seen timer decorators that list how much time a particular function took to run, so that can be useful too.
I’ll discover more as I continue onward. Right now I think I’ll wrap up and move onward!