Metaclasses made simple

You probably have added the __repr__ method to a Python class:

class Cat:
    def __repr__(self):
        return "<😻>"

This gives a customized representation to the instance at the command line:

>>> Cat()
<😻>

However, this __repr__ has no effect on the class itself:

>>> Cat
<class 'meta_cats.Cat'>

The reason is that a method defined on a class acts on instances of the class but not the class itself. But what if the class is itself an instance? An instance of what class? Python provides an easy way to find out:

>>> type(Cat)
<class 'type'>

So we see that a class is an instance of the built-in class type. This means that we should be able to subclass type, add methods to it, and instantiate classes using our customized type. Here is a small example:

class MetaCat(type):
    def __repr__(cls):
        return cls.repr_string

Note that MetaCat descends from type and has a __repr__ method that has a parameter cls. This is to emphasize that instances of MetaCat are themselves classes.

We can instantiate a class in the usual way, except for an additional optional parameter.

class FancyCat(metaclass=MetaCat):
    repr_string = "<class 😻>"

The metaclass parameter in the class definition instructs Python to make an instance of MetaCat instead of type. Let’s verify this:

>>> type(FancyCat) is MetaCat
True

Finally, let’s see what happens when we evaluate the class FancyCat on the Python command line:

>>> FancyCat
<class 😻>

Indeed, the __repr__ method defined on MetaCat is called, and returns a customized representaion.

To summarize, every class in Python is an instance of some class, often referred to as a metaclass to distinguish it from other classes. type is the built-in metaclass of which int, dict, and your own classes are instances. And just like with most classes, you can subclass type to make custom metaclasses, analogous to subclassing object to make custom classes.

A C style enum in Python

The enum standard module allows you to make names whose values are arbitrary:

from enum import Enum
class Color(Enum):
    RED = 0
    GREEN = 1
    BLUE = 2

It is convenient to use Color.RED in code because the compiler/interpreter will ensure against typos, and sensibly chosen names will enhance readibility. However, unlike in C, Python’s enum forces you to assign values. It would be nice if we can simply write

class Color(Enum):
    RED
    GREEN
    BLUE

and have the attributes autoincrement. It turns out that with a little metaprogramming, we can.

Python 3 introduced __prepare__, which is a method on the metaclass invoked before class creation. It by default returns a dictionary, which will contain the class attributes required for class creation. For example, the statement RED = 1 above has the effect of adding a key/value pair to this dictionary: prepared_dict['RED'] = 1.

The idea is to override __prepare__ and return a customized dictonary that assigns each key the next available integer. Here is an implementation of this dictionary:

class AutoDict(dict):
    def __init__(self):
        self.count = 0

    def __getitem__(self, key):
        if key.startswith("__") and key.endswith("__"):
            return super().__getitem__(key)

        self[key] = self.count
        self.count += 1

Under a class definition, for each variable that appears in an expression - as opposed to the left hand side of an assignment - Python will look up its value first in this dictionary, calling the __getitem__ method, which in turn assigns to it the next integer.

Then we create a metaclass with a __prepare__ method that returns an instance of AutoDict,

class EnumMeta(type):
    def __prepare__(*args):
        return AutoDict()

and a base class that instantiates EnumMeta

class Enum(metaclass=EnumMeta):
    pass

Let’s try running the example above:

>>> class Color(Enum):
...     RED
...     GREEN
...     BLUE
>>> Color.RED
0

>>> Color.GREEN
1

>>> Color.BLUE
2

So we have an enum class that automatically assign and increment values to its attributes.

Balance brackets with the call stack

A classic algorithm problem is to determine whether a string containing some sequences of brackets (possibly of different types) is balanced, that is, whether the string is a valid mathematical expression. For example, () is balanced, and )( is not.

The typical solution involves iterating thru the string and maintaining a stack - pushing an opening bracket onto the stack when encountered, and popping it off when the corresponding closing bracket is seen.

But why not, instead of making our own stack, use the call stack? Python’s inspect module allows us to do just that.

import inspect

This module’s stack function returns the call stack from top to bottom, so the previous frame is stack()[1].

We define a function _stack that iterates through a list of characters. Upon seeing an opening bracket, it pushes onto the call stack by calling itself, and in case of a closing bracket, pops by simply returning. By inspecting the previous frame, we ensure that the closing bracket matches the most recent opening bracket, and that there are no opening brackets left on the call stack at the end,

def _stack(chars):
    """Push/pop frames to/from call stack."""
    while chars:
        char = chars.pop(0)
        if char in BRACKETS.values():
            _stack(chars)  # push
        elif char in BRACKETS:
            previous = inspect.stack()[1]
            if (
                previous.function != "_stack"
                or previous.frame.f_locals["char"] != BRACKETS[char]
            ):
                raise IndexError
            return  # pop

    if inspect.stack()[1].function == "_stack":  # check no brackets remain
        raise IndexError

where BRACKETS is a mapping of closing to opening brackets:

BRACKETS = {")": "(", "]": "[", "}": "{"}

In the event of an unbalanced expression, _stack raises IndexError, hence we define is_balanced that wraps _stack inside of a try/except:

def is_balanced(string):
    """Check whether brackets in given string balanced."""
    try:
        _stack(list(string))
    except IndexError:
        return False
    else:
        return True

Some test examples:

>>> is_balanced("[]")
True
>>> is_balanced(")(")
False
>>> is_balanced("{[}]")
False
>>> is_balanced("(")
False