🐍 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