Наслідування, MRO та суперсила super()
Наслідування, MRO та суперсила super()
Вступ: Коли код перестає масштабуватися
Уявіть: ви розробляєте систему сповіщень для SaaS-платформи. Є три канали — Email, SMS, Push. Кожен потребує логування, авторизації та серіалізації для збереження в базу даних. Перша версія виглядає так:
# ❌ Монолітний підхід — катастрофа при масштабуванні
class EmailNotification:
def send(self, user, msg): ...
def log(self, msg): print(f"[EMAIL] {msg}") # дублювання
def to_json(self): return json.dumps(self.__dict__) # дублювання
class SmsNotification:
def send(self, user, msg): ...
def log(self, msg): print(f"[SMS] {msg}") # те саме!
def to_json(self): return json.dumps(self.__dict__) # те саме!
class PushNotification:
def send(self, user, msg): ...
def log(self, msg): print(f"[PUSH] {msg}") # і знову те саме!
def to_json(self): return json.dumps(self.__dict__) # і знову!
Три класи — і вже три копії однакового коду. Через місяць вимога змінилась: логування тепер у JSON-форматі. Вам доведеться змінювати код у трьох місцях — і це гарантований шлях до помилок.
Саме для вирішення цієї проблеми існує наслідування: механізм, що дозволяє визначити поведінку один раз і розподілити її між усіма нащадками. Але наслідування — це не просто «підключення батьківського коду». У Python під ним ховається складна система пошуку методів (MRO), кооперативний виклик конструкторів і архітектурний патерн Mixins.
Проблема дублювання
Проблема ієрархії
Проблема конфліктів
Python-рішення
super() як проксі-об'єкт по MRO, C3-лінеаризація для детермінованого порядку, Mixins для горизонтального розподілу поведінки.Частина I: Одиночне наслідування та перевизначення методів
Базовий синтаксис та відношення IS-A
Одиночне наслідування — це коли клас має рівно одного безпосереднього батька. У Python базовий клас вказується в дужках після назви дочірнього класу. Якщо батько не вказаний явно, клас автоматично наслідує від вбудованого object.
Головне правило: використовуйте наслідування лише тоді, коли між сутностями є чітке відношення IS-A («є об'єктом типу»). Для відношення HAS-A («містить у собі») — використовуйте композицію.
Перевизначення методів (Method Overriding) — ключовий механізм поліморфізму. Коли Python шукає метод на об'єкті Tesla.start_engine(), він перевіряє класи в порядку MRO і знаходить першу відповідну реалізацію. У нашому випадку — в ElectricCar, ігноруючи Car.start_engine та Vehicle.start_engine.
__dict__ екземпляра проти __dict__ класу
Важливо розуміти відмінність між атрибутами, що зберігаються безпосередньо на екземплярі, та тими, що належать класу. Python шукає атрибут спочатку в obj.__dict__, а потім — у type(obj).__dict__ по MRO.
class Vehicle:
wheels = 4 # атрибут класу — спільний для всіх екземплярів
def __init__(self, brand: str):
self.brand = brand # атрибут екземпляра — індивідуальний
car1 = Vehicle("Toyota")
car2 = Vehicle("Honda")
# Атрибути класу — спільні
print(Vehicle.wheels) # 4
print(car1.wheels) # 4 ← знайдено у Vehicle.__dict__, НЕ у car1.__dict__
print(car2.wheels) # 4
# Перевіряємо: car1.__dict__ не містить 'wheels'
print(car1.__dict__) # {'brand': 'Toyota'} ← лише атрибут екземпляра!
print(Vehicle.__dict__.keys()) # ..., 'wheels', ...
# Якщо присвоїти 'wheels' через екземпляр — він стає атрибутом ЕКЗЕМПЛЯРА,
# і ТІНЬУЄ (shadows) атрибут класу
car1.wheels = 2
print(car1.wheels) # 2 ← тепер з car1.__dict__
print(car2.wheels) # 4 ← Vehicle.__dict__ незмінений
print(Vehicle.wheels)# 4 ← Vehicle.__dict__ незмінений
Vehicle.tags.append(...)) змінює його для всіх екземплярів. Тоді як присвоєння через екземпляр (car.tags = [...]) лише створює новий атрибут екземпляра, не змінюючи класовий. Тому мутабельні атрибути (списки, словники) завжди ініціалізуйте у __init__, а не на рівні класу.Дослідження ієрархії: __base__, __bases__ та __subclasses__()
Python зберігає повну інформацію про ієрархію наслідування безпосередньо в об'єктах класів. Ці dunder-атрибути є вашими інструментами для дослідження та налагодження ієрархій.
cls.__bases__[0].object.object. Визначається алгоритмом C3-лінеаризації при створенні класу.from vehicle import Vehicle
from car import Car
from electric_car import ElectricCar
# ── Перевірка ієрархії ────────────────────────────────────────────────────────
print("ElectricCar.__base__: ", ElectricCar.__base__)
# ElectricCar.__base__: <class 'car.Car'>
print("ElectricCar.__bases__: ", ElectricCar.__bases__)
# ElectricCar.__bases__: (<class 'car.Car'>,)
print("Car.__base__: ", Car.__base__)
# Car.__base__: <class 'vehicle.Vehicle'>
print("Vehicle.__base__: ", Vehicle.__base__)
# Vehicle.__base__: <class 'object'> ← всі класи неявно від object
print("object.__base__: ", object.__base__)
# object.__base__: None ← вершина ієрархії
# ── MRO для ElectricCar ───────────────────────────────────────────────────────
print("\nElectricCar.__mro__:")
for cls in ElectricCar.__mro__:
print(f" {cls}")
# <class 'electric_car.ElectricCar'>
# <class 'car.Car'>
# <class 'vehicle.Vehicle'>
# <class 'object'>
# ── Живі підкласи ─────────────────────────────────────────────────────────────
print("\nVehicle.__subclasses__(): ", Vehicle.__subclasses__())
# [<class 'car.Car'>]
# (ElectricCar тут не відображений — він є підкласом Car, а не Vehicle напряму)
print("Car.__subclasses__(): ", Car.__subclasses__())
# [<class 'electric_car.ElectricCar'>]
# ── isinstance та issubclass ──────────────────────────────────────────────────
tesla = ElectricCar("Tesla", 2024, 75.0)
# isinstance перевіряє весь MRO-ланцюжок!
print(isinstance(tesla, ElectricCar)) # True — прямий тип
print(isinstance(tesla, Car)) # True — батько
print(isinstance(tesla, Vehicle)) # True — дід
print(isinstance(tesla, object)) # True — завжди True
# issubclass: перевірка на рівні класів
print(issubclass(ElectricCar, Car)) # True
print(issubclass(ElectricCar, Vehicle)) # True
print(issubclass(Car, ElectricCar)) # False — зворотній напрям неможливий
__subclasses__() — потужний інструмент для реалізації реєстрів плагінів та фабричних методів. Якщо всі плагіни наслідують від базового класу, ви можете динамічно отримати їх список без явної реєстрації. Але пам'ятайте: клас з'являється у __subclasses__() лише після того, як його модуль буде імпортований.Частина II: super() — проксі-об'єкт, а не батьківський клас
Чому super() — це не просто «виклик батька»
Більшість розробників-початківців читають super().__init__(...) як «виклик конструктора батьківського класу». Це хибна ментальна модель, яка ламається при першій же зустрічі з множинним наслідуванням.
Реальна поведінка super(): він повертає проксі-об'єкт, який делегує виклик методів на наступний клас у черзі MRO від поточного, а не на безпосереднього батька.
Різниця стає очевидною в ієрархії з множинним наслідуванням:
class A:
def greet(self):
print("A.greet")
super().greet() # Наступний у MRO — не object, а може бути B або C!
class B(A):
def greet(self):
print("B.greet")
super().greet() # Наступний у MRO від B в контексті D — це C
class C(A):
def greet(self):
print("C.greet")
super().greet() # Наступний у MRO від C — object (немає greet — AttributeError)
class D(B, C):
def greet(self):
print("D.greet")
super().greet() # Наступний у MRO від D — B
# MRO: [D, B, C, A, object]
d = D()
d.greet()
# D.greet → B.greet → C.greet → A.greet
Якби super() у B.greet означав «виклик батька» (A.greet), то C.greet ніколи б не виконався. Але завдяки MRO виклик проходить через увесь ланцюжок — саме це називається кооперативним наслідуванням (cooperative inheritance).
__class__ як cell variable: чому super() без аргументів не є магією
У Python 3 super() без аргументів — це не магія, а синтаксичний цукор компілятора. Коли Python компілює метод класу, він автоматично додає приховану клітинну змінну (cell variable) __class__, яка замикається (closure) на об'єкт класу, в якому описано метод.
class MyClass:
def my_method(self):
# Компілятор автоматично перетворює super() на super(__class__, self)
# де __class__ — це cell variable, що замикається на MyClass
# Ці два записи АБСОЛЮТНО еквівалентні:
super().some_method() # синтаксичний цукор Python 3
super(__class__, self).some_method() # явна форма Python 2/3
# Перевіримо: __class__ доступна у будь-якому методі
class Inspector:
def check(self):
# __class__ — це Inspector, незалежно від того, хто викликає метод
print(f"__class__ = {__class__}")
print(f"type(self) = {type(self)}")
class Child(Inspector):
pass
Child().check()
# __class__ = <class 'Inspector'> ← НЕЗМІННО: клас, де описано метод
# type(self) = <class 'Child'> ← тип реального об'єкта
super() без аргументів використовує __class__ (cell variable), він працює лише всередині методу класу. Якщо ви витягнете метод назовні (наприклад, через unwrapped = MyClass.my_method або у декораторі, що замінює функцію), __class__ може бути недоступною, і super() кине RuntimeError. У таких сценаріях використовуйте явну форму super(MyClass, self).Повна сигнатура super()
type у черзі MRO. При super() без аргументів — компілятор підставляє поточний клас (__class__).super() без аргументів — компілятор підставляє self (або cls у @classmethod). Це визначає, який саме MRO-ланцюжок буде обходитися.class Manager(Employee):
def __init__(self, name, salary, team_size):
# Python автоматично підставляє super(Manager, self)
# __class__ = Manager (cell variable)
# self = поточний екземпляр
super().__init__(name, salary)
class Manager(Employee):
def __init__(self, name, salary, team_size):
# Явне вказання — необхідно у Python 2,
# корисно в Python 3 при динамічному використанні
super(Manager, self).__init__(name, salary)
class GrandChild(Child):
def method(self):
# «Перескок» через рівень: шукати починаючи ПІСЛЯ Child
super(Child, self).method()
# УВАГА: це може пропустити важливу логіку Child.method!
# Використовуйте тільки коли точно знаєте MRO.
super() у @classmethod: фабричний патерн
Особливість super() у @classmethod — він отримує клас (cls), а не екземпляр (self). Це ключовий інструмент для успадкованих фабричних методів:
# factory_pattern.py
import json
from datetime import datetime
class BaseRecord:
"""Базовий запис з фабричним методом."""
def __init__(self, record_id: int, created_at: str):
self.record_id = record_id
self.created_at = created_at
@classmethod
def from_dict(cls, data: dict) -> "BaseRecord":
"""
Фабричний метод: створює екземпляр з словника.
cls — це реальний клас, на якому викликається метод!
Завдяки цьому підкласи автоматично отримують правильний тип.
"""
return cls(
record_id=data["id"],
created_at=data.get("created_at", datetime.now().isoformat())
)
@classmethod
def from_json(cls, json_str: str) -> "BaseRecord":
return cls.from_dict(json.loads(json_str))
class UserRecord(BaseRecord):
"""Підклас з розширеним конструктором."""
def __init__(self, record_id: int, created_at: str, username: str, email: str):
super().__init__(record_id, created_at)
self.username = username
self.email = email
@classmethod
def from_dict(cls, data: dict) -> "UserRecord":
# Викликаємо from_dict батька через super() у classmethod
# super() тут — super(UserRecord, cls), тобто BaseRecord
base_instance = super().from_dict(data)
# Але нам потрібен UserRecord, тому використовуємо cls напряму
return cls(
record_id=data["id"],
created_at=data.get("created_at", datetime.now().isoformat()),
username=data["username"],
email=data["email"],
)
def __repr__(self) -> str:
return f"UserRecord(id={self.record_id}, user={self.username!r})"
# Тест
data = {"id": 42, "username": "arakviel", "email": "arakviel@example.com"}
# Виклик from_dict на базовому класі → BaseRecord
base = BaseRecord.from_dict({"id": 1, "created_at": "2024-01-01"})
print(type(base)) # <class 'BaseRecord'>
# Виклик from_dict на підкласі → UserRecord (cls = UserRecord)
user = UserRecord.from_dict(data)
print(type(user)) # <class 'UserRecord'>
print(user) # UserRecord(id=42, user='arakviel')
@classmethod. Завдяки тому, що cls містить реальний клас (а не той, де описаний метод), метод from_dict або from_json визначений один раз у базовому класі, але автоматично повертає правильний підтип при виклику через підклас.Кооперативне наслідування та *args, **kwargs
Коли класи проектуються для роботи у складних ієрархіях, виникає проблема несумісності сигнатур конструкторів __init__. Кожен клас приймає свій набір аргументів, але повинен передати «зайві» аргументи далі по MRO.
Стандартне рішення — патерн **kwargs:
# cooperative_init.py
class GraphicElement:
def __init__(self, color: str = "black", **kwargs):
print(f" → GraphicElement.__init__(color={color!r})")
super().__init__(**kwargs) # передаємо залишок далі по MRO
self.color = color
class ClickableElement:
def __init__(self, on_click: str = "none", **kwargs):
print(f" → ClickableElement.__init__(on_click={on_click!r})")
super().__init__(**kwargs) # передаємо залишок далі
self.on_click = on_click
class ResizableElement:
def __init__(self, min_size: int = 10, **kwargs):
print(f" → ResizableElement.__init__(min_size={min_size})")
super().__init__(**kwargs)
self.min_size = min_size
# Button наслідує всіх трьох
class Button(GraphicElement, ClickableElement, ResizableElement):
def __init__(self, label: str, **kwargs):
print(f" → Button.__init__(label={label!r})")
super().__init__(**kwargs)
self.label = label
print("MRO:", [c.__name__ for c in Button.__mro__])
# MRO: ['Button', 'GraphicElement', 'ClickableElement', 'ResizableElement', 'object']
print("\nСтворення кнопки:")
btn = Button(
label="Submit",
color="blue",
on_click="submit_form",
min_size=20,
)
print(f"\nАтрибути: label={btn.label!r}, color={btn.color!r}, "
f"on_click={btn.on_click!r}, min_size={btn.min_size}")
**kwargsдалі черезsuper().**init**(**kwargs), і при цьому залишилися нерозпізнані аргументи — object.__init__ отримає їх і кине TypeError. Тому у всіх класах, що беруть участь у кооперативному наслідуванні, обов'язково використовуйте **kwargs і передавайте їх далі.Частина III: Множинне наслідування та проблема «діаманта»
Diamond Problem: як це виглядає
«Проблема діаманта» виникає, коли два батьківські класи мають спільного предка, і дочірній клас наслідує обох. Структура нагадує форму ромба (діаманта):
# diamond.py
class A:
def speak(self) -> str:
return "A.speak"
class B(A):
def speak(self) -> str:
return f"B.speak → {super().speak()}"
class C(A):
def speak(self) -> str:
return f"C.speak → {super().speak()}"
class D(B, C):
def speak(self) -> str:
return f"D.speak → {super().speak()}"
d = D()
print(d.speak())
# D.speak → B.speak → C.speak → A.speak
print(D.mro())
# [D, B, C, A, object]
Стара проблема: DFLS (Depth-First, Left-to-Right)
До Python 2.2 пошук методів відбувався за наївним алгоритмом DFLS: спочатку вглиб по лівій гілці, потім по правій. Для класу D(B, C) черга виглядала так:
Чому DFLS руйнує логіку: якщо C перевизначає метод speak() для специфічної поведінки, DFLS ніколи до нього не дістанеться — він знаходить A.speak() раніше. Специфічні нащадки ігноруються на користь далеких предків. Це пряме порушення принципу Liskov Substitution.
Саме ця проблема змусила розробників Python у версії 2.2 запровадити C3-лінеаризацію та класи нового стилю.
A.__init__ викликається двічі: один раз через ліву гілку B, інший — через праву C. Це призводить до повторної ініціалізації атрибутів батьківського класу — непередбачувана поведінка, що у сучасних Django/SQLAlchemy-проектах могла б стати критичним багом. C3-лінеаризація гарантує, що кожен клас у MRO відвідується рівно один раз.Частина IV: Глибокий розбір C3-лінеаризації
Математична основа алгоритму
C3-лінеаризація — алгоритм, запозичений з мови Dylan, що гарантує три властивості MRO:
- Локальний порядок предків: якщо
class D(B, C), то в MROBзавжди передC - Монотонність: якщо
XпередYв MRO будь-якого батька, тоXпередYі в MRO нащадка - Єдиний обхід: кожен клас відвідується рівно один раз
Формула лінеаризації класу Завантаження..., що наслідує Завантаження...:
Де Завантаження... — операція злиття кількох впорядкованих списків за правилом:
- Кожен список має голову (перший елемент) та хвіст (решта)
- Беремо голову першого списку — перевіряємо, чи немає її у хвостах інших списків
- Якщо її немає у жодному хвості — вилучаємо з усіх списків, додаємо до MRO
- Якщо є хоча б в одному хвості — вона заблокована, переходимо до голови наступного списку
- Якщо всі голови заблоковані — MRO неможливо побудувати (
TypeError)
Покроковий ручний розрахунок для «діаманта»
class A(object): pass
class B(A): pass
class C(A): pass
class D(B, C): pass
Відомі лінеаризації:
- Завантаження...
- Завантаження...
- Завантаження...
- Завантаження...
Розрахунок Завантаження...:
Крок 1: Беремо голову першого списку — B
Перевіряємо хвости:
- Хвіст
[B, A, object]→[A, object]:Bвідсутній ✅ - Хвіст
[C, A, object]→[A, object]:Bвідсутній ✅ - Хвіст
[B, C]→[C]:Bвідсутній ✅ (хвіст — це список БЕЗ голови)
B не заблокований → додаємо до MRO. Видаляємо B звідусіль.
Крок 2: Беремо голову першого списку — A
Перевіряємо хвости:
- Хвіст
[A, object]→[object]:Aвідсутній ✅ - Хвіст
[C, A, object]→[A, object]:Aприсутній ❌
A заблокований → переходимо до голови наступного списку → C.
- Хвіст
[A, object]:Cвідсутній ✅ - Хвіст
[C]→[](порожній):Cвідсутній ✅ (голова не є частиною свого хвоста)
C не заблокований → додаємо до MRO. Видаляємо C звідусіль.
Крок 3: Беремо голову — A
Перевіряємо хвости:
- Хвіст
[A, object]→[object]:Aвідсутній ✅ - Хвіст
[A, object]→[object]:Aвідсутній ✅
A не заблокований → додаємо до MRO.
Крок 4: Залишається object
Обидва списки містять лише object як голову (хвости порожні). Додаємо.
# Верифікація розрахунку
print(D.mro())
# [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]
Що це означає практично: при виклику d.speak() Python шукає метод у порядку D → B → C → A → object. Перевизначений у C метод знаходиться раніше ніж загальний у A — проблема DFLS вирішена.
Конфлікт лінеаризації: коли MRO неможливо побудувати
Бувають ієрархії, де вимоги двох батьків суперечать одна одній. Python виявляє це під час оголошення класу і кидає TypeError:
# mro_conflict.py
class X: pass
class Y: pass
class A(X, Y): pass # A вимагає: X перед Y
class B(Y, X): pass # B вимагає: Y перед X (суперечить A!)
# C не може задовольнити обидві вимоги одночасно
class C(A, B): pass # ← TypeError при оголошенні!
TypeError: Cannot create a consistent MRO — це сигнал архітектурної проблеми. Рішення: перегляньте порядок батьків або виділіть спільну логіку у третій незалежний клас.Налагодження MRO: __mro__ vs mro()
# Два способи отримати MRO — різний тип результату
# 1. Атрибут __mro__ — кортеж (tuple), незмінний
print(type(D.__mro__)) # <class 'tuple'>
print(D.__mro__) # (<class 'D'>, <class 'B'>, ..., <class 'object'>)
# 2. Метод mro() — список (list), зручніший для ітерації
print(type(D.mro())) # <class 'list'>
print(D.mro()) # [<class 'D'>, <class 'B'>, ..., <class 'object'>]
# Практичне використання: пошук класу, що надає метод
def find_method_owner(cls, method_name: str) -> type | None:
"""Знаходить, у якому класі фактично визначено метод."""
for klass in cls.__mro__:
if method_name in klass.__dict__: # __dict__ — лише власні атрибути!
return klass
return None
print(find_method_owner(D, "speak")) # <class 'D'> — D.speak існує
print(find_method_owner(D, "__init__")) # <class 'object'> — жоден не визначив
Частина V: CPython Internals — як MRO зберігається у пам'яті
PyTypeObject та tp_mro
На рівні CPython (реалізація Python на C) кожен клас Python є екземпляром структури PyTypeObject. MRO зберігається у полі tp_mro цієї структури як звичайний кортеж Python-об'єктів.
/* Спрощено з CPython/Include/cpython/object.h */
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* ім'я типу */
Py_ssize_t tp_basicsize; /* розмір екземпляра */
/* ... інші поля ... */
PyObject *tp_mro; /* кортеж MRO — обчислюється C3 при type.__new__ */
PyObject *tp_bases; /* кортеж батьківських класів (__bases__) */
PyObject *tp_base; /* перший батьківський клас (__base__) */
/* ... */
} PyTypeObject;
Ключові факти про tp_mro:
- MRO обчислюється один раз — під час виконання
type.__new__()при оголошенні класу - Результат кешується у
tp_mroназавжди (клас незмінний після створення) - Кожен виклик
obj.method()використовує цей кеш — пошук методів є O(n) по довжині MRO - Атрибут
cls.__mro__— це Python-обгортка навколоtp_mro
Як Python шукає метод: LOAD_ATTR та type_getattro
Коли Python виконує obj.method(), відбуваються такі кроки на рівні CPython:
# Ілюстрація Python-еквівалента алгоритму type_getattro
def type_getattro(obj, name: str):
"""
Спрощений Python-еквівалент C-функції type_getattro з CPython.
Саме цей алгоритм виконується при кожному доступі до атрибута.
"""
obj_type = type(obj)
# 1. Шукаємо дескриптор у MRO (перевіряємо __dict__ кожного класу)
meta_attribute = None
for base in obj_type.__mro__:
if name in base.__dict__:
meta_attribute = base.__dict__[name]
break
# 2. Якщо знайдений атрибут — дескриптор даних (має __set__) — він має пріоритет
if meta_attribute is not None and hasattr(meta_attribute, '__set__'):
return meta_attribute.__get__(obj, obj_type) # виклик дескриптора
# 3. Перевіряємо власний __dict__ екземпляра
if name in obj.__dict__:
return obj.__dict__[name]
# 4. Повертаємо non-data дескриптор або просто атрибут класу
if meta_attribute is not None:
if hasattr(meta_attribute, '__get__'):
return meta_attribute.__get__(obj, obj_type) # наприклад, метод
return meta_attribute
raise AttributeError(f"'{obj_type.__name__}' object has no attribute '{name}'")
__get__, але не __set__). При доступі через екземпляр викликається function.__get__(obj, type), що повертає зв'язаний метод (bound method) з вже підставленим self.Дескрипторний протокол: data vs non-data
Алгоритм type_getattro вище згадує дескриптори — об'єкти, що реалізують спеціальні методи __get__, __set__ і/або __delete__. Розуміння різниці між двома типами дескрипторів є ключовим для правильного трактування пріоритету атрибутів.
obj.__dict__). Типовий приклад — property. При записі obj.attr = value викликається descriptor.__set__(obj, value), а не безпосередній запис у obj.__dict__.obj.__dict__ є атрибут з таким самим ім'ям, він тіньує non-data дескриптор.# Ілюстрація пріоритетів дескрипторів
class DataDesc:
"""Data descriptor: має __get__ та __set__."""
def __get__(self, obj, objtype=None):
return obj.__dict__.get('_data_value', 'default')
def __set__(self, obj, value):
print(f"DataDesc.__set__({value!r})")
obj.__dict__['_data_value'] = value
class NonDataDesc:
"""Non-data descriptor: лише __get__."""
def __get__(self, obj, objtype=None):
return 'non_data_result'
class MyClass:
data_attr = DataDesc() # data descriptor
nondata_attr = NonDataDesc() # non-data descriptor
obj = MyClass()
# Data descriptor: __set__ перехоплює присвоєння
obj.data_attr = "hello" # → DataDesc.__set__('hello')
print(obj.data_attr) # → DataDesc.__get__ → 'hello'
# Non-data descriptor: атрибут екземпляра ПЕРЕМАГАЄ
obj.__dict__['nondata_attr'] = 'instance_value'
print(obj.nondata_attr) # → 'instance_value' (не NonDataDesc!)
# property — це data descriptor:
class Circle:
def __init__(self, radius):
self._radius = radius
@property # data descriptor: __get__ + __set__ + __delete__
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Радіус не може бути від'ємним")
self._radius = value
c = Circle(5)
c.radius = 10 # → property.__set__ → перевірка → c._radius = 10
print(c.radius) # → property.__get__ → 10
# c.__dict__ не містить 'radius' — лише '_radius'
print(c.__dict__) # {'_radius': 10}
@property або @cached_property, ці дескриптори завжди перехоплюють доступ до атрибута навіть якщо в obj.__dict__ є запис з тим самим ім'ям (у разі data descriptor). Саме тому присвоєння obj.radius = -1 викликає сеттер, а не записує у __dict__.Вимірювання: вартість пошуку по MRO
# mro_performance.py
import timeit
class A: pass
class B(A): pass
class C(B): pass
class D(C): pass
class E(D): pass
# Метод визначено тільки в A (кінець MRO)
class A:
def method(self): return 42
# Наскільки повільніше шукати метод через 4 рівні vs через 0?
e = E()
a = A() if hasattr(A, 'method') else E()
# Benchmark
t_short = timeit.timeit(lambda: a.method(), number=1_000_000)
t_long = timeit.timeit(lambda: e.method(), number=1_000_000)
print(f"Виклик через 1 рівень MRO: {t_short:.3f}с")
print(f"Виклик через 5 рівнів MRO: {t_long:.3f}с")
print(f"Різниця: {(t_long/t_short - 1)*100:.1f}%")
Накладні витрати пошуку по MRO є незначними для більшості застосунків. CPython додатково використовує inline cache (починаючи з Python 3.11) для кешування результату пошуку методу безпосередньо у байткоді — повторні виклики того самого методу фактично безкоштовні.
Частина VI: Liskov Substitution Principle та правильне наслідування
LSP: математична основа правильного IS-A
Принцип підстановки Барбари Лісков (LSP) стверджує: якщо S є підтипом T, то об'єкти типу T можна замінити об'єктами типу S без зміни коректності програми.
Простіше: підклас повинен повністю виконувати контракт батьківського класу.
# ❌ Порушення LSP: підклас змінює очікувану поведінку
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def set_width(self, w: float) -> None:
self.width = w
def set_height(self, h: float) -> None:
self.height = h
class Square(Rectangle):
"""❌ НЕПРАВИЛЬНО: квадрат порушує LSP при наслідуванні від прямокутника."""
def set_width(self, w: float) -> None:
# Квадрат вимушений змінювати обидва розміри одночасно —
# це порушує контракт Rectangle
self.width = w
self.height = w
def set_height(self, h: float) -> None:
self.width = h
self.height = h
def test_rectangle_contract(rect: Rectangle) -> None:
"""Тест, що повинен проходити для будь-якого Rectangle та його підкласів."""
rect.set_width(5)
rect.set_height(10)
expected_area = 5 * 10 # 50
actual_area = rect.area()
assert actual_area == expected_area, \
f"LSP ПОРУШЕНО: очікувалось {expected_area}, отримано {actual_area}"
print(f"Контракт виконано: {actual_area}")
r = Rectangle(3, 4)
test_rectangle_contract(r) # ✅ Площа: 50
s = Square(3, 3)
test_rectangle_contract(s) # ❌ AssertionError: LSP порушено!
# Після set_height(10) — width теж стало 10 → area = 100, а не 50
Правильне вирішення: якщо Square не може повністю виконати контракт Rectangle, вони не повинні бути у відношенні IS-A. Обидва можуть наслідувати від спільного абстрактного класу Shape, який не нав'язує незалежні сеттери розмірів.
# ✅ Правильна архітектура: спільний предок без порушення контракту
import math
from abc import ABC, abstractmethod
class Shape(ABC):
"""Абстрактна фігура: спільний предок Rectangle та Square."""
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
class Rectangle(Shape):
"""Прямокутник: незалежні ширина та висота."""
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float: return self.width * self.height
def perimeter(self) -> float: return 2 * (self.width + self.height)
class Square(Shape):
"""
Квадрат: єдина сторона.
НЕ наслідує Rectangle — вони обидва Shape, але не пов'язані IS-A.
"""
def __init__(self, side: float):
self.side = side
def area(self) -> float: return self.side ** 2
def perimeter(self) -> float: return 4 * self.side
# Тепер LSP виконується для обох:
def print_shape_info(shape: Shape) -> None:
print(f"{type(shape).__name__}: area={shape.area():.2f}, perimeter={shape.perimeter():.2f}")
for s in [Rectangle(4, 6), Square(5)]:
print_shape_info(s) # ✅ Будь-який Shape працює коректно
class Child(Parent), запитайте: «Чи можу я замінити кожне використання Parent на Child без зміни коректності програми?» Якщо ні — переходьте до композиції або перегляньте ієрархію абстракцій.Частина VII: Патерн Mixins (Класи-домішки)
Концепція та правила проектування
Принципове розмежування: наслідування моделює онтологічні відносини предметної галузі (IS-A), тоді як Mixins є технічним механізмом горизонтального перевикористання коду, що не несе семантичного навантаження.
Mixin (домішка) — це клас, що призначений виключно для горизонтального розподілу поведінки між непов'язаними класами. Він не моделює сутність (не є окремою концепцією предметної галузі), а надає набір функцій.
Без власного стану
__init__. Якщо __init__ потрібен — він обов'язково має викликати super().__init__(*args, **kwargs), щоб не обривати ланцюжок MRO.Single Responsibility
Порядок у наслідуванні
Суфікс 'Mixin'
Mixin. Це конвенція, що сигналізує читачу коду: «цей клас не призначений для самостійного використання».Практична реалізація: повноцінний набір домішок
# mixins.py
import json
import hashlib
from datetime import datetime
from typing import Any
class JSONSerializableMixin:
"""
Домішка для автоматичної JSON-серіалізації та десеріалізації.
Ігнорує атрибути з підкресленням (_protected, __private).
"""
def to_json(self) -> str:
"""Серіалізує публічні атрибути об'єкта у JSON-рядок."""
public_data = {
key: value
for key, value in self.__dict__.items()
if not key.startswith('_')
}
return json.dumps(public_data, ensure_ascii=False, default=str)
@classmethod
def from_json(cls, json_str: str) -> "JSONSerializableMixin":
"""Десеріалізує JSON-рядок у новий об'єкт класу."""
data = json.loads(json_str)
return cls(**data)
def to_dict(self) -> dict[str, Any]:
"""Повертає публічні атрибути як словник."""
return {
key: value
for key, value in self.__dict__.items()
if not key.startswith('_')
}
class LoggingMixin:
"""
Домішка для структурованого логування операцій.
Автоматично підставляє ім'я реального класу в лог.
"""
def log(self, message: str, level: str = "INFO") -> None:
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
class_name = self.__class__.__name__
print(f"[{timestamp}] [{level:5}] [{class_name}] {message}")
def log_error(self, error: Exception) -> None:
self.log(f"Помилка: {type(error).__name__}: {error}", level="ERROR")
class ValidationMixin:
"""
Домішка для декларативної валідації даних.
Підкласи можуть перевизначити `_validation_rules` — список функцій-перевірок.
"""
_validation_rules: list = [] # клас-рівень: перевизначте у підкласі
def validate(self) -> list[str]:
"""Запускає всі правила валідації. Повертає список помилок."""
errors = []
for rule in self._validation_rules:
error = rule(self)
if error:
errors.append(error)
return errors
def assert_valid(self) -> None:
"""Перевіряє валідність або кидає ValueError зі списком помилок."""
errors = self.validate()
if errors:
raise ValueError(f"Помилки валідації: {'; '.join(errors)}")
class HashableMixin:
"""
Домішка для генерації хешу об'єкта на основі його JSON-представлення.
Вимагає JSONSerializableMixin.
"""
@property
def content_hash(self) -> str:
"""SHA-256 хеш вмісту об'єкта. Ідентичні дані → ідентичний хеш."""
if not hasattr(self, 'to_json'):
raise TypeError("HashableMixin вимагає JSONSerializableMixin")
content = self.to_json().encode('utf-8')
return hashlib.sha256(content).hexdigest()[:16]
# business_entities.py
from mixins import JSONSerializableMixin, LoggingMixin, ValidationMixin, HashableMixin
class Product(JSONSerializableMixin, LoggingMixin, ValidationMixin, HashableMixin):
"""Продукт: поєднує всі чотири домішки."""
_validation_rules = [
lambda self: "Ціна має бути > 0" if self.price <= 0 else None,
lambda self: "Назва не може бути порожньою" if not self.name.strip() else None,
lambda self: "Кількість не може бути від'ємною" if self.stock < 0 else None,
]
def __init__(self, name: str, price: float, stock: int = 0):
self.name = name
self.price = price
self.stock = stock
self.log(f"Створено продукт '{self.name}' (ціна: {self.price} грн)")
def __repr__(self) -> str:
return f"Product(name={self.name!r}, price={self.price})"
class User(JSONSerializableMixin, LoggingMixin, ValidationMixin):
"""Користувач: серіалізація, логування, валідація (без хешування)."""
_validation_rules = [
lambda self: "Username занадто короткий" if len(self.username) < 3 else None,
lambda self: "Email повинен містити '@'" if '@' not in self.email else None,
]
def __init__(self, username: str, email: str):
self.username = username
self.email = email
self._password_hash = "..." # захищений — не потрапить у to_json()
self.log(f"Зареєстровано користувача {self.username!r}")
def __repr__(self) -> str:
return f"User(username={self.username!r})"
# demo.py
from business_entities import Product, User
print("=== Демонстрація Mixins ===\n")
# ── Product ────────────────────────────────────────────────────────────────────
prod = Product("MacBook Pro", 89999.0, stock=5)
print(f"JSON: {prod.to_json()}")
print(f"Hash: {prod.content_hash}")
# Валідація
errors = prod.validate()
print(f"Помилки валідації: {errors or 'немає'}")
# Невалідний продукт
bad = Product("", -100.0, stock=-1)
errors = bad.validate()
print(f"Помилки bad-продукту: {errors}")
print()
# ── User ───────────────────────────────────────────────────────────────────────
user = User("arakviel", "arakviel@example.com")
print(f"JSON: {user.to_json()}")
# Зверніть увагу: _password_hash відсутній!
# MRO
print(f"\nMRO Product: {[c.__name__ for c in Product.__mro__]}")
print(f"MRO User: {[c.__name__ for c in User.__mro__]}")
Антипатерни Mixin: що не слід робити
Попри гнучкість, Mixins мають чіткі межі застосування. Порушення цих меж призводить до архітектурних проблем, що складно усунути.
❌ Mixin зі станом
__init__, що зберігає стан без передачі **kwargs далі через super(), обриває ланцюжок ініціалізації MRO. Усі наступні класи у черзі залишаться неініціалізованими.❌ Залежності між Mixin'ами
HashableMixin потребує to_json з JSONSerializableMixin), це прихована залежність, яка не видна зі сигнатури класу. Документуйте такі вимоги явно або перевіряйте через hasattr.❌ Занадто глибокі ланцюжки
❌ Mixin перевизначає бізнес-метод
super()), а не замінювати саму логіку.# ❌ Антипатерн 1: Mixin зі станом без super().__init__(**kwargs)
class BadCacheMixin:
def __init__(self):
self._cache = {} # ← ПОМИЛКА: kwargs не передаються!
# Якщо ця домішка у ланцюжку, наступні __init__ ніколи не викличуться
# ✅ Правильно: завжди передавайте **kwargs
class GoodCacheMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs) # ← ланцюжок не обривається
self._cache: dict = {}
def get_cached(self, key: str):
return self._cache.get(key)
def set_cached(self, key: str, value) -> None:
self._cache[key] = value
# ❌ Антипатерн 2: прихована залежність між Mixin'ами
class BadHashMixin:
@property
def content_hash(self) -> str:
# Неявна вимога: об'єкт повинен мати метод to_json()
# Якщо JSONSerializableMixin не підключений — RuntimeError
return hashlib.sha256(self.to_json().encode()).hexdigest()[:16] # type: ignore
# ✅ Правильно: явна перевірка з інформативним повідомленням
class SafeHashMixin:
@property
def content_hash(self) -> str:
if not hasattr(self, 'to_json'):
raise TypeError(
f"{type(self).__name__} використовує SafeHashMixin, але не має методу "
f"to_json(). Додайте JSONSerializableMixin до переліку батьків."
)
return hashlib.sha256(self.to_json().encode()).hexdigest()[:16]
__init_subclass__: сучасна альтернатива метакласам
__init_subclass__ (PEP 487, Python 3.6+) — це метод, що автоматично викликається Python щоразу, коли оголошується новий підклас даного класу. Це дозволяє базовому класу реагувати на факт успадкування без використання метакласів.
class Base:
def __init_subclass__(cls, /, **kwargs):
"""
Викликається при оголошенні КОЖНОГО підкласу Base.
cls — це щойно створений підклас (не екземпляр!).
kwargs — ключові аргументи, передані в дужках при оголошенні.
"""
super().__init_subclass__(**kwargs) # обов'язково для кооперативності
print(f"Новий підклас: {cls.__name__}")
class Child(Base): # → друкує: «Новий підклас: Child»
pass
class GrandChild(Child): # → друкує: «Новий підклас: GrandChild»
pass
Практичне застосування — автоматичний реєстр без метакласів:
# auto_registry.py
from abc import ABC, abstractmethod
class Serializer(ABC):
"""
Базовий клас для серіалізаторів з автоматичним реєстром форматів.
Підкласи оголошуються з аргументом format_name='...' у дужках.
"""
_registry: dict[str, type["Serializer"]] = {}
def __init_subclass__(cls, format_name: str | None = None, **kwargs) -> None:
super().__init_subclass__(**kwargs)
if format_name is not None:
# Реєструємо підклас у момент його оголошення
Serializer._registry[format_name] = cls
@abstractmethod
def dumps(self, data: object) -> str: ...
@abstractmethod
def loads(self, raw: str) -> object: ...
@classmethod
def for_format(cls, fmt: str) -> "Serializer":
try:
return cls._registry[fmt]()
except KeyError:
raise ValueError(f"Невідомий формат: {fmt!r}. Доступні: {list(cls._registry)}")
# Реєстрація відбувається АВТОМАТИЧНО при оголошенні класу:
class JsonSerializer(Serializer, format_name="json"):
def dumps(self, data) -> str:
import json; return json.dumps(data, ensure_ascii=False)
def loads(self, raw):
import json; return json.loads(raw)
class YamlSerializer(Serializer, format_name="yaml"):
def dumps(self, data) -> str:
return "\n".join(f"{k}: {v}" for k, v in data.items())
def loads(self, raw):
return dict(line.split(": ", 1) for line in raw.splitlines() if ": " in line)
# Використання фабрики — жодного явного імпорту конкретних класів:
serializer = Serializer.for_format("json")
print(serializer.dumps({"name": "Alice", "age": 30}))
# {"name": "Alice", "age": 30}
print(Serializer._registry)
# {'json': <class 'JsonSerializer'>, 'yaml': <class 'YamlSerializer'>}
__init_subclass__ над метакласами: метакласи змінюють сам процес створення класу і можуть вступати в конфлікт між собою (неможливо мати два різні метакласи в одній ієрархії без спеціального об'єднання). __init_subclass__ — це звичайний метод, що бере участь у MRO за стандартними правилами кооперативного наслідування через super().__init_subclass__(**kwargs).Частина VIII: Практичні завдання
Рівень 1 (Базовий): Ієрархія геометричних фігур
Закріпіть синтаксис одиночного наслідування, super() та перевизначення методів.
Завдання: Реалізуйте ієрархію Shape → Polygon → Triangle/Rectangle → Square. Кожен клас повинен коректно передавати аргументи через super().__init__() та перевизначати метод area().
# shapes.py
import math
class Shape:
"""Абстрактний базовий клас для фігур."""
def __init__(self, color: str = "black"):
self.color = color
def area(self) -> float:
raise NotImplementedError(f"{type(self).__name__} має реалізувати area()")
def perimeter(self) -> float:
raise NotImplementedError(f"{type(self).__name__} має реалізувати perimeter()")
def describe(self) -> str:
return (
f"{self.__class__.__name__} [{self.color}]: "
f"площа={self.area():.2f}, периметр={self.perimeter():.2f}"
)
class Polygon(Shape):
"""Багатокутник: знає кількість сторін."""
def __init__(self, sides: int, color: str = "black"):
super().__init__(color)
self.sides = sides
def describe(self) -> str:
base = super().describe()
return f"{base}, сторін={self.sides}"
class Rectangle(Polygon):
def __init__(self, width: float, height: float, color: str = "black"):
super().__init__(sides=4, color=color)
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
class Square(Rectangle):
def __init__(self, side: float, color: str = "black"):
super().__init__(width=side, height=side, color=color)
self.side = side
# Не перевизначаємо area() та perimeter() — Rectangle вже правильно рахує
class Triangle(Polygon):
def __init__(self, a: float, b: float, c: float, color: str = "black"):
if a + b <= c or a + c <= b or b + c <= a:
raise ValueError(f"Недопустимі сторони: {a}, {b}, {c}")
super().__init__(sides=3, color=color)
self.a, self.b, self.c = a, b, c
def area(self) -> float: # Формула Герона
s = self.perimeter() / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def perimeter(self) -> float:
return self.a + self.b + self.c
# Тест
shapes: list[Shape] = [
Rectangle(4, 6, "blue"),
Square(5, "red"),
Triangle(3, 4, 5, "green"),
]
for shape in shapes:
print(shape.describe())
print(f" isinstance(shape, Shape): {isinstance(shape, Shape)}")
print(f" isinstance(shape, Polygon): {isinstance(shape, Polygon)}")
print()
# Перевірка MRO
print("Square MRO:", [c.__name__ for c in Square.__mro__])
Рівень 2 (Середній): Система банківських транзакцій
Реальний production-сценарій: комбінація лінійного наслідування та Mixins у системі фінансових транзакцій.
Рівень 3 (Advanced): Міні-фреймворк реєстру плагінів через __init_subclass__
Реалізуйте систему плагінів для обробки даних. Кожен плагін автоматично реєструється при оголошенні класу — без явних викликів реєстрації. Це сучасна альтернатива метакласам для Python 3.6+.
# plugin_framework.py
from abc import ABC, abstractmethod
from typing import ClassVar
class DataProcessor(ABC):
"""
Базовий клас для плагінів обробки даних.
Автоматичний реєстр через __init_subclass__ (PEP 487).
"""
# Реєстр: {ім'я_формату: клас_плагіна}
_registry: ClassVar[dict[str, type["DataProcessor"]]] = {}
def __init_subclass__(cls, format_name: str | None = None, **kwargs) -> None:
"""
Викликається Python АВТОМАТИЧНО при оголошенні кожного підкласу.
Це альтернатива метакласам — чиста та явна.
"""
super().__init_subclass__(**kwargs)
if format_name is not None:
DataProcessor._registry[format_name.lower()] = cls
print(f"[Registry] Зареєстровано: '{format_name}' → {cls.__name__}")
# ── Абстрактний інтерфейс ─────────────────────────────────────────────────
@abstractmethod
def parse(self, raw: str) -> list[dict]:
"""Розбирає вхідний рядок у список записів."""
...
@abstractmethod
def serialize(self, records: list[dict]) -> str:
"""Серіалізує список записів у вихідний формат."""
...
# ── Конкретні методи (шаблонний метод) ───────────────────────────────────
def process(self, raw: str, transform=None) -> str:
"""
Шаблонний метод: parse → transform → serialize.
Підкласи реалізують parse/serialize, а transform — зовнішня функція.
"""
records = self.parse(raw)
if transform is not None:
records = [transform(r) for r in records]
return self.serialize(records)
# ── Фабричний метод ───────────────────────────────────────────────────────
@classmethod
def for_format(cls, format_name: str) -> "DataProcessor":
"""Фабрика: повертає екземпляр плагіна для вказаного формату."""
plugin_cls = cls._registry.get(format_name.lower())
if plugin_cls is None:
available = list(cls._registry.keys())
raise ValueError(
f"Невідомий формат: {format_name!r}. "
f"Доступні: {available}"
)
return plugin_cls()
@classmethod
def available_formats(cls) -> list[str]:
return sorted(cls._registry.keys())
# ── Конкретні плагіни: автоматично реєструються при оголошенні ────────────────
class CsvProcessor(DataProcessor, format_name="csv"):
"""CSV-плагін: format_name='csv' → автореєстрація."""
def parse(self, raw: str) -> list[dict]:
lines = [l.strip() for l in raw.strip().splitlines() if l.strip()]
if not lines:
return []
headers = [h.strip() for h in lines[0].split(',')]
result = []
for line in lines[1:]:
values = [v.strip() for v in line.split(',')]
result.append(dict(zip(headers, values)))
return result
def serialize(self, records: list[dict]) -> str:
if not records:
return ""
headers = list(records[0].keys())
rows = [",".join(headers)]
for record in records:
rows.append(",".join(str(record.get(h, "")) for h in headers))
return "\n".join(rows)
class JsonLinesProcessor(DataProcessor, format_name="jsonl"):
"""JSONL-плагін (один JSON-об'єкт на рядок)."""
import json as _json
def parse(self, raw: str) -> list[dict]:
import json
return [json.loads(line) for line in raw.strip().splitlines() if line.strip()]
def serialize(self, records: list[dict]) -> str:
import json
return "\n".join(json.dumps(r, ensure_ascii=False) for r in records)
class TsvProcessor(DataProcessor, format_name="tsv"):
"""TSV-плагін (Tab-Separated Values)."""
def parse(self, raw: str) -> list[dict]:
lines = raw.strip().splitlines()
if not lines:
return []
headers = lines[0].split('\t')
return [dict(zip(headers, line.split('\t'))) for line in lines[1:]]
def serialize(self, records: list[dict]) -> str:
if not records:
return ""
headers = list(records[0].keys())
rows = ["\t".join(headers)]
for r in records:
rows.append("\t".join(str(r.get(h, "")) for r in [r] for h in headers))
return "\n".join(rows)
# ── Демонстрація ──────────────────────────────────────────────────────────────
def main() -> None:
print(f"\nДоступні формати: {DataProcessor.available_formats()}")
print(f"Реєстр: {list(DataProcessor._registry.keys())}")
# CSV → обробка → JSONL (конвертація формату)
csv_data = """name,age,city
Олена,28,Київ
Іван,35,Харків
Марія,22,Львів"""
print("\n=== CSV → parse → JSONL serialize ===")
csv_proc = DataProcessor.for_format("csv")
records = csv_proc.parse(csv_data)
print(f"Розібрано записів: {len(records)}")
jsonl_proc = DataProcessor.for_format("jsonl")
output = jsonl_proc.serialize(records)
print("JSONL:")
print(output)
# Шаблонний метод з трансформацією
print("\n=== CSV → process → uppercase names ===")
def uppercase_name(r: dict) -> dict:
return {**r, "name": r["name"].upper()}
result = csv_proc.process(csv_data, transform=uppercase_name)
print(result)
# Невідомий формат
try:
DataProcessor.for_format("xml")
except ValueError as e:
print(f"\nОчікувана помилка: {e}")
if __name__ == "__main__":
main()
Практична лабораторія: HTTP Middleware Pipeline від А до Я
Реалізуємо мінімалістичний фреймворк для обробки HTTP-запитів, що демонструє всі концепції статті в єдиній системі:
| Концепція | Де застосовано |
|---|---|
| Одиночне наслідування | Request, Response — базові типи даних |
Кооперативний super().handle() | Кожен Middleware передає запит далі по MRO |
| Множинне наслідування + diamond | SecureLoggingHandler(LoggingMixin, AuthMixin, BaseHandler) |
| Mixins | LoggingMixin, AuthMixin, RateLimitMixin, CORSMixin |
super() у __init__ з **kwargs | Всі Mixin-конструктори передають **kwargs далі |
isinstance / issubclass | Типізована маршрутизація запитів |
__init_subclass__ | Автоматична реєстрація обробників маршрутів |
| LSP | AuthMixin.handle() повністю виконує контракт BaseHandler |
Архітектура системи
Реалізація
Підсумки та найкращі практики
IS-A vs HAS-A
Кооперативний super()
**kwargs через super().__init__(**kwargs). Якщо хоч один клас у ланцюжку пропустить це — object.__init__ отримає невідомі аргументи і впаде з TypeError.Гігієна Mixins
Mixin, ліворуч від основного класу. Одна домішка — одна відповідальність. Замість глибокої ієрархії Base → Loggable → Serializable → Product — плоска Product(LoggingMixin, JSONSerializableMixin).Перевірка MRO
ClassName.mro(). Використовуйте find_method_owner(cls, 'method_name') щоб точно знайти, де визначено конкретний метод.LSP як критерій
class Child(Parent) запитуйте: чи виконує підклас всі передумови та постумови батьківського класу? Класичний приклад порушення — Square(Rectangle).**init_subclass** замість метакласів
__init_subclass__ (PEP 487, Python 3.6+). Метакласи потрібні лише для найбільш складних сценаріїв модифікації самого процесу створення класу.Інкапсуляція, Керування Доступом та Властивості
Глибоке дослідження інкапсуляції в Python — від філософії «дорослих людей» та угод про іменування до механізму Name Mangling, обчислювальних властивостей з @property, кастомних дескрипторів та валідації даних через геттери й сеттери.
Абстракція — ABC проти Статичних Протоколів (PEP 544)
Глибокий аналіз двох підходів до абстракції в Python — формальних Abstract Base Classes (ABC) та структурних Protocols (PEP 544). Номінативна та структурна типізація, mypy, runtime-перевірки та вибір правильного інструменту для production-коду.