Python Metaclasses - A blueprint for types

Everything in Python is an object. In order to create an object, a blueprint of the object must first be declared. Like most object-oriented languages, blueprints are declared in the form of a class. In order to create an object, one must first call the class's constructor. For example:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

person = Person('Aaron', 'Boman')

Classes are also known as types as they depict the type of an object. In the above example, when instantiating the Person class, it returns a new object of type Person. You can see what type an object is in Python using the type() function.

>>> type(person)
<class Person>

Python also has the ability to programatically generate a class. To create the same Person class programatically, use the second signature to the type() function:

# create the init method
def person_initializer(self, first_name, last_name):
    self.first_name = first_name
    self.last_name = last_name

# create the class
Person = type('Person', (), {'__init__': person_initializer})

person = Person('Aaron', 'Boman')
print type(person)
#  outputs: <class 'Person'>

The above will create a class which will behave just like declaring a class with the class keyword. The second signature for type() is type(name, bases, attributes) where

  • name: is the name of the type (or class) to create
  • bases: is a tuple of classes to inherit from
  • attributes: is a dictionary of class-level attributes. Note how __init__ is being declared as a class-level attribute.

So, the type() function serves two purposes:

  • to determine what type an object is
  • to create a new type

Metaclasses

Since everything in Python is an object, that means that classes are objects too. That definitely blew my mind the first time I had heard it. If classes are objects, that must mean that type is a class? Yes, that's absolutely correct. And, since type is a class, you can inherit from that class. What is a class that inherits from type? Why, it's a blueprint for types, of course!

A class that creates types (or classes) is called a metaclass. To create one, simply declare a new class that inherits from type:

class MyMetaClass(type):
    pass

The new metaclass can then be used just like type():

>>> MyClass = MyMetaClass('MyClass', (), {})
>>> my_instance = MyClass()
>>> type(my_instance)
<class MyClass>

It is also possible to instruct a class declared with the class keyword to use a particular metaclass in order to construct that class. The way to do this is dependent upon which version of Python is being used. I'll show Python2 first followed by Python3.

# in python2
class MyClass(object):
    __metaclass__ = MyMetaClass

# in python 3
class MyClass(metaclass=MyMetaClass):
    pass

Now, when the MyClass class is constructed, it will use MyMetaClass to construct it. This is absolutely pointless unless the metaclass extends the type class in some way. This is where the fun begins.

Extending the type class

If you declare a __new__ method on a metaclass, it controls how the class is constructed. The signature for __new__ is
__new__(mcs, name, bases, attrs) where mcs is the metaclass instance and the remaining three parameters are exactly the same as the type() function. One use for overriding the __new__() method is to dynamically generate methods for the class.

def functon_factory(method_name, something_to_return):
    def created(self):
        return something_to_return
    created.__name__ = method_name
    return created

class MyMetaClass(type):
    def __new__(mcs, name, bases, attrs):
        for name, message in (('method1', 'Hello'), ('method2', 'How are you?')):
            attrs[name] = functon_factory(name, message)
        # python 3 use of super() here
        return super().__new__(mcs, name, bases, attrs)

Then, the metaclass can be used to construct a new class. The new class will automatically have two methods: method1 and method2.

class MyClass(metaclass=MyMetaClass):
    pass
# ...
>>> mc = MyClass()
>>> mc.method1()
'Hello'
>>> mc.method2()
'How are you?'

Note: you must return a newly created object from the __new__() method! You will always want to call super() in some way, which will return a new class instance. Then, you will want to return that instance before the method ends.

A real world use of __new__ in metaclasses

If you're familiar with Django at all, Django allows you to specify a model of a table in a database by inheriting a class named Model. The Model class expects that you specify the fields for the table by setting class-level attributes. The name of the attribute becomes the name of the field in the table. The value is expected an instance of one of the Field types. The field type describes how the database should store the field value. For example,

class Employee(Model):
    first_name = CharField(max_length=255)
    last_name = CharField(max_length=255)

This will create an employee table in a database with two varchar fields that hold an employee's first and last name. The Employee class itself can be instantiated, however, it doesn't behave like a normal class

>>> e1 = Employee(first_name='Aaron', last_name='Boman')
>>> e1.first_name
'Aaron'

Without specifying an __init__ method, I am able to instantiate an Employee instance with the first_name and last_name keyword arguments. How is this done? Metaclasses of course! By overriding the __new__ method on the ModelMeta class, Django is able to first see that there are two fields on the employee table in the database, and second, see how to create the constructor for the Employee class. Neat, huh?

Wanna see the implementation? Have a look at Django's source.

Other uses of a metaclass

There have been a few times where I want to create a singleton instance of an object. A singleton instance is like memoization for the constructor: whenever the constructor is called with the same arguments, I want the same instance to be returned. This is a piece of cake with metaclasses.

class PersonMeta(type):
    def __init__(cls, first_name, last_name):
        super().__init__(*args, **kwargs)
        cls._instance_map = {}

    def __call__(cls, first_name, last_name):
        key = (first_name, last_name)
        if key not in cls._instance_map:
            new_instance = super().__call__(first_name, last_name)
            cls._instance_map[key] = new_instance
        return cls._instance_map[key]

class Person(metaclass=PersonMeta):
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

This then allows me to do something like

>>> person1 = Person('Aaron', 'Boman')
>>> person2 = Person('Aaron', 'Boman')
>>> person1 is person2
True

Definining the __init__ method instructs how to initialize the class. In this case, I'm creating a class-level variable, _instance_map; a dictionary that maps the constructor's parameters to an instance that was constructed with those parameters. Then, by defining the __call__ method, I'm actually intercepting the call to the class's constructor. In it, I check to see if an object has been instantiated with the values passed to the constructor. If it has, it returns the already existing instance. Otherwise, it continues on to the original constructor, creates the new instance, then updates the cache to point to the new instance. Finally, the new instance is returned.

Note: I could have used *args and **kwargs for the constructor's argument and made this a broad-use, but in order to use the dictionary from kwargs, it will first have to be converted to a sorted tuple of key value pairs and this may not be desirable as it could potentially be a performance hit. Thus, I elected to hard-code the constructor parameters in the above example.

Another use for a metaclass is to create a classproperty. If you haven't ever heard of the property() function, it is a function that allows you to declare a method on a class as a descriptor. Basically, this is just a fancy way of saying that you can treat a method like an attribute. For example:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return '{} {}'.format(self.first_name, self.last_name)

>>> person = Person('Aaron', 'Boman')
>>> # note how this isn't a function call!
>>> person.full_name
'Aaron Boman'

Sometimes it is desirable to create a class-level property for similar reasons. Python doesn't support this by default, but it can be done with metaclasses:

class ToolsetConfigMeta(type):
    def __init__(cls, *args, **kwargs):
        # declare the default values here in case a base
        # class doesn't declare them as full_toolset below
        # will throw an error if the attributes are not declared.
        cls.toolset = ''
        cls.version = ''
        super().__init__(*args, **kwargs)

    @property
    def full_toolset(cls):
        return '{}-{}'.format(cls.toolset, cls.version)


class ToolsetConfig(metaclass=ToolsetConfigMeta):
    toolset = 'msvc'
    version = '12.0'


>>> ToolsetConfig.full_toolset
'msvc-12.0'

Simply declare the attributes in the __init__ (don't forget to call super()!), then declare the method decorated with the property function, and that's it! Computed class properties just like object properties.

Warnings

Metaclasses are really neat. They provide a way to create custom types purely in Python. However, with all of this great power comes great responsibility. Much to my dismay, there aren't too many real-world uses for them. As always, code should be readable and understandable. Metaclasses throw a level of indirection in understanding code that most Python programmers are not familiar with and therefore reduces the readability of the code. A general rule of thumb for using metaclasses should be: Does it make the end-user API substantially easier to use? In the case of the Django ORM API, I would say that's a definite yes.


Comments