Python

Метакласи — Динамічне створення класів під капотом CPython

Глибокий розбір динамічної природи класів у Python. Використання type() для створення класів на льоту, створення та налаштування власних метакласів, життєвий цикл __new__ та __init__, а також сучасна альтернатива у вигляді __init_subclass__ (PEP 487).

Метакласи — Динамічне створення класів під капотом CPython

Проблема: коли класів занадто багато, або потрібна тотальна стандартизація

У більшості мов програмування (наприклад, Java, C++ чи C#) клас — це статична, незмінна інструкція, за якою компілятор створює об'єкти в пам'яті. Ви не можете під час виконання програми змінити структуру класу, додати нові методи чи «на ходу» згенерувати абсолютно новий тип даних. Усі типи фіксуються на етапі компіляції.

Але Python — динамічна мова. Тут класи не просто описують об'єкти. Класи самі є об'єктами. Вони існують у пам'яті як повноцінні сутності типу type, які можна передавати як аргументи функцій, повертати, модифікувати та створювати в реальному часі.

Уявіть ситуацію: ви будуєте великий фреймворк для розробки API чи ORM. Користувачі вашого фреймворку будуть створювати сотні класів моделей. Ви хочете гарантувати, що:

  • Кожен клас моделі обов'язково має унікальний атрибут __tablename__.
  • Усі назви методів записані виключно в snake_case.
  • Будь-який оголошений клас автоматично реєструється в центральному списку плагінів (Plugin Registry).

Можна спробувати перевіряти це під час запуску програми через рефлексію, або написати складні декоратори для кожного класу. Але декоратор легко забути написати, а перевірки після запуску — це втрачений час.

Саме для таких завдань — повного контролю над створенням, структурою та поведінкою самих класів — існують метакласи. Якщо спрощено:

Звичайний клас — це креслення для створення екземплярів об'єктів. Метаклас — це креслення для створення самих класів.

Перед тим як перейти до метакласів, ми повинні детально розібратися з тим, як Python створює класи під капотом і чому класи — це звичайні об'єкти.


Частина I: Динамічна природа класів (Класи як об'єкти)

Коли інтерпретатор Python бачить ключове слово class, він виконує код у тілі класу в окремому просторі імен (тимчасовому словнику), а потім викликає конструктор спеціального класу, який створює об'єкт нашого класу.

Доведемо, що клас — це об'єкт:

class Dummy:
    pass

print(type(Dummy))  # Виведе: <class 'type'>

Клас Dummy — це звичайний об'єкт у купі (heap) пам'яті. Він є екземпляром вбудованого класу type. Це означає, що type є метакласом за замовчуванням для всіх класів у Python.

Це створює цікаву подвійність функції type(). Вона виконує дві абсолютно різні ролі залежно від кількості аргументів, які ви їй передаєте:

  1. type(obj) — приймає один аргумент і повертає клас (тип) об'єкта.
  2. type(name, bases, dict) — приймає три аргументи й створює новий об'єкт класу динамічно «на льоту».

Параметри конструктора type

Коли ми створюємо клас динамічно, конструктор type очікує три параметри:

name
str
Ім'я класу, яке буде записано в його магічний атрибут __name__.
bases
tuple[type, ...]
Кортеж базових (батьківських) класів, від яких буде наслідуватися новий клас.
dict
dict[str, Any]
Словник, що описує простір імен класу (його атрибути та методи). Це майбутній __dict__ нашого класу.

Створення класу вручну

Давайте порівняємо стандартне оголошення класу та його створення за допомогою type():

# dynamic_class.py

# 1. Класичний опис класу
class User:
    species = "Homo sapiens"

    def __init__(self, username: str):
        self.username = username

    def greet(self) -> str:
        return f"Вітаю, я {self.username}!"


# 2. Динамічне створення еквівалентного класу через type()
def user_init(self, username: str):
    self.username = username

def user_greet(self) -> str:
    return f"Вітаю, я {self.username}!"

# Створюємо клас і записуємо в змінну DynamicUser
DynamicUser = type(
    "User",                 # Ім'я класу
    (object,),              # Кортеж базових класів (наслідуємось від object)
    {
        "species": "Homo sapiens",   # Атрибут класу
        "__init__": user_init,       # Метод ініціалізації
        "greet": user_greet,         # Звичайний метод
    }
)

Тепер ми можемо використовувати обидва класи абсолютно однаково. Вони створюють об'єкти з ідентичною поведінкою та структурою:

# Продовження dynamic_class.py

# Створюємо екземпляр звичайного класу
user1 = User("arakviel")
print(f"User 1: {user1.greet()} ({user1.species})")

# Створюємо екземпляр динамічного класу
user2 = DynamicUser("kostyl_dev")
print(f"User 2: {user2.greet()} ({user2.species})")

# Перевіряємо їхні класи
print(f"Тип User: {type(User)}")
print(f"Тип DynamicUser: {type(DynamicUser)}")
print(f"Чи є user2 екземпляром DynamicUser? {isinstance(user2, DynamicUser)}")
python dynamic_class.py
$ python dynamic_class.py
User 1: Вітаю, я arakviel! (Homo sapiens)
User 2: Вітаю, я kostyl_dev! (Homo sapiens)
Тип User: <class 'type'>
Тип DynamicUser: <class 'type'>
Чи є user2 екземпляром DynamicUser? True

Зверніть увагу: змінна, у яку ви записуєте динамічно створений клас (у нашому випадку DynamicUser), не обов'язково має збігатися з першим аргументом конструктора ("User"). Перший аргумент — це саме ім'я об'єкта класу в системі типів (__name__), а змінна — це просто посилання на цей об'єкт.

Коли Python бачить декларацію class MyClass: ..., під капотом відбувається саме виклик type("MyClass", bases, dict). Різниця лише в тому, що синтаксис class є більш читабельним декларативним «цукром».

Частина II: Власний метаклас — контроль над народженням класу

Ієрархія типів у Python

Перш ніж писати власний метаклас, треба чітко зрозуміти ієрархію типів у Python. Вона виглядає як матрьошка:

  • Звичайний екземпляр (obj) є об'єктом свого класу (class MyClass).
  • Клас (class MyClass) є об'єктом свого метакласу (type).
  • Метаклас (type) є об'єктом самого себе (type(type) is type).

Ця рекурсивна замкнутість — одна з найелегантніших особливостей моделі даних Python:

# type_hierarchy.py

class Animal:
    pass

class Dog(Animal):
    pass

d = Dog()

# Звичайна ієрархія екземпляра
print(type(d))          # <class '__main__.Dog'>
print(type(Dog))        # <class 'type'>       ← клас Dog є об'єктом type
print(type(Animal))     # <class 'type'>
print(type(type))       # <class 'type'>       ← type є об'єктом самого себе!

# Ієрархія наслідування (між класами)
print(Dog.__bases__)    # (<class '__main__.Animal'>,)
print(Animal.__bases__) # (<class 'object'>,)
print(type.__bases__)   # (<class 'object'>,) ← type наслідується від object
print(object.__bases__) # ()                   ← object є коренем ієрархії
object — корінь ієрархії наслідування. type — корінь ієрархії метакласів. При цьому isinstance(type, object) == True і isinstance(object, type) == True — вони взаємозалежні.

Написання першого метакласу

Метаклас — це клас, що успадковується від type. Так само, як звичайні класи успадковуються від object. Щоб метаклас вступив у дію, ми вказуємо metaclass=... у заголовку класу:

class MyMeta(type):
    ...

class MyClass(metaclass=MyMeta):
    ...

Тепер MyClass є об'єктом MyMeta, а не type. Метаклас MyMeta отримає повний контроль над тим, що відбуватиметься в момент, коли Python «читатиме» і «компілюватиме» тіло класу MyClass.

Два ключових методи, які слід перевизначати у метакласі:

__new__(mcs, name, bases, namespace)
статичний конструктор
Викликається першим при створенні класу. Приймає ім'я класу (name), кортеж батьківських класів (bases) та словник простору імен (namespace — майбутній __dict__). Повертає новий об'єкт класу. Тут зручно модифікуватиnamespace до того, як клас буде сформований. Параметр mcs — традиційне позначення для метакласу (за аналогією з cls у класметодах).
__init__(cls, name, bases, namespace)
ініціалізатор
Викликається відразу після __new__. Отримує вже готовий об'єкт класу cls. Тут зручно реєструвати клас десь або виконувати дії над уже створеним класом.

Перший метаклас: логування створення класів

Напишемо метаклас, що просто логує факт оголошення кожного нового класу:

# first_metaclass.py

class LoggingMeta(type):
    """Метаклас-спостерігач: логує кожне створення класу."""

    def __new__(mcs, name: str, bases: tuple, namespace: dict):
        print(f"[META __new__] Починаємо створення класу '{name}'")
        print(f"  Базові класи: {[b.__name__ for b in bases]}")
        print(f"  Атрибути простору імен: {[k for k in namespace if not k.startswith('__')]}")

        # Делегуємо реальне створення класу базовому type.__new__
        cls = super().__new__(mcs, name, bases, namespace)
        print(f"[META __new__] Клас '{name}' створено. Об'єкт: {cls}")
        return cls

    def __init__(cls, name: str, bases: tuple, namespace: dict):
        print(f"[META __init__] Ініціалізуємо клас '{name}'")
        super().__init__(name, bases, namespace)
        print(f"[META __init__] Клас '{name}' готовий до використання.\n")


# Оголошуємо класи з нашим метакласом
class Vehicle(metaclass=LoggingMeta):
    max_speed: int = 0

    def move(self) -> str:
        return "Рухаюсь"


class Car(Vehicle):  # Car теж отримає LoggingMeta через спадкування!
    max_speed: int = 200

    def honk(self) -> str:
        return "Бі-бі!"
python first_metaclass.py
$ python first_metaclass.py
[META __new__] Починаємо створення класу 'Vehicle'
Базові класи: []
Атрибути простору імен: ['max_speed', 'move']
[META __new__] Клас 'Vehicle' створено. Об'єкт: <class '__main__.Vehicle'>
[META __init__] Ініціалізуємо клас 'Vehicle'
[META __init__] Клас 'Vehicle' готовий до використання.
[META __new__] Починаємо створення класу 'Car'
Базові класи: ['Vehicle']
Атрибути простору імен: ['max_speed', 'honk']
[META __new__] Клас 'Car' створено. Об'єкт: <class '__main__.Car'>
[META __init__] Ініціалізуємо клас 'Car'
[META __init__] Клас 'Car' готовий до використання.

Це ключове спостереження: метаклас успадковується підкласами. Car сам не вказує metaclass=LoggingMeta, але оскільки Car успадковується від Vehicle, а у Vehicle метаклас — LoggingMeta, Python автоматично застосовує той самий метаклас і до Car. Саме тому Django ORM достатньо один раз прописати метаклас у базовому класі Model — і всі тисячі моделей у проекті автоматично матимуть потрібну поведінку.

Повний lifecycle: від class до obj()

Коли Python зустрічає оголошення класу і потім створення екземпляра, відбувається наступна строга послідовність викликів:

Lifecycle: class MyClass(Base, metaclass=Meta) → obj = MyClass()
# ── Фаза оголошення класу (виконується один раз при імпорті) ──
[1] type.__prepare__(mcs, name, bases) # (опційно) створює namespace-словник
[2] <виконується тіло класу в namespace>
[3] Meta.__new__(mcs, name, bases, namespace) # Створює об'єкт класу
[4] Meta.__init__(cls, name, bases, namespace) # Ініціалізує об'єкт класу
─── Клас MyClass готовий ────────────────────────────────────────
# ── Фаза створення екземпляра (виконується при кожному MyClass()) ──
[5] MyClass.__new__(cls, *args, **kwargs) # Виділяє пам'ять для екземпляра
[6] MyClass.__init__(obj, *args, **kwargs) # Ініціалізує екземпляр
─── Екземпляр obj готовий ───────────────────────────────────────
Методи 3 та 4 у метакласі викликаються лише один раз — у момент оголошення класу (зазвичай при імпорті модуля). Методи 5 та 6 — при кожному виклику конструктора MyClass(). Плутати ці два рівні — найчастіша помилка при роботі з метакласами.

Частина III: Практичне застосування метакласів

Сценарій 1: Валідація структури класу (обов'язкові поля)

Перший класичний сценарій: гарантувати, що кожен клас-нащадок обов'язково визначає певний атрибут або метод. Зробимо метаклас, що перевіряє наявність __tablename__ у кожній моделі бази даних:

# strict_model_meta.py
from typing import Any


class StrictModelMeta(type):
    """
    Метаклас для ORM-моделей.
    Гарантує, що кожна не-абстрактна модель визначає __tablename__.
    """
    _REQUIRED_ATTRS = {"__tablename__"}

    def __new__(mcs, name: str, bases: tuple, namespace: dict) -> type:
        cls = super().__new__(mcs, name, bases, namespace)

        # Пропускаємо сам базовий клас Model та абстрактні класи
        is_base = not bases  # клас без батьків — сам є базовим
        is_abstract = namespace.get("__abstract__", False)

        if not is_base and not is_abstract:
            missing = mcs._REQUIRED_ATTRS - set(namespace)
            if missing:
                raise TypeError(
                    f"Клас '{name}' не визначає обов'язкові атрибути: "
                    f"{', '.join(sorted(missing))}. "
                    f"Кожна модель повинна мати атрибут __tablename__."
                )

        return cls


class Model(metaclass=StrictModelMeta):
    """Абстрактний базовий клас для всіх моделей."""
    __abstract__ = True  # Сам Model пропускається перевіркою


# ✅ Правильно — є __tablename__
class User(Model):
    __tablename__ = "users"

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


class Article(Model):
    __tablename__ = "articles"


# ❌ Неправильно — немає __tablename__ → TypeError при оголошенні класу!
try:
    class BadModel(Model):
        pass  # Забули __tablename__!
except TypeError as e:
    print(f"TypeError: {e}")
python strict_model_meta.py
$ python strict_model_meta.py
TypeError: Клас 'BadModel' не визначає обов'язкові атрибути: __tablename__. Кожна модель повинна мати атрибут __tablename__.

Зверніть увагу: помилка виникає не при BadModel(), а вже під час оголошення класу BadModel. Метаклас перехоплює момент народження класу і забороняє його, якщо структура некоректна. У великих проектах це знаходить помилки ще при старті сервера, не при першому зверненні до БД.

Сценарій 2: Plugin Registry (Автоматична реєстрація плагінів)

Другий потужний сценарій — система плагінів. Кожен клас, що успадковується від базового, автоматично реєструється в центральному словнику:

# plugin_registry.py
from __future__ import annotations
from typing import ClassVar


class PluginMeta(type):
    """
    Метаклас, що автоматично реєструє всі підкласи у словнику _registry.
    Базовий клас, що його використовує, виключається з реєстру.
    """

    def __init__(cls, name: str, bases: tuple, namespace: dict) -> None:
        super().__init__(name, bases, namespace)

        # Реєструємо лише нащадків (не сам базовий клас)
        if bases:
            plugin_name = namespace.get("name", name.lower())
            # Отримуємо реєстр з першого батьківського класу, що має метаклас
            root = bases[0]
            while hasattr(root, "__bases__") and root.__bases__:
                potential_root = root.__bases__[0]
                if not hasattr(potential_root, "_registry"):
                    break
                root = potential_root

            # Знаходимо справжній корінь з _registry
            for base in bases:
                if hasattr(base, "_registry"):
                    base._registry[plugin_name] = cls
                    break


class Formatter(metaclass=PluginMeta):
    """Базовий клас для форматерів виводу."""
    _registry: ClassVar[dict[str, type[Formatter]]] = {}

    def format(self, data: object) -> str:
        raise NotImplementedError


# Реєструються автоматично при оголошенні!
class JSONFormatter(Formatter):
    name = "json"

    def format(self, data: object) -> str:
        import json
        return json.dumps(data, ensure_ascii=False, indent=2)


class CSVFormatter(Formatter):
    name = "csv"

    def format(self, data: object) -> str:
        if isinstance(data, list) and data:
            keys = data[0].keys() if isinstance(data[0], dict) else []
            rows = [",".join(keys)]
            rows += [",".join(str(row.get(k, "")) for k in keys) for row in data]
            return "\n".join(rows)
        return str(data)


class PlainTextFormatter(Formatter):
    name = "text"

    def format(self, data: object) -> str:
        return str(data)


# Використання через реєстр
def get_formatter(fmt_name: str) -> Formatter:
    cls = Formatter._registry.get(fmt_name)
    if cls is None:
        available = ", ".join(Formatter._registry)
        raise ValueError(f"Невідомий формат '{fmt_name}'. Доступні: {available}")
    return cls()


# ─── Демонстрація ────────────────────────────────────────────────────────────

print("Зареєстровані форматери:", list(Formatter._registry.keys()))

data = [{"name": "Іван", "age": 30}, {"name": "Марія", "age": 25}]

for fmt in ("json", "csv", "text"):
    formatter = get_formatter(fmt)
    print(f"\n[{fmt.upper()}]")
    print(formatter.format(data))
python plugin_registry.py
$ python plugin_registry.py
Зареєстровані форматери: ['json', 'csv', 'text']
[JSON]
[
{
"name": "Іван",
"age": 30
},
...
]
[CSV]
name,age
Іван,30
Марія,25
[TEXT]
[{'name': 'Іван', 'age': 30}, {'name': 'Марія', 'age': 25}]

Додати новий форматер тепер надзвичайно просто: достатньо оголосити клас, що успадковується від Formatter. Ніякої ручної реєстрації, ніякого редагування списків. Метаклас робить усе автоматично.

Сценарій 3: Автоматичне перетворення імен методів у snake_case

Метаклас може трансформувати сам простір імен класу ще до його остаточного збирання:

# snake_case_meta.py
import re


def to_snake_case(name: str) -> str:
    """CamelCase → snake_case."""
    s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
    return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()


class SnakeCaseMeta(type):
    """
    Метаклас, що автоматично переіменовує методи з camelCase у snake_case.
    Корисно при інтеграції з Java/JS бібліотеками або при рефакторингу.
    """

    def __new__(mcs, name: str, bases: tuple, namespace: dict) -> type:
        new_namespace: dict = {}

        for key, value in namespace.items():
            snake_key = to_snake_case(key)
            if snake_key != key and callable(value):
                print(f"  Перейменовано: {key!r}{snake_key!r}")
                new_namespace[snake_key] = value
                # Також залишаємо оригінал для зворотної сумісності
                new_namespace[key] = value
            else:
                new_namespace[key] = value

        return super().__new__(mcs, name, bases, new_namespace)


class APIClient(metaclass=SnakeCaseMeta):
    """Клас з методами у camelCase — метаклас додасть snake_case-аліаси."""

    def getUserById(self, user_id: int) -> dict:
        return {"id": user_id, "name": "Тестовий користувач"}

    def createNewOrder(self, product: str, quantity: int) -> dict:
        return {"product": product, "quantity": quantity, "status": "created"}

    def deleteExpiredSessions(self) -> int:
        return 42  # кількість видалених сесій


# Обидва варіанти працюють!
client = APIClient()
print(client.getUserById(1))        # camelCase (оригінал)
print(client.get_user_by_id(1))     # snake_case (автоматично додано метакласом)
python snake_case_meta.py
$ python snake_case_meta.py
Перейменовано: 'getUserById''get_user_by_id'
Перейменовано: 'createNewOrder''create_new_order'
Перейменовано: 'deleteExpiredSessions''delete_expired_sessions'
{'id': 1, 'name': 'Тестовий користувач'}
{'id': 1, 'name': 'Тестовий користувач'}

Частина IV: Сучасна альтернатива — __init_subclass__ (PEP 487)

Python 3.6 вніс суттєве покращення для найпоширенішого use case метакласів. У 90% випадків метаклас потрібен лише для того, щоб зробити щось при оголошенні підкласу. PEP 487 вирішив цю задачу елегантно, не вимагаючи писати цілий метаклас.

Що таке __init_subclass__

Метод __init_subclass__ є звичайним методом класу, що викликається автоматично щоразу, коли оголошується підклас даного класу. Це означає, що базовий клас може «знати» про своїх нащадків без метакласу.

class Base:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        # cls — це щойно оголошений підклас
        print(f"Зареєстровано новий підклас: {cls.__name__}")

class Child(Base):  # ← __init_subclass__ викликається тут!
    pass

class GrandChild(Child):  # ← і тут!
    pass

Переписуємо Plugin Registry без метакласу

Давайте перепишемо попередній приклад із Plugin Registry, використовуючи __init_subclass__:

# modern_registry.py
from __future__ import annotations
from typing import ClassVar


class Formatter:
    """
    Базовий клас для форматерів.
    Замість метакласу використовуємо __init_subclass__.
    """
    _registry: ClassVar[dict[str, type[Formatter]]] = {}

    def __init_subclass__(cls, name: str = "", **kwargs) -> None:
        """
        Автоматично викликається при оголошенні будь-якого підкласу Formatter.
        Параметр name береться з заголовка класу: class JSON(Formatter, name="json")
        """
        super().__init_subclass__(**kwargs)
        # Якщо name не задано явно — використовуємо ім'я класу в нижньому регістрі
        plugin_name = name or cls.__name__.lower()
        Formatter._registry[plugin_name] = cls
        print(f"  [Registry] Зареєстровано: {plugin_name!r}{cls.__name__}")

    def format(self, data: object) -> str:
        raise NotImplementedError


# Параметр name передається через заголовок класу!
class JSONFormatter(Formatter, name="json"):
    def format(self, data: object) -> str:
        import json
        return json.dumps(data, ensure_ascii=False)


class CSVFormatter(Formatter, name="csv"):
    def format(self, data: object) -> str:
        return str(data)


class PlainTextFormatter(Formatter, name="text"):
    def format(self, data: object) -> str:
        return str(data)


print("\nРеєстр:", list(Formatter._registry.keys()))
python modern_registry.py
$ python modern_registry.py
[Registry] Зареєстровано: 'json' → JSONFormatter
[Registry] Зареєстровано: 'csv' → CSVFormatter
[Registry] Зареєстровано: 'text' → PlainTextFormatter
Реєстр: ['json', 'csv', 'text']

Код став значно читабельнішим! Але найцікавіше — параметр name="json" у заголовку класу. Python автоматично передає всі «зайві» параметри із заголовка класу у __init_subclass__ через **kwargs. Це надзвичайно потужний механізм декларативного конфігурування.

Переписуємо валідацію структури без метакласу

# modern_validation.py

class Model:
    """Базовий клас моделей з валідацією через __init_subclass__."""

    def __init_subclass__(cls, abstract: bool = False, **kwargs) -> None:
        super().__init_subclass__(**kwargs)

        if abstract:
            return  # Абстрактні класи пропускаємо

        if not hasattr(cls, "__tablename__"):
            raise TypeError(
                f"Клас '{cls.__name__}' не визначає __tablename__. "
                f"Кожна модель бази даних повинна мати цей атрибут."
            )
        print(f"  [Model] Модель '{cls.__name__}' → таблиця '{cls.__tablename__}'")


class TimestampedModel(Model, abstract=True):
    """Абстрактний міксін — додає created_at та updated_at."""
    created_at: str = ""
    updated_at: str = ""


# ✅ Правильно
class User(Model):
    __tablename__ = "users"

class Article(TimestampedModel):   # TimestampedModel абстрактний → не вимагає tablename
    __tablename__ = "articles"     # але Article вже конкретна, тому вимагає

# ❌ Неправильно
try:
    class BadModel(Model):
        pass
except TypeError as e:
    print(f"\nTypeError: {e}")

Метакласи проти __init_subclass__: коли що вибирати

КритерійМетаклас__init_subclass__
Синтаксична складністьВисока (новий клас)Низька (метод у базовому класі)
Модифікація namespace✅ Так (до фіксації класу)❌ Ні (клас вже готовий)
Кілька незалежних механізмівСкладно (конфлікти метакласів)✅ Легко (кожен визначає свій)
ЧитабельністьНижчаВища
Конфлікти при множинному спадкуванніЧастоРідко
Перейменування/додавання атрибутів✅ Через namespaceОбмежено (через setattr)
Рекомендований підхід (Python 3.6+)Для складних трансформаційДля більшості use cases
Метакласи можуть конфліктувати при множинному спадкуванні. Якщо ClassA має метаклас MetaA, а ClassB — метаклас MetaB, то class C(ClassA, ClassB) призведе до TypeError, якщо MetaA і MetaB не пов'язані спадкуванням. Один із метакласів повинен бути підкласом іншого. __init_subclass__ цієї проблеми не має.

Підсумок: де метакласи живуть у реальному коді

Метакласи — потужний, але «важкий» інструмент. Більшість Python-розробників ніколи не пишуть власних метакласів — і це правильно. Але вони щодня використовують їхні результати:

  • Django ORM: ModelBase — метаклас, що читає оголошені поля, збирає метадані та реєструє кожну модель.
  • SQLAlchemy: DeclarativeMeta реалізує декларативний стиль Base.
  • Pydantic (v1): ModelMetaclass обробляє анотації типів та поля.
  • Python abc.ABCMeta: метаклас, що відстежує абстрактні методи та забороняє інстанціювання класів з незаповненими абстрактними методами.
  • enum.EnumMeta: метаклас, що перетворює атрибути класу на членів переліку.

✅ Використовуйте метакласи коли...

Вам потрібно трансформувати простір імен (namespace) до фіксації класу, або коли __init_subclass__ недостатньо (наприклад, потрібно змінити ім'я методів при оголошенні).

✅ Використовуйте `__init_subclass__` коли...

Вам потрібно реагувати на оголошення підкласів: реєструвати, валідувати структуру, ін'єктувати поведінку. Це 90% use cases і значно простіший підхід.

❌ Не використовуйте метакласи коли...

Завдання можна вирішити декоратором класу, __init_subclass__ або __post_init__ (для dataclasses). Метаклас — це найнижчий рівень API Python, і він має свою ціну в читабельності.

Ключові принципи, які варто пам'ятати

ПринципДеталь
Клас — це об'єктtype(MyClass) повертає type або кастомний метаклас
type — конструктор класівtype(name, bases, dict) створює клас динамічно
Метаклас успадковуєтьсяПідкласи отримують метаклас батьківського класу автоматично
Порядок: __new____init__Спочатку метакласу, потім — у самого класу при інстанціюванні
__prepare__Опційний метод метакласу для задання кастомного словника namespace
__init_subclass__Сучасна альтернатива для більшості сценаріїв (Python 3.6+)
Конфлікти метакласівВиникають при множинному спадкуванні від класів з різними метакласами
Copyright © 2026