11. Metaclass

“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

11.1. Metaclass mechanism

11.1.1. What are Metaclasses?

  • Class is an instance of a Metaclass

  • Class defines how an instance of the class behaves

  • Metaclass defines how a class behaves

../_images/metaclass-instances.png

Figure 11.1. Class is an instance of a metaclass.

11.1.2. How Metaclasses works?

  • Instances are created by calling the class

  • Python creates a new class (when it executes the ‘class’ statement) by calling the metaclass

  • Combined with the normal __init__ and __new__ methods

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

11.1.3. Example use of Metaclasses

  • 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

11.2. Type and objects

11.2.1. Types

type(int)       # <class 'type'>
type(float)     # <class 'type'>
type(dict)      # <class 'type'>
type(list)      # <class 'type'>
type(tuple)     # <class 'type'>

type(type)      # <class 'type'>
../_images/metaclass-class-chain.png

Figure 11.2. Class is an instance of a metaclass.

11.2.2. Objects

Listing 11.1. Metaclass
class Iris:
    pass

flower = Iris()

isinstance(flower, Iris)    # True
isinstance(flower, object)  # True

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

11.3. Example

class Iris:
    pass

def new(cls):
    obj = object.__new__(cls)
    obj.kingdom = 'Plantae'
    return obj

Iris.__new__ = new

setosa = Iris()
versicolor = Iris()

setosa.kingdom      # Plantae
versicolor.kingdom  # Plantae
Listing 11.2. Spoiler alert: This doesn’t work!
def new(cls):
    obj = type.__new__(cls)
    obj.kingdom = 'Plantae'
    return obj

type.__new__ = new
# TypeError: can't set attributes of built-in/extension type 'type'
class Iris(type):
    def __new__(cls, *args, **kwargs):
        obj = super().__new__(cls, *args, **kwargs)
        obj.kingdom = 'Plantae'
        return obj

class Setosa(metaclass=Iris):
    pass

class Virginica(metaclass=Iris):
    pass

class Versicolor(metaclass=Iris):
    pass


Setosa.kingdom         # Plantae
Virginica.kingdom      # Plantae
Versicolor.kingdom     # Plantae

11.4. Factories

11.4.1. Object factory

Listing 11.3. Object factory
class Iris:
    def __init__(self):
        self.kingdom = 'Plantae'


setosa = Iris()
versicolor = Iris()
virginica = Iris()

setosa.kingdom          # Plantae
versicolor.kingdom      # Plantae
virginica.kingdom       # Plantae

11.4.2. Class Factory

Listing 11.4. Class Factory
class Iris(type):
    def __init__(cls, *args, **kwargs):
        cls.kingdom = 'Plantae'


 class Setosa(metaclass=Iris):
    pass

class Virginica(metaclass=Iris):
    pass

class Versicolor(metaclass=Iris):
    pass


Setosa.kingdom         # Plantae
Virginica.kingdom      # Plantae
Versicolor.kingdom     # Plantae

11.5. Metaclass replacements

  • Effectively accomplish the same thing

11.5.1. Inheritance

class Iris:
    kingdom = 'Plantae'

class Setosa(Iris):
    pass

Setosa.kingdom
# Plantae

11.5.2. Class Decorator

def add_kingdom(cls):
    class NewIris(cls):
        kingdom = 'Plantae'
    return NewIris

@add_kingdom
class Iris:
    pass

Iris.kingdom
# Plantae