Unwrapping Decorators, Part 2

1622 words. Time to Read: About 16 minutes.

Quick Recap

Last post, I wrote about the basics of decorators in Python. For those of you that missed it, here are the highlights.

1. Decorators are placed before function definitions, and serve to wrap or add additional functionality to functions without obscuring the single purpose of a given function.
2. They are used like this:
1. When defining a decorator function, it should take a function as input and output a new/different/modified/wrapped function.

Okay. That about covers it. Let’s get to the good stuff! I’m going to cover passing arguments to decorators (a la Flask’s @app.route('/')), stacking decorators, and Class-Based decorators.

Decorator Arguments

You can pass arguments to the decorator! It gets a little more complicated though. Remember how a basic decorator function takes in a function, defines a new function, and returns that? If you have arguments, you actually have to generate the decorator on the fly, so you have to define a function that returns a decorator function that returns the actual function you care about. Oy vey. Go go gadget code example!

Again, it may look confusing at first. You can think about it this way: The outermost function, delay in this case, behaves like it is being called right when you add the decorator. As soon as the interpreter reads @delay(5), it runs the delay function and replaces the @delay decorator with the modified returned decorator. At run-time, when we call sneeze, it looks like sneeze is wrapped in delay_decorator with seconds = 5. Thus, the actual function that gets called is inner, which is sneeze wrapped in a 5 second sleeping function. Still confused? Me too, a bit. Maybe just sleep on it and come back.

Stacking Decorators

I’d like to move to something easier, in the hopes that you continue processing the previous section in the background and by the end of this, it will magically make sense. We’ll see how that works out. Let’s talk about stacking. I can pretty much just show you. You’ll get the gist.

As you can see, you can wrap a function that is already wrapped. In math (and, actually, in programming), they would call this Function Composition. Just as f o g(x) == f(g(x)), stacking @pop on @lock on drop produces pop(lock(drop(it))). Huey would be so proud.

Class-Based Decorators…

…With No Arguments

A decorator can actually be created out of anything that is callable, i.e. anything that provides the __call__ magic method. Usually, I try to come up with my own examples, but the one that I found here illustrated what was happening so darn well, I’m going to poach it with minimal modification.

Which outputs:

…With Arguments

Class-based decorators make decorator arguments much easier, but they behave differently from above. I’m not sure why. Someone who is smarter than me should explain it. Anyways, when arguments are provided to the decorator, three things happen.

1. The decorator arguments are passed to the __init__ function.
2. The function itself is passed to the __call__ function.
3. The __call__ function is only called once, and it is called immediately, similar to how function-based decorators work.

Here’s an example I promised to sneak in.

Pretty cool right? I submit that this version of creating a decorator is, at least for me, the most intuitive.

Bonus!

A bonus is that in Python, since functions are objects, you can add attributes to them. Thus, if you modify the __call__ method above to add the following:

Wrap Up

Anyways, I know this is a lot. This topic is one of the more confusing Python topics for me, but it can really make for a slick API if you’re making a library. Just look at Flask, a web framework or Click, a CLI framework. Both written by the same team, in fact! Actually, I wrote a brief post about Click a while ago, if you’re interested.

Anyways anyways, if you have any questions about decorators (or anything else for that matter), don’t hesitate to ask me! I’m always happy to help (even though I usually end up doing some vigorous googling before I am able to fully answer most questions). Ditto goes for if you can explain something better than I did or have extra input. 😁

Author: Ryan Palo | Tags: python pythonic functional |

Like my stuff? Have questions or feedback for me? Want to mentor me or get my help with something? Get in touch! To stay updated, subscribe via RSS