2.6. Metaclass

2.6.1. Rationale

"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

  • Object is an instance of a class

  • Class is an instance of a Metaclass

../_images/metaclass-instances.png

Figure 2.16. Class is an instance of a metaclass.

2.6.2. How Metaclasses works?

  • 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

name = 'Mark Watney'


def hello():
    print('My name... José Jiménez')


class Astronaut:
    pass


astro = Astronaut()

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

2.6.4. Metaclass Mechanism

class Astronaut:
    pass

astro = Astronaut()
class Astronaut(object):
    pass

astro = Astronaut()
class Astronaut(metaclass=object):
    pass

astro = Astronaut()
class MyMetaclass(type):
    pass

class Astronaut(metaclass=MyMetaclass):
    pass

astro = Astronaut()

2.6.5. 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.6.6. 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/metaclass-class-chain.png

Figure 2.17. Class is an instance of a metaclass.

2.6.7. Method Resolution Order

class Astronaut:
    pass


astro = Astronaut()

isinstance(astro, Astronaut)
# True

isinstance(astro, object)
# True

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


class Astronaut(metaclass=AstroMeta):
    pass


astro = Astronaut()

isinstance(astro, Astronaut)
# True

isinstance(astro, object)
# True

isinstance(astro, AstroMeta)
# False

isinstance(Astronaut, AstroMeta)
# True

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

2.6.8. 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.6.9. Use Case

Listing 2.158. 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)>
Listing 2.159. Abstract Base Class
from abc import ABCMeta, abstractmethod


class Astronaut(metaclass=ABCMeta):

    @abstractmethod
    def say_hello(self):
        pass


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

2.6.10. Metaclass replacements

  • Effectively accomplish the same thing

Listing 2.160. Inheritance and __init__() method
import logging


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


class Astronaut(Logger):
    pass


astro = Astronaut()
print(astro._logger)
# <Logger Astronaut (WARNING)>
Listing 2.161. 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


astro = Astronaut()
print(astro._logger)
# <Logger Astronaut (WARNING)>
Listing 2.162. Inheritance for abstract base class validation
from abc import ABC, abstractmethod


class Astronaut(ABC):

    @abstractmethod
    def say_hello(self):
        pass


astro = Astronaut()
# Traceback (most recent call last):
#     ...
# TypeError: Can't instantiate abstract class Astronaut with abstract methods hello
Listing 2.163. 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.6.11. Assignments

Todo

Create assignments