2.8. Metaclass

  • Object is an instance of a class

  • Class is an instance of a Metaclass

../../_images/oop-metaclass-inheritance1.png

Figure 2.55. Object is an instance of a Class. Class is an instance of a Metaclass. Metaclass is an instance of a type. Type is an instance of a type.

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't. The people who actually need them know with certainty that they need them, and don't need an explanation about why.

—Tim Peters

2.8.1. About

When a class definition is executed, the following steps occur:

  1. MRO entries are resolved;

  2. the appropriate metaclass is determined;

  3. the class namespace is prepared;

  4. the class body is executed;

  5. the class object is created.

When using the default metaclass type, or any metaclass that ultimately calls type.__new__, the following additional customisation steps are invoked after creating the class object:

  1. type.__new__ collects all of the descriptors in the class namespace that define a __set_name__() method;

  2. all of these __set_name__ methods are called with the class being defined and the assigned name of that particular descriptor;

  3. the __init_subclass__() hook is called on the immediate parent of the new class in its method resolution order. [2]

Class Definition:

class MyClass:
    pass
MyClass = type('MyClass', (), {})

Class Attributes:

class MyClass:
    myattr = 1
MyClass = type('MyClass', (), {'myattr': 1})

Class Methods:

class MyClass:
    def mymethod(self):
        pass
def mymethod(self):
    pass

MyClass = type('MyClass', (), {'mymethod': mymethod})

Class Inheritance:

class Parent:
    pass


class MyClass(Parent):
    pass
MyClass = type('MyClass', (Parent,), {})

Recap:

class Parent:
    pass


class MyClass(Parent):
    myattr = 1

    def mymethod(self):
        pass
MyClass = type('MyClass', (Parent,), {'myattr': 1, 'mymethod': mymethod})

Create Classes Dynamically:

for classname in ['Astronaut', 'Cosmonaut', 'Taikonaut']:
    globals()[classname] = type(classname, (), {})

2.8.2. Syntax

class MyMeta(type):
    pass

class MyClass(metaclass=MyMeta):
    pass

class MySubclass(MyClass):
    pass


myinstance = MySubclass()


type(MyMeta)
# <class 'type'>

type(MyClass)
# <class '__main__.MyMeta'>

type(MySubclass)
# <class '__main__.MyMeta'>

type(myinstance)
# <class '__main__.MySubclass'>

2.8.3. Metaclasses

  • Is a callable which returns a class

  • Instances are created by calling the class

  • Classes are created by calling the metaclass (when it executes the class statement)

  • Combined with the normal __init__ and __new__ methods

  • Class defines how an object behaves

  • Metaclass defines how a class behaves

class MyClass:
    pass
class MyClass(object):
    pass
class MyMeta(type):
    pass


class MyClass(metaclass=MyMeta):
    pass
class MyMeta(type):
    def __new__(mcs, classname, bases, attrs):
        return type(classname, bases, attrs)


class MyClass(metaclass=MyMeta):
    pass
def mymeta(classname, bases, attrs):
    return type(classname, bases, attrs)


class MyClass(metaclass=mymeta):
    pass

2.8.4. Usage

  • Metaclasses allow you to do 'extra things' when creating a class

  • Allow customization of class instantiation

  • Most commonly used as a class-factory

  • Registering the new class with some registry

  • Replace the class with something else entirely

  • Inject logger instance

  • Injecting static fields

  • Ensure subclass implementation

  • Metaclasses run when Python defines class (even if no instance is created)

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. [2]

class MyMeta(type):
    def __new__(mcs, classname, bases, attrs):
        print(locals())
        return type(classname, bases, attrs)


class MyClass(metaclass=MyMeta):
    myattr = 1

    def mymethod(self):
        pass

# {'self': <class '__main__.MyMeta'>,
#  'classname': 'MyClass',
#  'bases': (),
#  'attrs': {'__module__': '__main__',
#            '__qualname__': 'MyClass',
#            'myattr': 1,
#            'mymethod': <function MyClass.mymethod at 0x10ae39ca0>}}

2.8.5. Keyword Arguments

class MyMeta(type):
    def __new__(mcs, classname, bases, attrs, myvar):
        if myvar:
            ...
        return type(classname, bases, attrs)


class MyClass(metaclass=MyMeta, myvar=True):
    pass

2.8.6. Methods

  • __prepare__(metacls, name, bases, **kwargs) -> dict - on class namespace initialization

  • __new__(mcs, classname, bases, attrs) -> cls - before class creation

  • __init__(self, name, bases, attrs) -> None - after class creation

  • __call__(self, *args, **kwargs) - allows custom behavior when the class is called

Once the appropriate metaclass has been identified, then the class namespace is prepared. If the metaclass has a __prepare__ attribute, it is called as namespace = metaclass.__prepare__(name, bases, **kwds) (where the additional keyword arguments, if any, come from the class definition). The __prepare__ method should be implemented as a classmethod(). The namespace returned by __prepare__ is passed in to __new__, but when the final class object is created the namespace is copied into a new dict. If the metaclass has no __prepare__ attribute, then the class namespace is initialised as an empty ordered mapping. [1]

class MyMeta(type):
    @classmethod
    def __prepare__(metacls, name, bases) -> dict:
        pass

    def __new__(mcs, classname, bases, attrs) -> Any:
        pass

    def __init__(self, *args, **kwargs) -> None:
        pass

    def __call__(self, *args, **kwargs) -> Any:
        pass

2.8.7. Example

import logging


class Logger(type):
    def __init__(cls, *args, **kwargs):
        cls._logger = logging.getLogger(cls.__name__)


class Astronaut(metaclass=Logger):
    pass


class Cosmonaut(metaclass=Logger):
    pass



print(Astronaut._logger)
# <Logger Astronaut (WARNING)>

print(Cosmonaut._logger)
# <Logger Cosmonaut (WARNING)>

2.8.8. Type Metaclass

type(1)           # <class 'int'>
type(int)         # <class 'type'>
type(type)        # <class 'type'>
type(float)       # <class 'type'>
type(bool)        # <class 'type'>
type(str)         # <class 'type'>
type(bytes)       # <class 'type'>
type(list)        # <class 'type'>
type(tuple)       # <class 'type'>
type(set)         # <class 'type'>
type(frozenset)   # <class 'type'>
type(dict)        # <class 'type'>
type(object)      # <class 'type'>
type(type)        # <class 'type'>
../../_images/oop-metaclass-diagram1.png

Figure 2.56. Object is an instance of a Class. Class is an instance of a Metaclass. Metaclass is an instance of a type. Type is an instance of a type.

class MyClass:
    pass


my = MyClass()

MyClass.__class__.__bases__
# (<class 'object'>,)

my.__class__.__bases__
# (<class 'object'>,)
class MyClass(object):
    pass


my = MyClass()

MyClass.__class__.__bases__
# (<class 'object'>,)

my.__class__.__bases__
# (<class 'object'>,)
class MyMeta(type):
    pass

class MyClass(metaclass=MyMeta):
    pass


my = MyClass()

MyClass.__class__.__bases__
# (<class 'type'>,)

my.__class__.__bases__
# (<class 'object'>,)
class MyMeta(type):
    def __new__(mcs, classname, bases, attrs):
        return type(classname, bases, attrs)


class MyClass(metaclass=MyMeta):
    pass

2.8.9. Method Resolution Order

class Astronaut:
    pass


mark = Astronaut()

isinstance(mark, Astronaut)
# True

isinstance(mark, object)
# True

Astronaut.__mro__
# (<class '__main__.Astronaut'>, <class 'object'>)
class AstroMeta(type):
    pass


class Astronaut(metaclass=AstroMeta):
    pass


mark = Astronaut()

isinstance(mark, Astronaut)
# True

isinstance(mark, object)
# True

isinstance(mark, AstroMeta)
# False

isinstance(Astronaut, AstroMeta)
# True

Astronaut.__mro__
# (<class '__main__.Astronaut'>, <class 'object'>)

2.8.10. Example

import logging


def new(cls):
    obj = super().__new__(cls)
    obj._logger = logging.getLogger(cls.__name__)
    return obj


class Astronaut:
    pass


Astronaut.__new__ = new

mark = Astronaut()
melissa = Astronaut()

print(mark._logger)
# <Logger Astronaut (WARNING)>

print(melissa._logger)
# <Logger Astronaut (WARNING)>
import logging


def new(cls):
    obj = super().__new__(cls)
    obj._logger = logging.getLogger(cls.__name__)
    return obj

str.__new__ = new
# Traceback (most recent call last):
# TypeError: can't set attributes of built-in/extension type 'str'
import logging


def new(cls):
    obj = super().__new__(cls)
    obj._logger = logging.getLogger(cls.__name__)
    return obj

type.__new__ = new
# Traceback (most recent call last):
# TypeError: can't set attributes of built-in/extension type 'type'

2.8.11. Use Case

Injecting logger instance:

import logging


class Logger(type):
    def __init__(cls, *args, **kwargs):
        cls._logger = logging.getLogger(cls.__name__)


class Astronaut(metaclass=Logger):
    pass


class Cosmonaut(metaclass=Logger):
    pass



print(Astronaut._logger)
# <Logger Astronaut (WARNING)>

print(Cosmonaut._logger)
# <Logger Cosmonaut (WARNING)>

Abstract Base Class:

from abc import ABCMeta, abstractmethod


class Astronaut(metaclass=ABCMeta):

    @abstractmethod
    def say_hello(self):
        pass


mark = Astronaut()
# Traceback (most recent call last):
# TypeError: Can't instantiate abstract class Astronaut with abstract methods say_hello
class EventListener(type):
    listeners: dict[str, list[callable]] = {}

    @classmethod
    def register(cls, *clsnames):
        def wrapper(func):
            for clsname in clsnames:
                if clsname not in cls.listeners:
                    cls.listeners[clsname] = []
                cls.listeners[clsname] += [func]
        return wrapper

    def __new__(mcs, classname, bases, attrs):
        for listener in mcs.listeners.get(classname, []):
            listener.__call__(classname, bases, attrs)
        return type(classname, bases, attrs)


@EventListener.register('Astronaut')
def hello_class(clsname, bases, attrs):
    print(f'\n\nHello new class {clsname}\n')


@EventListener.register('Astronaut', 'Person')
def print_name(clsname, bases, attrs):
    print('\nNew class created')
    print('Classname:', clsname)
    print('Bases:', bases)
    print('Attrs:', attrs)


class Person(metaclass=EventListener):
    pass


class Astronaut(Person, metaclass=EventListener):
    pass

# New class created
# Classname: Person
# Bases: ()
# Attrs: {'__module__': '__main__', '__qualname__': 'Person'}
#
#
# Hello new class Astronaut
#
#
# New class created
# Classname: Astronaut
# Bases: (<class '__main__.Person'>,)
# Attrs: {'__module__': '__main__', '__qualname__': 'Astronaut'}
class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class MyClass(metaclass=Singleton):
    pass
class Final(type):
    def __new__(mcs, classname, base, attrs):
        for cls in base:
            if isinstance(cls, Final):
                raise TypeError(f'{cls.__name__} is final and cannot inherit from it')
        return type.__new__(mcs, classname, base, attrs)


class MyClass(metaclass=Final):
    pass


class SomeOtherClass(MyClass):
   pass

# Traceback (most recent call last):
# TypeError: MyClass is final and cannot inherit from it

Create classes dynamically:

DATA = [
    ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
    (5.8, 2.7, 5.1, 1.9, 'virginica'),
    (5.1, 3.5, 1.4, 0.2, 'setosa'),
    (5.7, 2.8, 4.1, 1.3, 'versicolor'),
    (6.3, 2.9, 5.6, 1.8, 'virginica'),
    (6.4, 3.2, 4.5, 1.5, 'versicolor'),
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
    (7.0, 3.2, 4.7, 1.4, 'versicolor'),
    (7.6, 3.0, 6.6, 2.1, 'virginica'),
    (4.9, 3.0, 1.4, 0.2, 'setosa'),]


class Iris:
    pass


for *data, species in DATA[1:]:
    species = species.capitalize()
    if species not in globals():
        globals()[species] = type(species, (Iris,), {})

Access static fields of a class, before creating instance:

from django.db import models

# class Model(metaclass=...)
#     ...


class Person(models.Model):
    firstname = models.CharField(max_length=255)
    lastname = models.CharField(max_length=255)

2.8.12. Metaclass replacements

  • Effectively accomplish the same thing

Inheritance and __init__() method:

import logging


class Logger:
    def __init__(self):
        self._logger = logging.getLogger(self.__class__.__name__)


class Astronaut(Logger):
    pass


mark = Astronaut()
print(mark._logger)
# <Logger Astronaut (WARNING)>

Inheritance and __new__() method:

import logging


class Logger:
    def __new__(cls, *args, **kwargs):
        obj = super().__new__(cls)
        obj._logger = logging.getLogger(obj.__class__.__name__)
        return obj


class Astronaut(Logger):
    pass


mark = Astronaut()
print(mark._logger)
# <Logger Astronaut (WARNING)>

Inheritance for abstract base class validation:

from abc import ABC, abstractmethod


class Astronaut(ABC):

    @abstractmethod
    def say_hello(self):
        pass


mark = Astronaut()
# Traceback (most recent call last):
# TypeError: Can't instantiate abstract class Astronaut with abstract methods hello

Class Decorator:

import logging


def add_logger(cls):
    class Wrapper(cls):
        _logger = logging.getLogger(cls.__name__)
    return Wrapper


@add_logger
class Astronaut:
    pass


print(Astronaut._logger)
# <Logger Astronaut (WARNING)>

2.8.13. References