ADVANCED PYTHON PROGRAMMING

To Functions, and Beyond!

This time, we consider functions as objects — and develop the decorator design pattern on top of that, in all its glory.

Dan Gittik
11 min readApr 8, 2020

--

Now that we know how to write functions and call them, let’s pause for a moment and talk about what they actually are; because in Python, everything is an object—and that includes functions. They can be passed on as arguments, returned as return values, have attributes, and take part in interesting software compositions, such as the decorator design pattern.

What Maketh an Object?

We’ll talk about objects in great detail in later posts—for now, we’ll settle for the formal definition of, “um, well, a thing you can.. uh, do stuff with?”. In light of this, we’ll try to understand whether Python functions qualify as objects or not.

Pitchin’

One thing you can do with objects is pass them on as an argument to some function. Passing a function to another function might sound cannibalistic, but it’s definitely doable (and even useful—but more on that later):

Catchin’

Another thing you can do with objects is get them back from a function. Receiving a function from another function might sound… surprisingly biologically viable, actually—but in any case, it’s also doable:

Functions as Objects

Another thing objects have in common is that they’re all “object-y”. They have some type, some representation, and possibly attributes—either built-in ones or custom ones. Well, functions do, too:

So as it turns out, you can get a function’s name, docstring and other internal details (like its code object) through special attributes, beginning and ending with a double underscore (pronounced “dunder-X”, as in “dunder-name”). You can also get, set and delete your own attributes, which allows for the occasionally useful function state:

Note that there’s nothing special about this state: having talked about scopes, we know that when the hello function runs, it simply tries to resolve the non-local name hello, reaches the scope of its own definition, gets a pointer to itself, and tickles its run attribute; nothing more, nothing less.

Decorum

So then, let’s talk about decorators: a decorator is a function that receives a function and returns a function, usually decorating it with some additional functionality in the process. If that seems like a lot, just see for yourself:

In this story, double was the decorator: it received the function inc as an argument (for the parameter f), defined a new function, wrapper, and returned it in its stead. This new function is defined with inc in its scope (again—as f), so whatever the original function, it can invoke it and double its result. The weird inc = double(inc) notation is a common idiom of rebinding the function’s name to this “enhanced” version of itself; in fact, it’s so common it even has its own syntactic sugar:

The @double notation takes whatever’s defined underneath it, passes it through double, and rebinds the result to the same name—in this case, inc.

The Omnisignature Strikes Back

Of course, this isn’t all that useful—the double function was tailor-made to create the wrapper function with exactly one argument, x, because inc is a function that receives exactly one argument, x. Had it been a function with any other signature, this wouldn’t have worked. If only we had a signature that’d always work, and some way to forward it perfectly…

This decorator is much more flexible: it replaces f with some wrapper of it, which receives whatever—and passes it on, exactly as it is, to the original f (and then doubles the result, of course).

This is a bit of a silly example, but it works well for illustration purposes; now that you’re familiar with the syntax, let’s see some real examples.

Tracing

I’m one of those old-fashioned people that don’t use a debugger, but prefer print statements. That means I’d often like to be able to trace my program’s execution: you know, which function gets invoked when, with what arguments, and to to what ends. Instead of adding logging before and after every piece of indented code, I can abstract this so-called “cross-cutting concern” into an external utility—and then apply it to my functions using the decorator design pattern, like so:

This can then decorate any function as easily as adding @trace to its crown:

This is nice, but we can make it even cooler:

This way, we also get the arguments, return values and raised exceptions!

This example is great for debugging; but if it’s not a compelling enough reason, here’s a use case that can speed your code up considerably—and everyone wants performance, right?

Caching

Let’s say you’ve implemented the Fibonacci function with recursion:

Go ahead and try to compute the 50th Fibonacci number — on my Mac, it took a whopping hour and a half to do so. But that doesn’t have to be the case! After all, fib(50) only ever computes 50 numbers—it’s just recomputing the same numbers again and again. With fib(50) = fib(49) + fib(48), and fib(49) = fib(48) + fib(47), it means fib(49) gets computed twice; imagine (or calculate!) how many times fib(1) gets computed.

Instead of letting this tomfoolery continue, we could cache the values that were already computed, and fetch them from our cache instead of recomputing them whenever they’re needed. This technique is called memoization, and fits a wide variety of problems; so instead of solving it just for Fibonacci, let’s solve it once and for all with a decorator that caches return values based on the arguments:

Apply it to your function, and bam:

Less than a second!

Cheap Wrapping Paper

Of course, nothing’s perfect—not even decorators. One of their unfortunate side effects is that the original function gets replaced by another—with a different name, and a different docstring. You might not care much, but auto-documentation tools sure do:

Luckily, Python comes with the built-in functools.wraps decorator, which you simply apply to your wrapper to make all your problems go away. This might melt you brain, but bear with me—we’ll implement it ourselves in a moment.

That’s… a lot. I mean, there’s a function call inside a decorator, which is used to decorate the wrapper returned from another decorator. Before we figure out what the fish is going on—let’s see that it works, at least.

Hooray! So then, back to business.

Higher-Order Decorators

Before we get back to functools.wraps, we need to talk about higher-order decorators—and it’s best to start with something simpler:

Decorator Factories

At the end of the day, a decorator is just a function, right? And as such, it can be a returned from another function. “Dude this is so meta” aside, it’s not that hard to imagine:

However daunting these inception-esque definitions might seem, they’re actually pretty straightforward: multiply(m) is a decorator factory, which receives m and returns a decorator that, once applied to some function, makes it so its results are multiplied by m. We can then do:

But also:

It turns out we don’t even have to create the decorator ahead of time; that expression after the @ sign—@me!!!—is actually just that: an expression. We’ve only used simple expressions so far: namely, names. But we can do much more—invocations, arithmetics, even ternary operations! As long as that expression returns a decorator, which can be applied to whatever’s underneath, we’re golden:

In this context, you could say multiply is a second-order decorator: a function that returns a function, which receives a function and returns a function. Quite a mouthful—but if you’re catching the drift, it’s pretty much the next logical step.

Functools.wrapsing Stuff Ourselves

Let’s try to simulate functools.wraps—copy over the function name and docstring. Here’s what I got:

And when we apply it…

It works! Now, let’s break it down. When we decorated inc with @double, it was as if we did inc = double(inc), so inc was passed into double as f. Then, we had to resolve wraps(f)—which returned the decorator fix, whose purpose is to fix its target to be like original, which is bound to inc. This fix is then used as wrapper’s decorator, so once wrapper is ready, we pass it on for fixing, and get its __name__ and __doc__ replaced by original's—that is, inc's—values. We end up with a wrapper that behaves like the decorated version, but resembles the original one—and that’s what we return to replace inc.

Parametrized Tracing

Second-order decorators are handy for more than just fixing other decorators. For example, in our previous tracing example, we hardcoded the print function in; what if we’d like to decorate our code to log stuff into a file?

Whenever you think “parameterized decorator”—it means you’re going to end up one order up, to accomodate for those parameters, and define your decorator inside a scope that has them fixed to some actual arguments. So if we’d like to parameterize our trace decorator to receive a log function, we’d indent it once and wrap it up in a scope with it:

Now we can apply it like so:

Or like so:

Semi-parametrization

If you’re still here, you’re either a jedi, or super high—in any case, let’s take it one step further. Some motivation: most of the time, our log is going to be the print function, so let’s bake it in as a default argument:

However, this has the unfortunate side effect that the second-order decorator has to be invoked (albeit with no arguments) in order to produce the regular, first-order decorator that is applied to the function:

Notice the parenthesis? It’s pretty annoying—and if you forget them, you’re in for some cryptic error messages. What I’d like to do is create a… creature, an abomination, which doubles as both a first-order decorator and a second-order decorator. Ready?

OK, let’s break it down. When we call the decorator like so:

What happens is that write is passed in as the keyword-only parameter log, and the position-only parameter f remains None; this in turn makes the if statement come true, and returns a lambda that can be thought of as a proxy-decorator. This lambda is then applied to div, but what it actually does is just re-invoke trace, this time with its arguments complete. The rest is pretty much the same as before: we define a wrapper in a scope with f and log, and get ourselves a traced division function! But what happens if…

In this case, div is passed immediately into trace, and log defaults to print. We skip the if statement altogether, and just define a wrapper—nothing to see, move along.

Conclusion

This has been fun! Functions, and the so-called functional programming style, which treats them as first-class citizens and juggles them around, really wrinkles your brain—but it’s really powerful, and once you get it, surprisingly straightforward. Stay with me as we venture deeper into functions, this time taking them apart and tinkering with them under the hood.

The Advanced Python Programming series includes the following articles:

  1. A Value by Any Other Name
  2. To Be, or Not to Be
  3. Loopin’ Around
  4. Functions at Last
  5. To Functions, and Beyond!
  6. Function Internals 1
  7. Function Internals 2
  8. Next Generation
  9. Objects — Objects Everywhere
  10. Objects Incarnate
  11. Meddling with Primal Forces
  12. Descriptors Aplenty
  13. Death and Taxes
  14. Metaphysics
  15. The Ones that Got Away
  16. International Trade

--

--

Dan Gittik

Lecturer at Tel Aviv university. Having worked in Military Intelligence, Google and Magic Leap, I’m passionate about the intersection of theory and practice.