Ключевые выводы по ООП Python
Этот урок — single-page synthesis Module 04. Если вы прочитали уроки 01-05, эта страница даёт comparative view: пять концепций OOP в Python — PyTypeObject как class, MRO как C3 linearization, descriptors как slot mechanism behind property, __slots__ как memory optimization, @dataclass как codegen. Используйте перед экзаменом для self-assessment, и для cross-module mental map с M02 / M03.
Pre-exam consolidation: 5 концепций модуля
| # | Концепция | M04 lesson | CPython source | Cross-module link |
|---|---|---|---|---|
| 1 | PyTypeObject = Python class на уровне C-runtime | Урок 01 | Include/cpython/object.h | M02 урок 03 (instance __dict__ это PyDictObject) |
| 2 | Slot wrappers мапят dunder → tp_* slots | Урок 02 | Objects/typeobject.c slot_tp_* | M02 урок 06 (__eq__/__hash__ invariant) |
| 3 | MRO via C3 (Barrett OOPSLA 1996) | Урок 03 | Objects/typeobject.c mro_implementation | n/a (M04 internal) |
| 4 | Descriptor protocol behind @property, @classmethod | Урок 04 | Objects/descrobject.c | n/a (M04 internal) |
| 5 | __slots__ + @dataclass: memory + codegen | Урок 05 | Lib/dataclasses.py _create_fn | M02 урок 06 (compact memory layout) |
Cross-module bridges — где OOP встречается с предыдущими модулями
M02 урок 03 → M04 урок 01: instance dict это PyDictObject
Каждый instance с __dict__ несёт тот же hash table, что и обычный {}-литерал — open addressing, perturbation probe (5*i + 1 + perturb) & mask, USABLE_FRACTION 2/3. На cpython-3.12.7 пустой __dict__ весит 296 байт. Это motivates __slots__ (M04 урок 05): для 1M instances — экономия ~300MB (296 → 0 байт __dict__).
# Один и тот же PyDictObject:
{'x': 10} # обычный dict literal
some_instance.__dict__ # instance __dict__
type(some_instance).__dict__ # mappingproxy → wraps PyDictObject (read-only)
M02 урок 06 → M04 урок 02: hashable contract
Hashable invariant a == b ⇒ hash(a) == hash(b) enforced на уровне CPython через tp_hash / tp_richcompare slots. Если переопределили __eq__ без __hash__ → CPython автоматически устанавливает tp_hash = NULL → unhashable. Это не «контракт на бумаге», а runtime mechanism.
M03 урок 04 → M04 урок 06: lru_cache требует hashable args
@functools.lru_cache(maxsize=N) хранит cache в dict (M02 урок 03), ключ = hashed args. Mutable list/dict как arg → unhashable → TypeError ‘unhashable type: list’. Это прямая связка M02 (hashable, dict, hash table) + M03 (functools, lru_cache) + M04 (tp_hash slot wrapper, unhashable enforcement).
from functools import lru_cache
@lru_cache(maxsize=128)
def f(arg): return arg * 2
f(5) # OK - int hashable
f((1, 2, 3)) # OK - tuple hashable (M02 урок 02 - immutable → hashable)
f([1, 2, 3]) # TypeError: unhashable type: 'list'
# (M02 урок 06 - mutable → не hashable)
# То же касается custom classes:
class Bad:
def __eq__(self, other): return True
# __hash__ не определён → tp_hash = NULL (M04 урок 02)
@lru_cache
def g(b): return id(b)
g(Bad()) # TypeError: unhashable type: 'Bad'
M03 урок 01 → M04 урок 01: mutable defaults и class attributes — один pattern
Оба evaluated один раз и shared:
| Pattern | Где хранится | Trap |
|---|---|---|
def f(x, lst=[]) (M03) | func.__defaults__ tuple | shared list между всеми calls без явного arg |
class C: items = [] (M04) | cls.__dict__['items'] (tp_dict) | shared list между всеми instances без instance attr |
Решение симметрично: для function — sentinel (lst=None; if lst is None: lst = []); для class — instance attr через __init__ (def __init__(self): self.items = []).
Comparative table — какой mechanism за каким декоратором
| Декоратор | Тип | C-implementation | Behavior |
|---|---|---|---|
@property | data descriptor | Objects/descrobject.c | __get__ + __set__ + __delete__; precedence > instance __dict__ |
@classmethod | non-data descriptor | Objects/funcobject.c | __get__ returns method bound to class (cls вместо self) |
@staticmethod | non-data descriptor | Objects/funcobject.c | __get__ returns underlying function as-is |
@dataclass | function (не descriptor) | Lib/dataclasses.py | codegen via exec(); generates __init__/__repr__/__eq__ |
@dataclass(slots=True) | function | Lib/dataclasses.py _add_slots | dataclass + creates new class с __slots__ |
@functools.lru_cache | function (returns wrapper) | Modules/_functoolsmodule.c | wrapper хранит dict с hashed-args ключами |
Все они — высокоуровневые abstractions над низкоуровневыми CPython mechanisms (descriptor protocol, tp_dict, exec()). Когда вы читаете чужой код с этими декораторами, на уровне runtime там разворачивается именно тот mechanism, что описан в правом столбце.
Self-assessment перед экзаменом
Чек-лист (8 пунктов). Проверьте каждое утверждение для себя — если не уверены, перечитайте указанный урок:
- Я могу объяснить, почему
type(int) is typeи почему type — its-own-metaclass. (M04 урок 01) - Я могу нарисовать
D.__mro__для diamondD(B, C)сB(A), C(A)через C3 step-by-step merge. (M04 урок 03) - Я могу различить data vs non-data descriptor и привести пример lookup precedence:
@property(data) выигрывает над instance__dict__,@classmethod(non-data) — нет. (M04 урок 04) - Я знаю, что генерирует
@dataclass:__init__,__repr__,__eq__(опционально__hash__еслиfrozen=True); генерация черезexec()source-строки в_create_fn(). (M04 урок 05) - Я могу перечислить 3 правила
__slots__inheritance: каждый класс в иерархии нуждается в__slots__; child пишет только дополнительные slots;__slots__блокирует динамические attrs. (M04 урок 05) - Я могу объяснить, почему переопределение
__eq__без__hash__делает класс unhashable (CPython устанавливаетtp_hash = NULL). (M04 урок 02) - Я понимаю, почему
lru_cache(f)(unhashable_arg)raises TypeError — связка M02 (dict-key hashable) + M03 (lru_cache внутренний dict) + M04 (tp_hashslot). (M04 урок 06 cross-module synthesis) - Я могу провести параллель между mutable-default-trap (M03) и class-attribute-trap (M04): оба evaluated один раз, оба shared, симметричное решение через explicit per-call/per-instance init. (M04 урок 01 + M04 урок 06)
Что в M05 — Generators и iterator protocol
Module 05 (PyGenObject и iterator protocol) — продолжает M04 dunder-mapping для iteration:
__iter__→tp_iter(создание iterator из iterable)__next__→tp_iternext(следующее значение или StopIteration)PyGenObjectstruct и frame suspension (yield)- Generator expressions vs list comprehensions (memory M02 + control M05)
Тот же паттерн, что в M04: dunder method ↔ C slot ↔ runtime mechanism. Если вы поняли __init__ → tp_init, то __iter__ → tp_iter — это просто другой slot.
Cross-course context
Колончатый формат Arrow: от Python-объектов к packed data Оптимальный выбор типов данных в ClickHouseКлючевые выводы
- PyTypeObject — class на уровне C (M04 урок 01); Instance —
PyObjectс pointer наPyTypeObject. Instance__dict__это тот же PyDictObject, что обычный{}(M02 урок 03 → M04 урок 01). - Slot wrappers связывают dunder с C-slots (M04 урок 02).
__init__ → tp_init,__hash__ → tp_hash,__eq__ → tp_richcompare. Hashable invariant enforced черезtp_hash = NULLпри переопределении__eq__без__hash__(M02 урок 06 → M04 урок 02). - MRO через C3 (Barrett OOPSLA 1996; M04 урок 03). Гарантирует local precedence, monotonicity, EPG consistency. TypeError при невозможной linearization — feature, не bug (защита от arbitrary порядка).
- Descriptor protocol — fundamental lookup mechanism (M04 урок 04). Data descriptor (с
__set__) > instance__dict__> non-data descriptor.@property— data;@classmethod/@staticmethod— non-data. __slots__экономит память (M04 урок 05): WithSlots = 48 bytes vs WithoutSlots = 344 bytes.@dataclassэкономит boilerplate через codegenexec()source-строки. Связка@dataclass(slots=True)(Python 3.10+) — оба benefits.- Cross-module synthesis:
lru_cache(unhashable)raises TypeError — связка M02 (dict, hashable) + M03 (functools cache) + M04 (tp_hashslot).class C: items = []shares list = mutable-default-trap M03 на class level.
Module 04 завершён. Дальше — module exam (_exam.json): 13 questions (~77% applied/analytical), passThreshold 70. После него — Module 05 (Generators, iterator protocol).