Learning Platform
Глоссарий Troubleshooting
Урок 05.06 · 12 мин
Продвинутый
SummarySynthesisPyTypeObjectMRODescriptors__slots__dataclassCross-module

Ключевые выводы по ООП 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 lessonCPython sourceCross-module link
1PyTypeObject = Python class на уровне C-runtimeУрок 01Include/cpython/object.hM02 урок 03 (instance __dict__ это PyDictObject)
2Slot wrappers мапят dunder → tp_* slotsУрок 02Objects/typeobject.c slot_tp_*M02 урок 06 (__eq__/__hash__ invariant)
3MRO via C3 (Barrett OOPSLA 1996)Урок 03Objects/typeobject.c mro_implementationn/a (M04 internal)
4Descriptor protocol behind @property, @classmethodУрок 04Objects/descrobject.cn/a (M04 internal)
5__slots__ + @dataclass: memory + codegenУрок 05Lib/dataclasses.py _create_fnM02 урок 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__ tupleshared 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-implementationBehavior
@propertydata descriptorObjects/descrobject.c__get__ + __set__ + __delete__; precedence > instance __dict__
@classmethodnon-data descriptorObjects/funcobject.c__get__ returns method bound to class (cls вместо self)
@staticmethodnon-data descriptorObjects/funcobject.c__get__ returns underlying function as-is
@dataclassfunction (не descriptor)Lib/dataclasses.pycodegen via exec(); generates __init__/__repr__/__eq__
@dataclass(slots=True)functionLib/dataclasses.py _add_slotsdataclass + creates new class с __slots__
@functools.lru_cachefunction (returns wrapper)Modules/_functoolsmodule.cwrapper хранит 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__ для diamond D(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_hash slot). (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)
  • PyGenObject struct и 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

Ключевые выводы

  1. PyTypeObject — class на уровне C (M04 урок 01); Instance — PyObject с pointer на PyTypeObject. Instance __dict__ это тот же PyDictObject, что обычный {} (M02 урок 03 → M04 урок 01).
  2. 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).
  3. MRO через C3 (Barrett OOPSLA 1996; M04 урок 03). Гарантирует local precedence, monotonicity, EPG consistency. TypeError при невозможной linearization — feature, не bug (защита от arbitrary порядка).
  4. Descriptor protocol — fundamental lookup mechanism (M04 урок 04). Data descriptor (с __set__) > instance __dict__ > non-data descriptor. @property — data; @classmethod/@staticmethod — non-data.
  5. __slots__ экономит память (M04 урок 05): WithSlots = 48 bytes vs WithoutSlots = 344 bytes. @dataclass экономит boilerplate через codegen exec() source-строки. Связка @dataclass(slots=True) (Python 3.10+) — оба benefits.
  6. Cross-module synthesis: lru_cache(unhashable) raises TypeError — связка M02 (dict, hashable) + M03 (functools cache) + M04 (tp_hash slot). 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).

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. Cross-link: почему `@functools.lru_cache(maxsize=128)` требует, чтобы аргументы функции были hashable? Связь с M02 + M04

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6