Writing/Metaprogramming: An Adventure

Chapter 2 · 19 min read

Tracer Tricks

I'd like to start with something deliberately useless; bear with me, and I'll justify this decision soon enough. For now, the challenge we set out to solve will be the following: given a class that defines some attributes:

>>> class A:
...     x = 1
...     y = 2
...     z = 3

How can we determine their order of definition (as in: x, then y, then z)? In many cases it doesn't really matter — but in some, like database or serialization frameworks, it can be quite essential. The reason I say it's useless, then, is that since Python 3.8 (or even 3.6), the whole problem is moot: dictionaries are inherently ordered, and seeing as class attributes are stored in a dictionary, the order of their definition is invariably retained:

>>> [name for name in vars(A) if not name.startswith("_")]
['x', 'y', 'z']

Ordering Attributes: with Objects

So let's pretend this isn't the case — like, for some reason, we have to use Python 3.5. The way various libraries solved it back in the day was something along the lines of:

>>> class A(Model):
...     x = Field(1)
...     y = Field(2)
...     z = Field(3)

Where we introduce a base class, Model, wrap all the attributes in an auxiliary class, Field, and make them work in tandem to keep track of the order of definition:

>>> import itertools
>>> counter = itertools.count()

>>> class Field:
...     def __init__(self, value):
...         self.value = value
...         self.count = next(counter)

>>> class Model:
...     def __init_subclass__(cls):
...         fields = sorted((field.count, name) for name, field in
...                          vars(cls).items() if isinstance(field, Field))
...         cls.order = [name for count, name in fields]

That is, Field assigns each of its instances a unique, auto-incrementing integer (courtesy of itertool's infinite count generator); while Model collects the names of any such fields and stores them in its subclasses' order attribute, sorted by that number.

This way, even if we can't guarantee the order in which fields appear in each subclass dictionary, we don't need to. As long as fields are instantiated in order (which they are, since class bodies are evaluated sequentially), that information is preserved in each instance's count, which is used for sorting a moment later:

>>> A.order
['x', 'y', 'z']

This approach, however, is not all that elegant: it requires a lot of additional boilerplate to make things work, and one has to wonder whether the original, cleaner syntax of:

>>> class A:
...     x = 1
...     y = 2
...     z = 3

Can be similarly accommodated.

Ordering Attributes: with Metaclasses

As it turns out, it can be. Python 3 introduced the __prepare__ method for metaclasses, which is called before the bodies of their classes are evaluated, and should return the dictionary into which the results of such evaluation will go. Normally, it's a regular (and as such, we pretend, unordered) dictionary; but it's easy enough to swap with a decidedly ordered one, such as collections.OrderedDict. This way we can distill its sequence into a separate attribute once the class is created:

>>> import collections

>>> class OrderedMetaclass(type):
...
...     def __prepare__(mcs, name):
...         return collections.OrderedDict()
...
...     def __init__(cls, name, bases, attributes):
...         cls.order = [name for name in attributes # ← an OrderedDict
...                      if not name.startswith("_")]

All we need to do is make our original class derive from this metaclass:

>>> class A(metaclass=OrderedMetaclass):
...     x = 1
...     y = 2
...     z = 3

Or, more elegantly still, create a base class that derives from it:

>>> class Ordered(metaclass=OrderedMetaclass):
...     pass

So subclassing it is enough:

>>> class A(Ordered):
...     x = 1
...     y = 2
...     z = 3

>>> A.order
['x', 'y', 'z']

Tracing: an Interlude

But let's keep pushing: suppose we need to achieve the same in that archaic language, Python 2.7, that some people thought would last forever. Back then, metaclasses didn't have a __prepare__ method, and ordered dictionaries were all but a distant dream. Still, we can do it as simply as:

>>> @ordered()
... class A:
...     x = 1
...     y = 2
...     z = 3

To do that, however, we'd have to resort to somewhat darker magic: the Python tracer. If you're not familiar with it, I don't blame you — it's mostly used for advanced and rather low-level considerations, like implementing debuggers and coverage tools. But, like everything else in Python, if we want it (and know how to use it), it's right there for the taking. All we have to do is call sys.settrace, passing in some function — and every line of code evaluated henceforth, so long as our tracer is installed, will invoke that function with three special arguments. It's a bit harder to demonstrate in interpreter mode, so let's put everything in a file like example.py:

import sys

def trace(*args):
    print(args)
    
sys.settrace(trace)

def f():
    x = 1
    y = 2
    return x + y
    
f()

And when we run it we should get:

$ python example.py
(<frame at 0x…, file '/home/user/example.py', line 8, code f>, 'call', None)

That's actually a bit underwhelming; it only caught line 8, namely f(), when in fact it's capable of stepping into functions and tracing their internal execution as well. To do that, though, our tracer ought to return another tracer, which will be used to track whatever happens inside a line. Since we'd like it to be exactly the same, nothing stops us from making our tracer return itself, which will keep it active indefinitely:

import sys

def trace(*args):
    print(args)
    return trace
    
# … same as before

And produce a more satisfactory result:

$ python example.py
(<frame at 0x…, file '.../example.py', line 9, code f>, 'call', None)
(<frame at 0x…, file '.../example.py', line 10, code f>, 'line', None)
(<frame at 0x…, file '.../example.py', line 11, code f>, 'line', None)
(<frame at 0x…, file '.../example.py', line 12, code f>, 'line', None)
(<frame at 0x…, file '.../example.py', line 12, code f>, 'return', 3)

If we'd have even deeper nesting (say, f would call g), we'd see it invoked recursively and relentlessly on every step the interpreter makes; hence its utility when it comes to debuggers or code coverage tools.

As for its three arguments, we can see that the first one is a frame object, which contains the scope information of each traced line; the second is a string indicating what type of event was traced — and incidentally, we've seen all the three options it can take: call when entering a function, return when leaving it, and line for anything else; and the third is an argument, used only for return events to denote the value returned.

Ordering Attributes: with Tracers

Interesting though it may be, it's not immediately clear how all this is related to our original problem, of keeping track of the attribute definition order; so let's see what our tracer can tell us in that case:

# … same as before

sys.settrace(trace)

class A:
    x = 1
    y = 2
    z = 3
$ python example.py
(<frame at 0x…, file '.../example.py', line 9, code A>, 'call', None)
(<frame at 0x…, file '.../example.py', line 9, code A>, 'line', None)
(<frame at 0x…, file '.../example.py', line 10, code A>, 'line', None)
(<frame at 0x…, file '.../example.py', line 11, code A>, 'line', None)
(<frame at 0x…, file '.../example.py', line 12, code A>, 'line', None)
(<frame at 0x…, file '.../example.py', line 12, code A>, 'return', None)

Looks good – all of the lines are traced; and curiously enough, the class declaration is considered an invocation as well, with a call event at its beginning and a return event at its end.

However, further trickery is in order: even now that we effectively get a callback on each line, parsing them to infer which attributes they define is easier said than done. So instead, we'll focus on the first trace only: namely, the call event at which the class definition begins.

Much like the call event of a regular function, the frame it receives already has a code object, compiled in advance and containing all the necessary information pertaining to the scope of its execution. If you're familiar with code objects, you know that part of this information has to do with the local, non-local and global names used by this scope; and in case of class bodies, the global ones happen to be (or rather, become) exactly those attributes, the order of which we aim to reconstruct. Luckily for us, the code object provides them in the order in which they are defined — which we can glimpse if we would print frame.f_code.co_names:

import sys

def trace(frame, event, arg):
    print(frame.f_code.co_names)
    
# … same as before

And notice how I removed the return statement, since we don't actually care to keep tracing inside the class definition – the outermost call event is enough:

$ python example.py
('__name__', '__module__', '__qualname__', 'x', 'y', 'z')

Now all that's left is to attach this order to our class object (and maybe filter out the standard attributes, added to all classes by default). To do so, we can make use of a decorator, albeit a tricky one:

>>> def ordered():
...     order = []
...     def record_order(frame, event, arg):
...         order.extend(name for name in frame.f_code.co_names
...                      if not name.startswith("_"))
...         sys.settrace(None)
...     sys.settrace(record_order)
...     def attach_order(cls):
...         cls.order = order
...         return cls
...     return attach_order

What it does is the following: starting with an empty order list, it defines the record_order function, sets it as the active tracer, and returns an altogether different decorator, attach_order. Normally, the next thing that would happen would be the returned function being applied to the class — but since the class definition must happen before that, however implicit this step usually is, and since we've installed a tracer that gets invoked on exactly such an event, record_order is actually called first. As its name implies, it collects the names — the attribute names, that is, and in their order of definition, as we've established — into our list; and since we only ever wanted to capture this particular event for our rather unusual purpose, it also retires itself from its role as the active tracer. This way, when attach_order is finally called, it can decorate the class with our recently (and one might add, cleverly) recorded order, as in:

>>> @ordered()
... class A:
...     x = 1
...     y = 2
...     z = 3

>>> A.order
['x', 'y', 'z']

Do notice that this is a second-order decorator, so we mustn't forget the parentheses in the decoration: A is not passed into ordered, as omitting them would imply. Instead, we invoke ordered() as part of the decoration itself, so it gets a chance to install the tracer before the class definition; A is then passed into whatever it returns, in our case attach_order.

C-Style Enums: with Tracers

This is pretty exciting; or at least, it would be, if dictionaries weren't already ordered, and the whole ordeal wouldn't be entirely pointless. So let's use our newfound knowledge to do something else, something important: add support for C-style enums!

In C, the way we define an enumeration of values is thus:

enum Fruit {
    APPLE,
    LEMON,
    ORANGE,
};

That is, we list the names, and their values are auto-assigned to an incrementing integer, so APPLE ends up as 0, LEMON as 1 and ORANGE as 2. In Python, however, we have to do this:

>>> import enum
>>> class Fruit(enum.Enum):
...     APPLE = 0
...     LEMON = 1
...     ORANGE = 2

That is, provide the values explicitly and manage them manually. There is a trick to spare us the manual management, at least:

>>> class Fruit(enum.Enum):
...     APPLE = enum.auto()
...     LEMON = enum.auto()
...     ORANGE = enum.auto()

But there is something irritating in having a statement that is more elegant in C than it is in Python, isn't there? Surely, we can do better:

>>> @enum()
... class Fruit:
...     APPLE
...     LEMON
...     ORANGE

>>> Fruit.APPLE
0
>>> Fruit.LEMON
1
>>> Fruit.ORANGE
2

At first sight, it might look impossible; like, is this syntax even valid in Python? But if we think about it for a moment, of course it is — class bodies are just code blocks, and what this code block does is resolve the name APPLE, discard its value, and then similarly resolve and discard LEMON and ORANGE. Normally it would throw a NameError, because these names weren't defined, so their resolution naturally fails:

>>> class Fruit:
...     APPLE
NameError: name 'APPLE' is not defined

But there's nothing inherently wrong with the syntax, so long as we find a way to define the names — like, running a piece of code that inspects them in advance and injects an incrementing integer for each before the scope is executed. And it so happens that we've just seen such a trick:

>>> def enum():
...     def inject_values(frame, event, arg):
...         names = [name for name in frame.f_code.co_names
...                  if not name.startswith("_")]
...         for i, name in enumerate(names):
...             frame.f_locals[name] = i
...         sys.settrace(None)
...     sys.settrace(inject_values)
...     def do_nothing(cls):
...         return cls
...     return do_nothing

This decorator has the same flow as ordered: it registers inject_values as a tracer, and returns a passthrough decorator, do_nothing, to be applied to the class. And just like before, since the class definition happens before its decoration, the tracer is called first. In this case, it simply enumerates the names, prefilling the frame's local scope accordingly, and unregisters itself. This way, by the time the class body is executed, APPLE is already 0, LEMON is 1 and ORANGE is 2; the odd statements that resolve those names and discard their values pass flawlessly without doing anything. But then, their actual purpose was to declare the names, so inject_values knows what to prefill – we never needed anything from their execution other than to not crash.

And now, just like we wanted:

>>> @enum()
... class Fruit:
...     APPLE
...     LEMON
...     ORANGE

>>> Fruit.APPLE
0
>>> Fruit.LEMON
1
>>> Fruit.ORANGE
2

C-Style Enums: with Metaclasses

Admittedly, if we deign to update our Python from 2.7 to 3+, I'd say that this approach is a bit too elaborate for our purpose, and suggest reverting back to the more standard __prepare__ method. In this case, instead of providing an ordered dictionary for the class body evaluation, we'd have to implement our own – say, EnumDict – to be tolerant of missing names and know to assign them on demand:

>>> class EnumMetaclass(type):
...     def __prepare__(mcs, name):
...         return EnumDict()

>>> class EnumDict(dict):
...     def __init__(self):
...         self.counter = itertools.count()
...     def __missing__(self, key):
...         if not key.startswith("_"):
...             self[key] = value = next(self.counter)
...             return value
...         raise KeyError(key)

>>> class Enum(metaclass=EnumMetaclass):
...     pass

This way, any class that inherits from Enum will have its body evaluated into an EnumDict; and any odd resolve-and-discard statements will end up calling its __missing__ method (since, to be fair, they are missing), which would make up for this oversight by quickly allocating them and pretending like nothing happened. The result is the same, but with much less voodoo:

>>> class Fruit(Enum):
...     APPLE
...     LEMON
...     ORANGE

>>> Fruit.APPLE
0
>>> Fruit.LEMON
1
>>> Fruit.ORANGE
2

The Linking of Scopes

So, is our tracing trick useful in any real scenario with a modern version of Python? Yes and no. First, I'll give such an example — although, admittedly, it's pretty niche – and then I'll circle back to the point from the beginning of the chapter, on why I don't think it matters all that much.

Suppose we have a configuration defined in a class; the following is common to a lot of web applications:

>>> class config:
...     port = 8000
...     log_path = "/app/log.txt"
...     log_maxsize = 20 * 1024 * 1024
...     log_backlog = 5
...     db_path = "/app/db.sqlite"

And suppose this configuration grows enough that we want to break it into sections; a natural way to do it would be by using nested classes:

>>> class config:
...     port = 8000
...     class db:
...         path = "/app/db.sqlite"
...     class log:
...         path = "/app/log.txt"
...         maxsize = 20 * 1024 * 1024
...         backlog = 5

So far, so good: we run a server on config.port, access the database at config.db.path, and write logs to config.log.path; but there's an unfortunate duplication here that might lead to trouble down the road. The more files we have (located, presumably, in the same app directory), the more paths starting with app/ we'll end up with — and if we ever move our deployment elsewhere and fail to update all of them properly, our results will be written to different places. A natural way to prevent this would be to declare the root as a separate attribute, and reuse it where necessary:

>>> class config:
...     port = 8000
...     root = pathlib.Path("/app")
...     class db:
...         path = root / "db.sqlite"
...     class log:
...         path = root / "log.txt"
...         maxsize = 20 * 1024 * 1024
...         backlog = 5

Except, this doesn't work: try running it and you'll get a NameError, complaining that root is not defined in any of the nested subclasses. This is a peculiar limitation of Python classes — the intuitive lexical scoping we're used to from functions doesn't quite work as expected, because when a class body is evaluated, its scope is managed in a bit of a different way. It can access the global scope in which the class is defined (so if we put root outside config, our code will work) — but it doesn't have a notion of intermediary scopes with non-local variables like functions do. In other words, the namespaces of nested classes are not stringed together with those of their nesting classes, and we simply can't organize our configuration in this seemingly innocuous way. That is, unless:

>>> def link_scopes():
...     def copy_parent_scope(frame, event, arg):
...         if event == "call" and "__name__" in frame.f_code.co_names:
...             frame.f_locals.update(frame.f_back.f_locals)
...             return copy_parent_scope
...     sys.settrace(copy_parent_scope)
...     def remove_tracer(cls):
...         sys.settrace(None)
...         return cls
...     return remove_tracer

This code is similar to what we had before, but there's a subtle change: because we want to keep the tracer on for nested classes, we don't remove it immediately, but only in our second function, invoked as a decorator once the outermost class definition is complete. This way, the tracer stays alive (and notice how it returns itself, to keep "stepping into" any scopes it encounters), awaiting a call event that would signal a class definition, and making sure its frame gets all the information from the preceding scope; this works because while class namespaces might not be linked in the way we expect, frame objects most definitely are. We also want to avoid stepping into actual calls, like the pathlib.Path("/app") invocation, so I've added the "__name__" in frame.f_code.co_names heuristic, which should only hold for classes. And now:

>>> @link_scopes()
... class config:
...     port = 8000
...     root = pathlib.Path("/app")
...     class db:
...         path = root / "db.sqlite" 

...     class log:
...         path = root / "log.txt"
...         maxsize = 20 * 1024 * 1024
...         backlog = 5
>>> config.db.path
Path('/app/db.sqlite')
>>> config.log.path
Path('/app/log.txt')

Just like we wanted. As far as I know, there aren't many ways to achieve the same with other Python features; I guess we could use __prepare__ to return cleverly-linked dictionaries that respect the class nesting hierarchy and resolve names accordingly, but that would take considerably more code than our little trick.

And a final note on performance: some people may be understandably horrified at the notion of installing a tracer in a production environment, since executing a callback on every single line of code does slow it considerably. Bear in mind, however, that we only do so surgically and temporarily, for definitions that happen once, at load time – so the actual impact of such an intervention is actually quite negligible.

The Making of People

Of course, I'd be surprised if this code makes it into production for any number of reasons – but like I said before, this is besides the point. I'm not advocating for you to install tracers that patch code in non-trivial ways because it's useful, but because I believe that knowing it's possible is.

This is the most important thing about metaprogramming – at least, how I see it, and why I wrote this book. I said as much in the introduction, but now that I've had a chance to take you for a spin, I'd like to repeat it one more time – and then I'll shut up about it for the rest of the book, I promise. Metaprogramming is not so much about the making of code — or even, as one might be inclined to think, about the making of code that makes code. Rather, it's about the making of people: its value is not in writing a more useful program (at least, not directly), but in educating, challenging, and otherwise enriching the programmer (who might then also write more useful programs as a result).

Now, cultivating creativity and imagination is always a worthy goal; or, more accurately, is always worthy of lip service, because most of us find it desirable, at least in theory. In practice, however, it's easier said than done: in many situations there is an inherent (if somewhat short-sighted) conflict between a person's growth and their throughput — an exploration vs. exploitation kind of dilemma.

And like I said, a lot of what makes us learn can be called play: doing things for fun, dreaming up new ideas or reinventing old ones. Consequently, it often leads to dead-ends; best practices are "best" for a reason, so unless we are struck by unusual inspiration, we will probably not reimagine the problem so radically as to arrive at a genuinely better solution. Even so, just as play-hunting is nature's tried and true way of learning how to hunt, so is play-inventing the best method I know of learning how to invent; or, to put it differently, if we don't try to reimagine problems, we'll never arrive at better solutions anyway.

Unfortunately, this takes time, and its utility is indirect. So when we engage in the opposite activity, that of work, where we care about the results rather than the process, it's easy to focus on what's at hand, and forget just how much the process shapes us as people – or how little the results do. Or, what results we could achieve in the future, if only we'd taken the time to grow ourselves, too.

That, I think, is where the meta- prefix becomes relevant. When we write code about other code, we're not exactly solving a problem; rather, we're building a tool or a framework that will facilitate the solution of future problems. As such, we are one step removed from any concrete requirement or burning need, almost by definition; it lets us focus on play, stretch our imagination — and, one might hope, rekindle a passion to our unusual craft.