Прежде чем говорить о метаклассах, нужно прочно зафиксировать одну идею: в Python классы — это обычные объекты. Не шаблоны, не типы данных в особом смысле, а именно объекты — такие же, как числа, строки или списки.
Что это означает на практике?
class User:
def __init__(self, name):
self.name = name
def greet(self):
return f"Привет, я {self.name}"
# Класс можно присвоить переменной — как любой объект
MyClass = User
obj = MyClass("Alice")
print(obj.greet()) # Привет, я Alice
# Класс можно передать в функцию
def create_instance(cls, *args):
return cls(*args)
user = create_instance(User, "Bob")
print(user.greet()) # Привет, я Bob
# Класс можно хранить в словаре
registry = {"user": User}
obj = registry["user"]("Carol")
print(obj.greet()) # Привет, я Carol
# Класс можно вернуть из функции
def get_class(name):
classes = {"user": User}
return classes[name]
Cls = get_class("user")
print(Cls("Dave").greet()) # Привет, я DaveРаз классы — это объекты, возникает закономерный вопрос: что является классом для класса?
У каждого объекта есть тип — type(obj) возвращает его класс. Применим это к самому классу:
user = User("Alice")
print(type(user)) # <class '__main__.User'> — тип объекта user
print(type(User)) # <class 'type'> — тип объекта User (самого класса!)
print(type(int)) # <class 'type'>
print(type(str)) # <class 'type'>
print(type(list)) # <class 'type'>Тип класса User — это type. Тип встроенных классов int, str, list — тоже type. type — это метакласс, класс всех классов. Именно type отвечает за создание новых классов в Python.
type в Python выполняет две разные роли, и это поначалу сбивает с толку.
Роль 1: функция для получения типа. type(obj) с одним аргументом возвращает тип (класс) объекта.
Роль 2: метакласс, создающий новые классы. type(name, bases, namespace) с тремя аргументами создаёт новый класс.
# Роль 1: получение типа
print(type(42)) # <class 'int'>
print(type("hello")) # <class 'str'>
print(type([1, 2, 3])) # <class 'list'>
# Роль 2: создание нового класса
MyClass = type("MyClass", (), {"x": 10, "greet": lambda self: "Привет"})
obj = MyClass()
print(obj.x) # 10
print(obj.greet()) # Привет
print(type(obj)) # <class '__main__.MyClass'>Проверим, что все классы являются экземплярами type:
class User:
pass
print(isinstance(User, type)) # True — User является экземпляром type
print(isinstance(int, type)) # True
print(isinstance(str, type)) # True
# И сам type является экземпляром самого себя
print(isinstance(type, type)) # True — интригующий факт Python
print(type(type)) # <class 'type'>Цепочка наследования type и object:
print(issubclass(type, object)) # True — type наследует от object
print(issubclass(object, type)) # False — object не является метаклассом
print(type(object)) # <class 'type'> — object тоже создан typeЭто замкнутая система: type является экземпляром самого себя, и object создан type, при этом type наследует от object. Не нужно слишком глубоко думать об этой рекурсии — примите её как встроенную особенность реализации CPython.
Самое практичное применение type с тремя аргументами — динамическое создание классов в рантайме. Это эквивалентно обычному class объявлению:
# Обычное объявление класса
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
# Эквивалентное создание через type
def point_init(self, x, y):
self.x = x
self.y = y
def point_repr(self):
return f"Point({self.x}, {self.y})"
PointDynamic = type(
"Point", # имя класса
(object,), # кортеж базовых классов
{ # словарь атрибутов
"__init__": point_init,
"__repr__": point_repr,
}
)
p = PointDynamic(1, 2)
print(p) # Point(1, 2)
print(type(p)) # <class '__main__.Point'>Практическое применение — создание классов по конфигурации. Например, динамическое создание классов моделей:
def create_model_class(name: str, fields: list) -> type:
"""
Динамически создаёт класс модели с заданными полями.
Аналог того, как Django создаёт модели из определений.
"""
def __init__(self, **kwargs):
for field in fields:
setattr(self, field, kwargs.get(field))
def __repr__(self):
field_str = ", ".join(f"{f}={getattr(self, f)!r}" for f in fields)
return f"{name}({field_str})"
def to_dict(self):
return {f: getattr(self, f) for f in fields}
namespace = {
"__init__": __init__,
"__repr__": __repr__,
"to_dict": to_dict,
"_fields": fields,
}
return type(name, (object,), namespace)
# Создаём классы из конфигурации — например, прочитанной из YAML
UserModel = create_model_class("UserModel", ["id", "username", "email"])
OrderModel = create_model_class("OrderModel", ["id", "user_id", "total", "status"])
user = UserModel(id=1, username="alice", email="alice@example.com")
order = OrderModel(id=1, user_id=1, total=89990, status="pending")
print(user) # UserModel(id=1, username='alice', email='alice@example.com')
print(order.to_dict()) # {'id': 1, 'user_id': 1, 'total': 89990, 'status': 'pending'}Понимание того, что происходит при выполнении оператора class, критично для работы с метаклассами.
class MyClass(Base1, Base2, metaclass=MyMeta):
class_var = 42
def method(self):
passPython выполняет следующую последовательность:
Шаг 1: Определить метакласс
→ явно указан metaclass=MyMeta? Использовать его.
→ нет? Взять метакласс первого базового класса.
→ нет базовых классов? Использовать type.
Шаг 2: Вызвать MyMeta.__prepare__(name, bases, **kwargs)
→ возвращает словарь-пространство имён для тела класса
→ обычно возвращает обычный dict или OrderedDict
Шаг 3: Выполнить тело класса
→ все определения попадают в пространство имён из шага 2
Шаг 4: Вызвать MyMeta(name, bases, namespace)
→ это вызывает MyMeta.__call__
→ который вызывает MyMeta.__new__(mcs, name, bases, namespace)
→ а затем MyMeta.__init__(cls, name, bases, namespace)
Результат: новый объект-класс
Аналогия с обычными объектами: когда вы пишете User("alice"), Python вызывает type.__call__(User, "alice"), который вызывает User.__new__(User, "alice") и User.__init__(user_obj, "alice"). Так же работает и создание классов, только type заменяется на метакласс.
Метакласс создаётся наследованием от type:
class MyMeta(type):
"""Простейший метакласс — пока ничего не меняет."""
def __new__(mcs, name, bases, namespace):
print(f"MyMeta.__new__: создаём класс {name!r}")
cls = super().__new__(mcs, name, bases, namespace)
return cls
def __init__(cls, name, bases, namespace):
print(f"MyMeta.__init__: инициализируем класс {name!r}")
super().__init__(name, bases, namespace)
class MyClass(metaclass=MyMeta):
x = 10
# Вывод при объявлении класса (не при создании объекта!):
# MyMeta.__new__: создаём класс 'MyClass'
# MyMeta.__init__: инициализируем класс 'MyClass'
obj = MyClass() # здесь ничего не выводится — MyMeta не трогает создание экземпляровПараметры __new__ и __init__ метакласса:
mcs/cls— сам метакласс (аналогclsвclassmethod)name— имя создаваемого класса (строка)bases— кортеж базовых классовnamespace— словарь атрибутов класса (результат выполнения тела класса)
Важное отличие __new__ от __init__:
__new__вызывается до создания объекта-класса и возвращает его__init__вызывается после того, как объект-класс уже создан, и ничего не возвращает
Когда использовать какой:
__new__: если нужно изменить сам объект-класс перед возвратом, или вернуть другой объект (например, для Singleton)__init__: если нужно что-то настроить после создания класса (регистрация, проверки)
Самый мощный момент: метакласс контролирует не только создание классов, но и создание экземпляров этих классов. Когда вы пишете MyClass(args), Python фактически вызывает type.__call__(MyClass, args) — то есть метод __call__ метакласса.
class TracingMeta(type):
"""Метакласс, логирующий каждое создание экземпляра."""
def __call__(cls, *args, **kwargs):
print(f"[TracingMeta] Создаём экземпляр {cls.__name__}")
instance = super().__call__(*args, **kwargs)
print(f"[TracingMeta] Экземпляр создан: {instance!r}")
return instance
class TracedUser(metaclass=TracingMeta):
def __init__(self, name):
self.name = name
def __repr__(self):
return f"TracedUser({self.name!r})"
user = TracedUser("alice")
# [TracingMeta] Создаём экземпляр TracedUser
# [TracingMeta] Экземпляр создан: TracedUser('alice')Именно через __call__ метакласса реализуется паттерн Singleton: мы перехватываем создание экземпляра и возвращаем уже существующий.
В уроке 5 курса мы реализовывали Singleton через __new__ экземпляра. Теперь реализуем его правильно — через метакласс:
import threading
class SingletonMeta(type):
"""
Метакласс для паттерна Singleton.
Гарантирует, что класс имеет только один экземпляр.
Потокобезопасная реализация через блокировку.
"""
_instances: dict = {}
_lock: threading.Lock = threading.Lock()
def __call__(cls, *args, **kwargs):
# Двойная проверка для минимизации накладных расходов блокировки
if cls not in cls._instances:
with cls._lock:
# Повторная проверка внутри блокировки
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
"""
Синглтон соединения с базой данных.
В приложении должно быть только одно соединение с основной БД.
"""
def __init__(self, host="localhost", port=5432, database="myapp"):
# Этот код выполняется ТОЛЬКО при первом создании
self.host = host
self.port = port
self.database = database
self._connected = False
print(f"DatabaseConnection.__init__ вызван для {host}:{port}/{database}")
def connect(self):
self._connected = True
return self
def __repr__(self):
return f"DatabaseConnection({self.host}:{self.port}/{self.database})"
# Демонстрация Singleton
db1 = DatabaseConnection()
# DatabaseConnection.__init__ вызван для localhost:5432/myapp
db2 = DatabaseConnection()
# (ничего не выводится — __init__ не вызывается повторно)
db3 = DatabaseConnection(host="replica.example.com")
# (ничего не выводится — возвращается тот же объект)
print(db1 is db2) # True — один и тот же объект
print(db1 is db3) # True — аргументы игнорируются при повторном вызове
print(db1) # DatabaseConnection(localhost:5432/myapp)
# Разные классы — разные синглтоны
class CacheConnection(metaclass=SingletonMeta):
def __init__(self, host="localhost", port=6379):
self.host = host
self.port = port
print(f"CacheConnection.__init__ вызван")
cache = CacheConnection()
# CacheConnection.__init__ вызван
print(cache is db1) # False — разные синглтоны для разных классовПочему метакласс лучше реализации через __new__ экземпляра:
- Чёткое разделение ответственности: метакласс отвечает за «политику создания», класс — за бизнес-логику.
- Переиспользование:
SingletonMetaприменяется к любому классу черезmetaclass=SingletonMeta. - Потокобезопасность реализуется в одном месте, не разбросана по
__new__разных классов. - Аргументы конструктора: при повторном вызове
__init__не вызывается — поведение корректное.
Сравним с Singleton через __new__:
# Singleton через __new__ (из урока 1)
class SingletonViaNew:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# Проблема: __init__ вызывается при каждом "создании"!
# Если __init__ меняет состояние — оно будет меняться каждый раз
s1 = SingletonViaNew()
s2 = SingletonViaNew()
print(s1 is s2) # True — работает
# Но __init__ вызвался дважды — если там есть логика, она выполнится дваждыRegistry — это реестр, который автоматически отслеживает все подклассы базового класса. Каждый раз, когда объявляется новый подкласс, он автоматически регистрируется без явного вызова.
class RegistryMeta(type):
"""
Метакласс-реестр. Автоматически регистрирует все подклассы.
"""
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
# Инициализируем реестр для базового класса
if not hasattr(cls, '_registry'):
cls._registry = {}
# Регистрируем все подклассы (но не сам базовый класс)
# Базовый класс — тот, у которого нет базовых классов в иерархии Registry
if bases:
registry_name = namespace.get('registry_name') or name.lower()
cls._registry[registry_name] = cls
@classmethod
def get_registry(mcs, cls):
return dict(cls._registry)Применим к системе обработчиков уведомлений:
class BaseNotificationHandler(metaclass=RegistryMeta):
"""Базовый обработчик уведомлений. Все подклассы регистрируются автоматически."""
registry_name = None # базовый класс не регистрируется
def send(self, recipient: str, message: str) -> dict:
raise NotImplementedError
class EmailHandler(BaseNotificationHandler):
registry_name = "email"
def send(self, recipient: str, message: str) -> dict:
print(f"[Email] → {recipient}: {message}")
return {"type": "email", "status": "sent", "recipient": recipient}
class SMSHandler(BaseNotificationHandler):
registry_name = "sms"
def send(self, recipient: str, message: str) -> dict:
print(f"[SMS] → {recipient}: {message[:160]}")
return {"type": "sms", "status": "sent", "recipient": recipient}
class PushHandler(BaseNotificationHandler):
registry_name = "push"
def send(self, recipient: str, message: str) -> dict:
print(f"[Push] → {recipient}: {message}")
return {"type": "push", "status": "sent", "recipient": recipient}
# Реестр заполняется автоматически при объявлении классов
print(BaseNotificationHandler._registry)
# {'email': <class 'EmailHandler'>, 'sms': <class 'SMSHandler'>, 'push': <class 'PushHandler'>}
def send_notification(channel: str, recipient: str, message: str) -> dict:
"""Отправляет уведомление через указанный канал."""
handler_class = BaseNotificationHandler._registry.get(channel)
if not handler_class:
raise ValueError(f"Неизвестный канал: {channel!r}. "
f"Доступны: {list(BaseNotificationHandler._registry.keys())}")
handler = handler_class()
return handler.send(recipient, message)
send_notification("email", "alice@example.com", "Ваш заказ подтверждён")
# [Email] → alice@example.com: Ваш заказ подтверждён
send_notification("sms", "+79001234567", "Код: 1234")
# [SMS] → +79001234567: Код: 1234
try:
send_notification("telegram", "@alice", "Привет")
except ValueError as e:
print(e) # Неизвестный канал: 'telegram'. Доступны: ['email', 'sms', 'push']Ключевое преимущество: добавление нового канала — это просто объявление нового класса. Никаких изменений в send_notification, никаких явных регистраций. Именно так работают системы плагинов в Django и других фреймворках.
Python 3.6 добавил хук __init_subclass__, который вызывается в базовом классе каждый раз, когда создаётся его подкласс. Для большинства задач Registry это более простая и читаемая альтернатива метаклассу:
class BaseSerializer:
"""
Базовый класс сериализатора.
__init_subclass__ автоматически регистрирует все подклассы.
"""
_registry: dict = {}
def __init_subclass__(cls, format_name: str = None, **kwargs):
super().__init_subclass__(**kwargs)
if format_name:
BaseSerializer._registry[format_name] = cls
print(f"[Registry] Зарегистрирован сериализатор: {format_name!r} → {cls.__name__}")
@classmethod
def get(cls, format_name: str):
serializer_class = cls._registry.get(format_name)
if not serializer_class:
raise ValueError(f"Нет сериализатора для формата {format_name!r}")
return serializer_class()
def serialize(self, data) -> str:
raise NotImplementedError
class JSONSerializer(BaseSerializer, format_name="json"):
def serialize(self, data) -> str:
import json
return json.dumps(data, ensure_ascii=False)
class CSVSerializer(BaseSerializer, format_name="csv"):
def serialize(self, data) -> str:
if not data:
return ""
if isinstance(data, list) and isinstance(data[0], dict):
headers = list(data[0].keys())
rows = [",".join(headers)]
rows += [",".join(str(row.get(h, "")) for h in headers) for row in data]
return "\n".join(rows)
return str(data)
class XMLSerializer(BaseSerializer, format_name="xml"):
def serialize(self, data) -> str:
if isinstance(data, dict):
items = "".join(f"<{k}>{v}</{k}>" for k, v in data.items())
return f"<root>{items}</root>"
return f"<data>{data}</data>"
# Регистрация произошла автоматически при объявлении классов
print(f"\nДоступные форматы: {list(BaseSerializer._registry.keys())}")
# Использование реестра
data = {"user": "alice", "action": "login", "status": "success"}
for format_name in ["json", "csv", "xml"]:
serializer = BaseSerializer.get(format_name)
print(f"\n{format_name.upper()}:")
print(serializer.serialize(data))Сравнение __init_subclass__ и метакласса:
__init_subclass__ |
Метакласс | |
|---|---|---|
| Синтаксис | Проще, встроен в класс | Отдельный класс, сложнее |
| Параметры | Через аргументы класса | Через атрибуты класса |
| Изменение создания экземпляра | Нет | Да (через __call__) |
| Изменение пространства имён | Нет | Да (через __prepare__) |
| Singleton | Нет | Да |
| Registry | Да — предпочтительно | Да |
| Конфликты при наследовании | Редко | Могут быть |
Правило: используйте __init_subclass__ для Registry и аналогичных задач. Используйте метакласс только когда нужно изменить сам процесс создания класса или объектов — например, Singleton, или нужен __prepare__.
Реализуем полноценную систему плагинов для обработки веб-хуков (webhooks):
class WebhookPlugin:
"""
Базовый класс для плагинов обработки webhook-событий.
Использует __init_subclass__ для автоматической регистрации.
"""
_registry: dict = {}
def __init_subclass__(cls, event_type: str = None, **kwargs):
super().__init_subclass__(**kwargs)
if event_type:
WebhookPlugin._registry[event_type] = cls
@classmethod
def get_handler(cls, event_type: str):
handler_class = cls._registry.get(event_type)
if not handler_class:
return None
return handler_class()
@classmethod
def supported_events(cls) -> list:
return list(cls._registry.keys())
def handle(self, payload: dict) -> dict:
"""Обрабатывает событие. Должен быть переопределён в подклассе."""
raise NotImplementedError(f"{self.__class__.__name__} не реализует handle()")
def validate_payload(self, payload: dict) -> bool:
"""Базовая валидация — проверяет наличие обязательных полей."""
return bool(payload)
class OrderCreatedPlugin(WebhookPlugin, event_type="order.created"):
"""Обработчик события создания заказа."""
def validate_payload(self, payload: dict) -> bool:
return all(k in payload for k in ["order_id", "user_id", "total"])
def handle(self, payload: dict) -> dict:
order_id = payload["order_id"]
user_id = payload["user_id"]
total = payload["total"]
print(f"[OrderCreated] Новый заказ #{order_id} от пользователя {user_id}, сумма: {total}")
# В реальном приложении здесь: отправка email, обновление аналитики, etc.
return {"processed": True, "order_id": order_id, "action": "send_confirmation_email"}
class PaymentCompletedPlugin(WebhookPlugin, event_type="payment.completed"):
"""Обработчик события успешной оплаты."""
def validate_payload(self, payload: dict) -> bool:
return all(k in payload for k in ["payment_id", "order_id", "amount"])
def handle(self, payload: dict) -> dict:
payment_id = payload["payment_id"]
order_id = payload["order_id"]
print(f"[PaymentCompleted] Оплата {payment_id} для заказа #{order_id}")
return {"processed": True, "payment_id": payment_id, "action": "update_order_status"}
class UserRegisteredPlugin(WebhookPlugin, event_type="user.registered"):
"""Обработчик события регистрации пользователя."""
def handle(self, payload: dict) -> dict:
user_id = payload.get("user_id")
email = payload.get("email")
print(f"[UserRegistered] Новый пользователь {user_id}: {email}")
return {"processed": True, "action": "send_welcome_email"}
class WebhookProcessor:
"""Процессор webhook-событий. Использует реестр плагинов."""
def process(self, event_type: str, payload: dict) -> dict:
handler = WebhookPlugin.get_handler(event_type)
if handler is None:
print(f"[WebhookProcessor] Нет обработчика для {event_type!r}")
return {"processed": False, "reason": "unknown_event_type"}
if not handler.validate_payload(payload):
return {"processed": False, "reason": "invalid_payload"}
return handler.handle(payload)
processor = WebhookProcessor()
print(f"Поддерживаемые события: {WebhookPlugin.supported_events()}\n")
events = [
("order.created", {"order_id": 1001, "user_id": 42, "total": 89990}),
("payment.completed", {"payment_id": "pay_abc", "order_id": 1001, "amount": 89990}),
("user.registered", {"user_id": 43, "email": "bob@example.com"}),
("unknown.event", {"data": "something"}),
]
for event_type, payload in events:
result = processor.process(event_type, payload)
print(f"Результат: {result}\n")Покажем ещё одно применение метакласса — валидацию при объявлении класса. Это аналог ABC, но с другими возможностями:
class APIViewMeta(type):
"""
Метакласс для API-вью. При объявлении класса проверяет:
1. Наличие обязательного атрибута endpoint_path
2. Что все http-методы имеют правильные имена
3. Автоматически добавляет метаданные
"""
VALID_HTTP_METHODS = {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Пропускаем базовый класс
is_base = not bases or all(
not hasattr(b, '_is_api_view') for b in bases
)
if is_base:
cls._is_api_view = True
return cls
# Проверяем обязательный атрибут endpoint_path
if 'endpoint_path' not in namespace:
raise TypeError(
f"APIView-класс {name!r} должен объявить атрибут 'endpoint_path'"
)
# Проверяем имена методов — все публичные методы должны быть
# либо стандартными HTTP-методами, либо начинаться с '_'
for attr_name, attr_value in namespace.items():
if callable(attr_value) and not attr_name.startswith('_'):
if attr_name not in mcs.VALID_HTTP_METHODS:
import warnings
warnings.warn(
f"В {name!r} метод {attr_name!r} не является стандартным "
f"HTTP-методом. HTTP-методы: {mcs.VALID_HTTP_METHODS}",
UserWarning,
stacklevel=2
)
# Автоматически добавляем метаданные
cls._allowed_methods = [
m.upper() for m in mcs.VALID_HTTP_METHODS
if m in namespace
]
print(f"[APIViewMeta] Зарегистрирован вью: {name!r} "
f"на {namespace['endpoint_path']!r}, "
f"методы: {cls._allowed_methods}")
return cls
class BaseAPIView(metaclass=APIViewMeta):
"""Базовый класс для всех API-вью."""
def dispatch(self, request: dict) -> dict:
method = request.get("method", "GET").lower()
handler = getattr(self, method, self._method_not_allowed)
return handler(request)
def _method_not_allowed(self, request: dict) -> dict:
return {"status": 405, "error": "Method Not Allowed",
"allowed": self._allowed_methods}
class UserListView(BaseAPIView):
endpoint_path = "/api/users"
def get(self, request: dict) -> dict:
return {"status": 200, "data": [{"id": 1, "name": "Alice"}]}
def post(self, request: dict) -> dict:
return {"status": 201, "data": {"id": 2, **request.get("body", {})}}
class UserDetailView(BaseAPIView):
endpoint_path = "/api/users/{id}"
def get(self, request: dict) -> dict:
user_id = request.get("path_params", {}).get("id")
return {"status": 200, "data": {"id": user_id, "name": "Alice"}}
def delete(self, request: dict) -> dict:
user_id = request.get("path_params", {}).get("id")
return {"status": 204, "deleted": user_id}
# Попытка объявить вью без endpoint_path вызовет ошибку при объявлении
try:
class BrokenView(BaseAPIView):
# endpoint_path не объявлен!
def get(self, request):
pass
except TypeError as e:
print(f"TypeError при объявлении класса: {e}")
# Использование
list_view = UserListView()
detail_view = UserDetailView()
print(list_view.dispatch({"method": "GET"}))
print(list_view.dispatch({"method": "DELETE"})) # не разрешён
print(detail_view.dispatch({"method": "GET", "path_params": {"id": "42"}}))Метакласс — мощный, но сложный инструмент. Перед его применением задайте себе вопросы:
Вместо метакласса можно использовать:
# Декоратор класса — часто проще и достаточно
def add_logging(cls):
original_init = cls.__init__
def new_init(self, *args, **kwargs):
print(f"Создаём {cls.__name__}")
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@add_logging
class MyService:
def __init__(self, name):
self.name = name
# __init_subclass__ — для Registry, проще метакласса
class Base:
_registry = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
if name:
Base._registry[name] = clsМетакласс нужен когда:
- Нужно контролировать создание экземпляров (
__call__) — паттерн Singleton. - Нужно изменить пространство имён ещё до выполнения тела класса (
__prepare__). - Нужно изменить сам объект-класс до его возврата (
__new__). - Нужно, чтобы логика применялась автоматически ко всем подклассам без
__init_subclass__. - Конфликты метаклассов при множественном наследовании требуют явного контроля.
Конфликт метаклассов. Если класс наследует от двух классов с разными метаклассами — TypeError:
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
try:
class C(A, B): # TypeError!
pass
except TypeError as e:
print(e)
# metaclass conflict: the metaclass of a derived class must be a subclass of the metaclasses of all its basesРешение — создать общий метакласс, наследующий от обоих:
class MetaAB(MetaA, MetaB):
pass
class C(A, B, metaclass=MetaAB): # OK
passМетакласс применяется ко всем подклассам. Если вы объявили класс с метаклассом — все его подклассы автоматически используют тот же метакласс:
class MyMeta(type):
def __init__(cls, name, bases, ns):
print(f"MyMeta создал класс: {name}")
super().__init__(name, bases, ns)
class Base(metaclass=MyMeta): # MyMeta создал класс: Base
pass
class Child(Base): # MyMeta создал класс: Child — автоматически!
pass
class GrandChild(Child): # MyMeta создал класс: GrandChild — тоже!
passtype(obj) vs obj.__class__. В нормальных случаях они совпадают. Но __class__ можно переопределить, а type() — нет. type() возвращает реальный тип, __class__ может возвращать то, что хочет класс:
class Deceiver:
@property
def __class__(self):
return int # притворяемся int
d = Deceiver()
print(d.__class__) # <class 'int'> — обманывает
print(type(d)) # <class 'Deceiver'> — правда__prepare__ для упорядоченного пространства имён. Метод __prepare__ вызывается до выполнения тела класса и возвращает словарь, который будет использоваться как пространство имён. Это редко нужно, но позволяет, например, гарантировать порядок атрибутов:
class OrderedMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
# Возвращаем обычный dict — в Python 3.7+ dict сохраняет порядок
# Можно вернуть специальный словарь с дополнительным поведением
return {}Классы в Python — это объекты, а метакласс — это класс-для-классов. Метакласс по умолчанию — type, который создаёт все классы. type(name, bases, namespace) с тремя аргументами динамически создаёт новый класс.
Последовательность при объявлении класса: __prepare__ готовит пространство имён, тело класса выполняется в нём, затем metaclass.__new__ создаёт объект-класс, затем metaclass.__init__ его настраивает. metaclass.__call__ вызывается при создании каждого экземпляра класса.
Singleton реализуется через __call__ метакласса: перехватываем создание экземпляра и возвращаем кешированный. Registry реализуется через __init__ метакласса или, проще, через __init_subclass__ базового класса.
Для большинства задач достаточно декораторов классов или __init_subclass__. Метакласс нужен только когда нужен контроль над самим процессом создания класса или его экземпляров.
- Что такое метакласс? Каким метаклассом является класс
strи каким — обычный пользовательский класс? - Что делает
type(name, bases, namespace)с тремя аргументами? Чем это отличается отtype(obj)с одним аргументом? - Какова последовательность вызовов при объявлении класса с метаклассом? Чем
__new__метакласса отличается от__init__? - Почему паттерн Singleton реализуется через
__call__метакласса, а не через__new__экземпляра? - Как паттерн Registry через метакласс отличается от Registry через
__init_subclass__? Когда предпочтительнее каждый подход? - Что произойдёт, если класс пытается наследовать от двух классов с разными метаклассами?
- Что такое
__init_subclass__и когда он вызывается? Как передать параметры в этот хук? - Почему метакласс автоматически применяется ко всем подклассам? Как это использовать и когда это может быть проблемой?
type для динамического создания классов
Используя только type(name, bases, namespace), создайте три класса динамически:
Rectangleс атрибутамиwidthиheight, методамиarea()→width * height,perimeter()→2 * (width + height),__repr__→"Rectangle(width=W, height=H)".Circleс атрибутомradius, методамиarea()→π * r²,perimeter()→2 * π * r,__repr__→"Circle(radius=R)".Triangleс атрибутамиa,b,c(стороны), методамиarea()(формула Герона),perimeter(),__repr__.
Создайте функцию shape_info(shape), которая принимает любую фигуру и выводит тип, площадь и периметр. Убедитесь через isinstance, что объекты являются экземплярами созданных классов.
Пример использования:
r = Rectangle(4, 6)
c = Circle(5)
t = Triangle(3, 4, 5)
shape_info(r) # Rectangle: area=24.00, perimeter=20.00
shape_info(c) # Circle: area=78.54, perimeter=31.42
shape_info(t) # Triangle: area=6.00, perimeter=12.00
print(isinstance(r, Rectangle)) # TrueSingleton через метакласс
Реализуйте SingletonMeta — потокобезопасный метакласс для паттерна Singleton. Примените его к трём классам:
AppConfig— конфигурация приложения (хранитdebug,database_url,secret_key).RequestCounter— счётчик запросов с методамиincrement()иcount.Logger— логгер с методомlog(message)и атрибутомmessages(список).
Убедитесь, что: а) повторные вызовы конструктора возвращают тот же объект, б) __init__ не вызывается повторно (состояние не сбрасывается), в) разные классы имеют независимые синглтоны.
Пример использования:
cfg1 = AppConfig(debug=True, database_url="postgres://localhost/db")
cfg2 = AppConfig(debug=False) # возвращает тот же объект, debug остаётся True
print(cfg1 is cfg2) # True
print(cfg1.debug) # True — не сброшен
counter = RequestCounter()
counter.increment()
counter.increment()
counter2 = RequestCounter() # тот же объект
print(counter2.count) # 2 — сохранилось
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2) # True
print(cfg1 is logger1) # False — разные синглтоныRegistry через __init_subclass__
Создайте систему обработчиков команд для чат-бота. Базовый класс Command с __init_subclass__, который регистрирует подклассы по command_name. Каждый обработчик имеет метод execute(args: list) -> str. Реализуйте обработчики:
HelpCommand(command_name="/help")— возвращает список доступных команд.StatusCommand(command_name="/status")— возвращает"Сервер работает нормально".EchoCommand(command_name="/echo")— возвращает переданные аргументы объединёнными.TimeCommand(command_name="/time")— возвращает текущее время.
Напишите функцию process_message(message: str) -> str, которая: разбирает сообщение на команду и аргументы, находит обработчик через реестр, вызывает его.
Пример использования:
print(process_message("/help"))
# Доступные команды: /help, /status, /echo, /time
print(process_message("/echo Hello World"))
# Hello World
print(process_message("/status"))
# Сервер работает нормально
print(process_message("/unknown"))
# Неизвестная команда: /unknownВалидирующий метакласс для моделей
Создайте метакласс ModelMeta, который при объявлении класса:
- Проверяет, что все атрибуты, объявленные в
_required_fields(список строк), действительно будут переданы в__init__(проверьте черезinspect.signature). - Автоматически добавляет метод
fields() -> list, возвращающий список_required_fields. - Автоматически добавляет метод
from_dict(cls, data: dict), создающий объект из словаря. - Выводит сообщение при регистрации каждой модели.
Создайте две модели: UserModel(_required_fields=['username', 'email']) и ProductModel(_required_fields=['name', 'price', 'category']). Обе должны иметь соответствующие __init__.
Пример использования:
user = UserModel(username="alice", email="alice@example.com")
print(UserModel.fields()) # ['username', 'email']
user2 = UserModel.from_dict({"username": "bob", "email": "bob@example.com"})
print(user2.username) # bob
product = ProductModel.from_dict({"name": "Ноутбук", "price": 89990, "category": "electronics"})
print(ProductModel.fields()) # ['name', 'price', 'category']Комплексная система: Singleton + Registry
Создайте систему middleware для веб-приложения, объединяющую два паттерна:
MiddlewareRegistry— метакласс, реализующий Registry: при объявлении подклассаBaseMiddlewareавтоматически регистрирует его поmiddleware_name.MiddlewarePipeline— Singleton (через метакласс или__init_subclass__): единственный экземпляр, управляющий цепочкой middleware.
Базовый класс BaseMiddleware использует MiddlewareRegistry. Каждый middleware имеет метод process(request, next_handler).
Реализуйте три middleware:
LoggingMiddleware(middleware_name="logging")— логирует запрос.AuthMiddleware(middleware_name="auth")— проверяет токен.TimingMiddleware(middleware_name="timing")— замеряет время.
MiddlewarePipeline (Singleton) позволяет: add(middleware_name), process(request) — запускает цепочку.
Пример использования:
pipeline = MiddlewarePipeline()
pipeline.add("logging")
pipeline.add("auth")
pipeline.add("timing")
pipeline2 = MiddlewarePipeline()
print(pipeline is pipeline2) # True — Singleton
result = pipeline.process({"path": "/api/users", "token": "valid"})
print(result)