Introduction to Python metaclasses

dandyvica

Dandy Vica

Posted on November 14, 2019

Introduction to Python metaclasses

Python metaclasses are an important, although not always used, metaprogramming feature. It allows, among other things, to dynamically create classes. As you can have an object factory method creating objects, using metaclasses, you can define a class factory.

What is a metaclass ? Basically, it's just a class of a class.

Classes are objects

When a class is instantiated, an object is created:

>>> a = [1,2,3,4,5]
>>> type(a)
<class 'list'>
>>> 
Enter fullscreen mode Exit fullscreen mode

But when the class statement is met by the Python interpreter, it also creates an object, of type type:

>>> class A:
...     pass
... 
>>> type(A)
<class 'type'>
Enter fullscreen mode Exit fullscreen mode

So objects created from a class T are of type T, but classes themselves are also objects of class type.

Being objects, classes can be manipulated directly as any Python object:

# dummy classes
class A:
    """ comment for class A """
    pass


class B:
    """ comment for class B """
    pass


class C:
    """ comment for class C """
    pass

# classes are objects: we can store them into a list
list_of_classes = [A,B,C]

# type of classes are 'type'
for cls in list_of_classes:
    print(type(cls))
    print(cls.__doc__)

# we can define a function whose parameter is a class
def class_name(cls: type) -> str:
    return cls.__name__

# treat classes like objects
list_of_str_classes = [class_name(cls) for cls in list_of_classes]

# we get ['A', 'B', 'C']
print(list_of_str_classes)
Enter fullscreen mode Exit fullscreen mode

Creating classes dynamically

As a class instance is created using their type, we can do the same, with a different syntax, to create classes on the fly.

You're probably used to creating class instances:

>>> a = A()
>>> type(a)
<class '__main__.A'>
>>> b = list()
>>> type(b)
<class 'list'>
>>> 
Enter fullscreen mode Exit fullscreen mode

To create a new class, we need to use the type class with a peculiar syntax.

The simplest way to create a class is:

# create a simple class A dynamically. A is an instance of 'type'
A = type('A', (), {})
print(type(A))

# create an instance of A. a is of type A
a = A()
print(type(a))
Enter fullscreen mode Exit fullscreen mode

The third argument allows to provide attributes to the brand new class:

# create a Person class with default values for its attributes
Person = type('Person', (), {
    'first_name': 'John',
    'last_name' : 'Doe',
    'get_name': lambda self: self.first_name + ' ' + self.last_name
})

# create a Person instance
person = Person()
print(person.get_name())
Enter fullscreen mode Exit fullscreen mode

You can also use the second argument (which is a tuple) to make the new class inheriting from another class.

Metaclasses

A metaclass is a way to customize the way a class is created.
To create a metaclass, you need to subclass the type type. To make a class an instance of a metaclass, you'll use a dedicated syntax with metaclass= argument:

class MyMetaClass(type):
    """ a metaclass derives from the 'type' type """

    def __new__(cls, name, bases, dict):
        """ __new()__ is the class constructor """
        print(f"cls={cls}, name={name}, bases={bases}, dict={dict}")
        return super().__new__(cls, name, bases, dict)

class MyClass(metaclass=MyMetaClass):
    pass

a = MyClass()
print(type(a))
Enter fullscreen mode Exit fullscreen mode

Metaclasses use cases

From the Python official documentation:

The potential uses for metaclasses are boundless. Some ideas that have been explored include enum, logging, interface checking, automatic delegation, automatic property creation, proxies, frameworks, and automatic resource locking/synchronization.

The following example illustrates an automatic attribute creation, used to pretty print instance variables:

import pprint

class PrettyPrinterWrapper(type):
    def __new__(cls, name, bases, dict):
        """ define a new handler attribute a pretty printer instance """
        dict['pp'] = pprint.PrettyPrinter(width=20)

        return super().__new__(cls, name, bases, dict)

class A(metaclass=PrettyPrinterWrapper):
    def __init__(self, my_list: list):
        self.list = my_list

    def __str__(self):
        return self.pp.pformat(self.list)

a = A(["one","two","three","four"])
print(a)
Enter fullscreen mode Exit fullscreen mode

Hope this helps !

Photo by Alina Grubnyak on Unsplash

💖 💪 🙅 🚩
dandyvica
Dandy Vica

Posted on November 14, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related