ruk·si

🐍 Python
Metaclasses

Updated at 2018-06-11 08:41

Metaclasses are powerful, but hard to get right. Consider class decorators first, only go for metaclasses if you have to.

Relationship of object and type classes is magic. object is an instance of type, and type is a subclass of object, this is kind of relationship is magic and can't be defined by the user.

All classes are instances of type. But are subclasses of object

Metaclasses are also subclasses of type. So they act as class factories.

You can use type() to create classes dynamically. You should avoid doing this, but it is the same way metaclasses use type so you should understand this.

def __init__(self, name):
    self.name = name

attributes = dict(
    __init__=__init__,
)
Animal = type('Animal', (object,), attributes)

assert str(Animal) == "<class '__main__.Animal'>"
assert Animal('Dog').name == 'Dog'

Django-like model metaclass example:

class Field:
    storage_name: str

    def __get__(self, instance, owner) -> object:
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)

    def __set__(self, instance, value) -> None:
        # you could do validation here
        setattr(instance, self.storage_name, value)

from collections import OrderedDict
from typing import List, Mapping

class ModelMeta(type):

    @classmethod
    def __prepare__(mcs, name, bases) -> Mapping:
        # mcs = this meta class itself
        # name = name of the target class
        # bases = non-meta classes that the target class extends

        # return the collection where attributes
        # of the extending class will be stored
        return OrderedDict()

    def __init__(cls, name, bases, attributes) -> None:
        # cls = target class; the class that extends this metaclass
        # name = name of the target class
        # bases = non-meta classes that the target class extends
        # attributes = attributes of the target class

        # the following is the same as type(name, bases, attributes)
        super().__init__(name, bases, attributes)
        cls._field_names: List[str] = []
        for key, attr in attributes.items():
            if isinstance(attr, Field):
                type_name = type(attr).__name__
                attr.storage_name = f'_{type_name}#{key}'
                cls._field_names.append(key)

        def greet(self) -> str:
            return f'{self.name}: Hello!'

        cls.greet = greet

class Model(metaclass=ModelMeta):

    @classmethod
    def field_names(cls) -> str:
        for name in cls._field_names:
            yield name

class Person(Model):
    name: str = Field()
    title: str = Field()

    def __init__(self, name: str, title: str) -> None:
        self.name = name
        self.title = title

john = Person('John', 'Coder')
assert john.name == 'John'
assert john.title == 'Coder'
assert vars(john) == {'_Field#name': 'John', '_Field#title': 'Coder'}
assert john.greet() == 'John: Hello!'
john.title = 'CTO'
assert john.title == 'CTO'

alice = Person('Alice', 'BizDev')
assert alice.name == 'Alice'
assert alice.title == 'BizDev'
assert vars(alice) == {'_Field#name': 'Alice', '_Field#title': 'BizDev'}
assert alice.greet() == 'Alice: Hello!'
alice.title = 'CEO'
assert alice.title == 'CEO'

assert list(Person.field_names()) == ['name', 'title']

Sources

  • Fluent Python, Luciano Ramalho