ADVANCED PYTHON PROGRAMMING

Function Internals 2

This time, we keep exploring bytecode—how does it work, what are closures, and whether we can actually rewrite functions on the fly.

Dan Gittik
8 min readApr 18, 2020

--

Last time, we saw that a function is actually an object, with a few special attributes that encapsulate its “function”-ness—namely, __code__, which keeps the bytecode describing its algorithm, as well as all the necessary values and names. This time, we’ll take an even closer look, figure out where scopes fit in it all—and see how to modify a function’s code dynamically.

Storing Names

Let’s start with a simple piece of code:

Now, let’s disassemble it:

The surprising bit about this code is that to access the value of x, and to access the built-in print function, it uses the same LOAD_NAME instruction; how does it know which scope to look in? Does it traverse the entire namespace hierarchy every time?

The truth is, I cheated—by writing a piece of code without any context, the assignment x = 1 actually did happen in the code’s only—and therefore, global—scope, right where print is.

If we’d actually define a new function with a scope of its own:

We’d see its code is a bit different:

Specifically, the two load instructions are different: to access the local x, Python uses LOAD_FAST; and to access the global print, it uses LOAD_GLOBAL. So it does optimize name lookup after all, whenever it compiles a function—and while the abstraction with the hierarchy of namespaces works well for understanding name resolution—reading the tiny letters is better still.

But Seriously, Who Cares

Except for being great fun, disassembling functions lets us reassemble them differently, and inject code dynamically—which is pretty crazy, and can lead to very powerful metaprogramming techniques. However, dealing with such primal forces is not easy, as you can see even from this simple example:

Contrary to what you might expect, it does this:

Which is weird, because we’ve clearly placed x in the locals dictionary. Well—besides the minor detail that you can’t actually edit the locals dictionary in some versions of Python—even if you could, there’d be a problem; and that problem would only become evident from the bytecode:

As you can see—after invoking the locals function and storing 1 under its x, Python actually goes ahead and prints the global x. It’s a funny misunderstanding, really: it had no idea we were manually fiddling with its scopes, so it went ahead and optimized the lookup away, jumping to the conclusion that x must be global.

Getting Some Closure

We’ve seen local names, and we’ve seen global names—but what about non-local ones? Let’s investigate:

So—it’s done with LOAD_DEREF. What that means is, since we’re not supposed to look neither in the local namespace nor in the global one, we need to figure out where exactly we were defined, and fetch the right value from some in-between, limbo scope.

In truth, Python does it for us: when it creates a function, it keeps track of its own scope—so it can create nested functions that reference its scope, by adding “pointers” to this scope into these functions’ closures. This is a fancy word for “all those scopes surrounding the function”, which “close in” on the function and (hopefully) contain all the information it needs to run. You can see it here:

Again, a tuple, whose indices are referenced by LOAD_DEREF; and in it, a cell—a level of indirection into a different scope, where there’s an int object of value…

Exactly—1. So why the extra overhead of cells? Because we can’t bake in the value, of course; it may change. We’d have to resolve it dynamically, and then, instead of keeping track of and traversing the hierarchy of scopes, we’d jump straight where we need, and resolve it immediately. If Python figures it’s a local name, it’d become LOAD_FAST, which goes straight to the local namespace; if it’s global, that’d be LOAD_GLOBAL, which goes straight there; and if it’s neither, LOAD_DEREF will go the the appropriate cell in the closure, providing a portal to that specific value, wherever it may be.

Fiat Function

So now that we know all that, the question is—what the hell can we do with knowledge so obscure. There are two schools of thoughts here; and the first one says, “absolutely nothing”. You should stay away from such… voodoo, from such black magic—it’s completely incomprehensible and unmaintainable to other people, and it can break in some cryptic way at the worst possible moment. Be that as it may, I’m of the second school. You see, growing up, I really liked fantasy—Dungeons and Dragons and the like. I’d always play the mage, throwing fireballs and thunderbolts and whatnot. And never in my life had I encountered a character that yielded magic powers, but shied away from using them because it was too dangerous. Sure, a mage wouldn’t light her cigarette with a fireball—but neither will she prefer to use the somewhat-more-boring-but-oh-so-reliable-longsword instead. Same goes for code: you should use your powers wisely—but this decision is orthogonal to getting more power.

My own fascination with function internals started when one day, I wrote a Python class:

And got annoyed with always having to include self in the signature explicitly. Languages like C++ and Java had an implicit this—yet my beautiful Python did not?

So I set out on a quest to inject this self dynamically, calling unto the magic of decorators, descriptors, and even the Python tracer. But for the life of me, I couldn’t change the local variable self to point to the instance—because, as I looked at the bytecode, I realized there was no local variable self. When you write it this way:

Python assumes self is a global variable, and ends up “jumping over” the local scope without even looking there.

So I had no choice but to rewrite the functions’ code! First, let’s find the index of the global name self, and remove it from that tuple:

Then, let’s add self to the end of the local names’ tuple, co_varnames, and record its index, too:

Finally, let’s add a piece of code that assigns self to the instance—or, to keep things simpler, to the value 1. In other words, we’d like this function:

To print 1. Here goes:

All we need to add is the assignment code:

Like so:

And now, let’s bake it all into a function. Full disclaimer—__code__ has a lot of arguments, but we’re just going to copy most of them over from f:

Recent versions of Python even added a replace method, which clones an existing code object with just a few changes—so it seems I wasn’t the only one playing with this stuff. It’s a tad cleaner:

And now, all we have to do is wrap it up in a function—again, preserving everything else as-is:

Et voilá:

Pretty cool, huh?

Injecting Code

Now let’s do something even cooler. Given this function,

And this function:

We’re going to inject g right in the middle of f—something even decorators can’t do. It’s actually much simpler than our first challenge; the only part missing for us to do it is determining which line we’re on, and how does bytecode even correlate to lines.

Without going into too much detail, __code__ has a co_firstlineno attribute, which has the number of its first line (which would be the function signature); and co_lnotab, which is a data structure mapping offsets in the bytecode to new lines. Luckily, we don’t need to work with something as low-level: dis's get_instructions returns an iterator of handy Instruction objects, which have a starts_line attribute for any instruction that is the first on its line. All we have to do, then, is this:

And there you have it:

Conclusion

In Python, nothing is impossible. That’s technically true for any Turing-complete language (or, even technically-er, false for any Turing-complete language)—but the point is, Python is an amazingly cool language, and having that sort of power at your fingertips is pretty exhilarating. And that’s just functions! Next time, we’ll get into generators—an important foundation for the rather advanced topic of coroutines—and then on to classes, and all the wonderful deliciousness you can do with object-oriented code.

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. Metaphysics
  14. The Ones that Got Away
  15. 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.