21. 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

21.1. Metaclass mechanism

21.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 38. Class is an instance of a metaclass.

21.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

21.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

21.2. Type and objects

21.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 39. Class is an instance of a metaclass.

21.2.2. Objects

Listing 358. 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'>

21.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 359. 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

21.4. Factories

21.4.1. Object factory

Listing 360. 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

21.4.2. Class Factory

Listing 361. 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

21.5. Metaclass replacements

  • Effectively accomplish the same thing

21.5.1. Inheritance

class Iris:
    kingdom = 'Plantae'

class Setosa(Iris):
    pass

Setosa.kingdom
# Plantae

21.5.2. Class Decorator

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

@add_kingdom
class Iris:
    pass

Iris.kingdom
# Plantae