Deep Engineering

MEASUREMENT

bench/attr-lookup/getattribute_equivalent.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/interview/python/descriptors
How to run it
У первых двух скриптов времени нет вовсе: сравниваются значения и
последовательности вызовов, а адреса объектов заменены на `0x...` ради
воспроизводимости прогона. Время появилось позже, вместе с двумя новыми
скриптами, — и про него ниже отдельный раздел.

## Время: что меряется и чему верить

Записи прогонов лежат в `runs/`, по файлу на скрипт и версию.

**Чередование обязательно.** Формы меряются не подряд, а кругами: в каждом
круге проходят все конфигурации, минимум для каждой берётся по кругам.
Причина не теоретическая. Пока замеры шли подряд, отношение между формами
гуляло в полтора раза от запуска к запуску — просадка машины в окне одной
формы целиком доставалась ей. Измерено на декораторах: 5,9 / 7,1 / 7,5 / 9,2
на четырёх запусках одного и того же замера.

**Кратности от малых величин не печатаются.** Частное, у которого в
знаменателе стоит величина порядка пятнадцати наносекунд, шумит вдвое и не
измеряет ничего: на двух подряд запусках `mro_cache.py` оно дало 7,3 и 10,7
при одинаковом наклоне. Поэтому оба скрипта печатают времена и разности, а
утверждения статьи строятся на наклоне, который устойчив.

**Что показал `mro_cache.py`.** При живом кеше время обращения не зависит от
длины `__mro__` — наклон по крайним точкам укладывается в сотые доли
наносекунды на уровень при разбросе самих значений в пару наносекунд. Стоит
сбивать кеш присваиванием в класс на каждой итерации, и появляется линейный
рост: 3,31 нс на уровень на 3.13.7 и 2,14 на 3.14.7 (наклон воспроизводится
между запусками, сами времена — только на нижней границе). Цена самого присваивания измеряется отдельной строкой и вычитается —
иначе замер показывал бы стоимость `setattr`, а не промаха кеша.

**Что показал `mro_override.py`, и это главное здесь.** Метакласс, который
переопределяет `mro()` — даже возвращая ровно `super().mro()`, то есть не меняя
в порядке ничего, — на 3.14.7 лишает свой класс кеша атрибутов типа. Обращение
к унаследованному атрибуту начинает расти вместе с длиной `__mro__`:
15,3 / 63,8 / 118,4 нс при длинах 2 / 21 / 51 против ровных 6,4 нс у обычного
типа. На длине 51 это в восемнадцать раз дороже. На 3.12.3 и 3.13.7 эффекта
нет: там все три конфигурации ровные.

Средняя строка таблицы — метакласс БЕЗ `mro()` — стоит в замере именно для
того, чтобы эффект нельзя было списать на метакласс вообще: она ровная на всех
трёх версиях.

**Эффект наследуется, и это его практическая цена.** Достаточно одного класса
с переопределённым `mro()` где-нибудь в основании иерархии: наследник
объявляется обычным `class Sub(Base)`, метакласс Python выводит из баз сам, и
кеша лишается тот, кто о метаклассе даже не знает. Замер: 118,7 нс при длине 51,
то есть ровно столько же, сколько у самого корня.

Со стороны исходников это видно так. Поле `tp_versions_used` появилось у типа
в 3.13 — голым, без комментария. В 3.14 рядом с ним встала константа
`_Py_ATTR_CACHE_UNUSED (30000)` и объяснение: это значение ставится, «if the
attribute cache is disabled for this type (e.g. due to custom MRO entries)».
Скрипт печатает оба объявления из заголовков того интерпретатора, на котором
запущен, поэтому проверить утверждение о C-уровне можно, не выходя из своей
системы.

В стандартной библиотеке `mro()` не переопределён нигде: `grep "def mro("` по
`Lib/` даёт ноль совпадений. Это приём стороннего кода.

**Что показал `bound_method.py`.** `obj.m` — взять атрибут и не вызывать —
стоит ДОРОЖЕ, чем `obj.m()` целиком: 24,6 против 16,3 нс на 3.13.7. Объект
связанного метода при слитной записи не создаётся вовсе, а при раздельной
обязан быть создан, потому что его кладут в переменную.

**Что показал `mro_shape.py`.** Времени там нет, только поведение, и оно
одинаково на 3.12.3, 3.13.7 и 3.14.7, кроме последнего пункта. Ромб
`class D(L, R)` даёт `['D', 'L', 'R', 'Base', 'object']`, и `D().m()`
возвращает `'R'`: общий предок уходит в хвост, а не «сначала вся ветка L».
Отказ C3 звучит дословно так: `Cannot create a consistent method resolution
order (MRO) for bases X, Y`. У встроенных типов `__mro__` короткий — два-четыре
элемента, — поэтому длины 21 и 51 в замерах кеша это модель, а не типичный код.
Все шесть видов членов класса оказываются дескрипторами: у типа каждого есть
`__get__`, и разделение на данных и не данных проходит по `__set__`.

Единственное расхождение по версиям — `functools.partial` в теле класса. На
3.13.7 он ещё обычный атрибут и предупреждает: «functools.partial will be a
method descriptor in future Python versions; wrap it in staticmethod() if you
want to preserve the old behavior». На 3.14.7 он уже метод-дескриптор, и тот же
код падает с `TypeError: two() takes 2 positional arguments but 3 were given`. Объект связанного метода при слитной
записи не создаётся вовсе, а при раздельной обязан быть создан, потому что его
кладут в переменную.

## Версии

**Вывод обоих скриптов одинаков на 3.11.15, 3.12.3, 3.13.7 и 3.14.7** —
различается только строка `PY`. Различий по версиям нет ни одного, и это
самостоятельный результат: правило приоритета не менялось, несмотря на то что
`_PyObject_GenericGetAttrWithDict` в C между 3.13 и 3.14 переписывался (см. ниже).

## Псевдокод и настоящий поиск

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры: где в поиске атрибута сидит протокол дескрипторов

Два сюжета, которых нет в bench/descriptors/: питоновский эквивалент object.__getattribute__, запущенный рядом с настоящим, и разница между поиском у экземпляра и поиском у класса — то есть дескриптор на метаклассе.

скрипт что показывает
getattribute_equivalent.py псевдокод из Doc/howto/descriptor.rst даёт те же ответы, что точка; три ветки приоритета; в псевдокоде нет __getattr__
metaclass_descriptor.py дескриптор метакласса виден у класса и не существует у экземпляра; дескриптор данных метакласса перекрывает __dict__ класса
mro_cache.py цена o.m при четырёх длинах __mro__ в двух режимах: кеш версий типа цел и сбивается на каждой итерации
bound_method.py во что обходится объект связанного метода и во что — super()
mro_override.py переопределённый mro() и кеш атрибутов типа: на 3.14 он выключается, на 3.12 и 3.13 нет
mro_shape.py форма MRO: ромб, отказ C3 дословным текстом, длины у настоящих типов, кто из членов класса дескриптор, functools.partial в теле класса
getattr_hook.py кто на самом деле зовёт __getattr__: делегирование в super() его НЕ отключает, прямой вызов обходит, поглощение исключения отключает
descriptor_kinds.py классификация дескриптора данных (__set__ или __delete__) против приоритета при чтении (нужен ещё __get__) — и строка, где они расходятся
mro_stdlib_scan.py разбором AST: сколько модулей стандартной библиотеки этой сборки переопределяют mro()

Запуск:

for v in 3.11 3.12 3.13 3.14; do
  echo "== $v"; python$v bench/attr-lookup/getattribute_equivalent.py
done

У первых двух скриптов времени нет вовсе: сравниваются значения и последовательности вызовов, а адреса объектов заменены на 0x... ради воспроизводимости прогона. Время появилось позже, вместе с двумя новыми скриптами, — и про него ниже отдельный раздел.

Время: что меряется и чему верить

Записи прогонов лежат в runs/, по файлу на скрипт и версию.

Чередование обязательно. Формы меряются не подряд, а кругами: в каждом круге проходят все конфигурации, минимум для каждой берётся по кругам. Причина не теоретическая. Пока замеры шли подряд, отношение между формами гуляло в полтора раза от запуска к запуску — просадка машины в окне одной формы целиком доставалась ей. Измерено на декораторах: 5,9 / 7,1 / 7,5 / 9,2 на четырёх запусках одного и того же замера.

Кратности от малых величин не печатаются. Частное, у которого в знаменателе стоит величина порядка пятнадцати наносекунд, шумит вдвое и не измеряет ничего: на двух подряд запусках mro_cache.py оно дало 7,3 и 10,7 при одинаковом наклоне. Поэтому оба скрипта печатают времена и разности, а утверждения статьи строятся на наклоне, который устойчив.

Что показал mro_cache.py. При живом кеше время обращения не зависит от длины __mro__ — наклон по крайним точкам укладывается в сотые доли наносекунды на уровень при разбросе самих значений в пару наносекунд. Стоит сбивать кеш присваиванием в класс на каждой итерации, и появляется линейный рост: 3,31 нс на уровень на 3.13.7 и 2,14 на 3.14.7 (наклон воспроизводится между запусками, сами времена — только на нижней границе). Цена самого присваивания измеряется отдельной строкой и вычитается — иначе замер показывал бы стоимость setattr, а не промаха кеша.

Что показал mro_override.py, и это главное здесь. Метакласс, который переопределяет mro() — даже возвращая ровно super().mro(), то есть не меняя в порядке ничего, — на 3.14.7 лишает свой класс кеша атрибутов типа. Обращение к унаследованному атрибуту начинает расти вместе с длиной __mro__: 15,3 / 63,8 / 118,4 нс при длинах 2 / 21 / 51 против ровных 6,4 нс у обычного типа. На длине 51 это в восемнадцать раз дороже. На 3.12.3 и 3.13.7 эффекта нет: там все три конфигурации ровные.

Средняя строка таблицы — метакласс БЕЗ mro() — стоит в замере именно для того, чтобы эффект нельзя было списать на метакласс вообще: она ровная на всех трёх версиях.

Эффект наследуется, и это его практическая цена. Достаточно одного класса с переопределённым mro() где-нибудь в основании иерархии: наследник объявляется обычным class Sub(Base), метакласс Python выводит из баз сам, и кеша лишается тот, кто о метаклассе даже не знает. Замер: 118,7 нс при длине 51, то есть ровно столько же, сколько у самого корня.

Со стороны исходников это видно так. Поле tp_versions_used появилось у типа в 3.13 — голым, без комментария. В 3.14 рядом с ним встала константа _Py_ATTR_CACHE_UNUSED (30000) и объяснение: это значение ставится, «if the attribute cache is disabled for this type (e.g. due to custom MRO entries)». Скрипт печатает оба объявления из заголовков того интерпретатора, на котором запущен, поэтому проверить утверждение о C-уровне можно, не выходя из своей системы.

В стандартной библиотеке mro() не переопределён нигде: grep "def mro(" по Lib/ даёт ноль совпадений. Это приём стороннего кода.

Что показал bound_method.py. obj.m — взять атрибут и не вызывать — стоит ДОРОЖЕ, чем obj.m() целиком: 24,6 против 16,3 нс на 3.13.7. Объект связанного метода при слитной записи не создаётся вовсе, а при раздельной обязан быть создан, потому что его кладут в переменную.

Что показал mro_shape.py. Времени там нет, только поведение, и оно одинаково на 3.12.3, 3.13.7 и 3.14.7, кроме последнего пункта. Ромб class D(L, R) даёт ['D', 'L', 'R', 'Base', 'object'], и D().m() возвращает 'R': общий предок уходит в хвост, а не «сначала вся ветка L». Отказ C3 звучит дословно так: Cannot create a consistent method resolution order (MRO) for bases X, Y. У встроенных типов __mro__ короткий — два-четыре элемента, — поэтому длины 21 и 51 в замерах кеша это модель, а не типичный код. Все шесть видов членов класса оказываются дескрипторами: у типа каждого есть __get__, и разделение на данных и не данных проходит по __set__.

Единственное расхождение по версиям — functools.partial в теле класса. На 3.13.7 он ещё обычный атрибут и предупреждает: «functools.partial will be a method descriptor in future Python versions; wrap it in staticmethod() if you want to preserve the old behavior». На 3.14.7 он уже метод-дескриптор, и тот же код падает с TypeError: two() takes 2 positional arguments but 3 were given. Объект связанного метода при слитной записи не создаётся вовсе, а при раздельной обязан быть создан, потому что его кладут в переменную.

Версии

Вывод обоих скриптов одинаков на 3.11.15, 3.12.3, 3.13.7 и 3.14.7 — различается только строка PY. Различий по версиям нет ни одного, и это самостоятельный результат: правило приоритета не менялось, несмотря на то что _PyObject_GenericGetAttrWithDict в C между 3.13 и 3.14 переписывался (см. ниже).

Псевдокод и настоящий поиск

$ python3.13 getattribute_equivalent.py
PY 3.13.7

1) в __dict__ экземпляра лежат все четыре имени: ['data', 'nondata', 'own', 'plain']

2) точка против псевдокода — одно и то же значение:
   c.data      = значение дескриптора данных
   псевдокод   = значение дескриптора данных   совпало: True
   c.nondata   = ЭКЗЕМПЛЯР nondata
   псевдокод   = ЭКЗЕМПЛЯР nondata   совпало: True
   c.plain     = ЭКЗЕМПЛЯР plain
   псевдокод   = ЭКЗЕМПЛЯР plain   совпало: True
   c.own       = только у экземпляра
   псевдокод   = только у экземпляра   совпало: True
   c.method    = <bound method C.method of <__main__.C object at 0x...>>
   псевдокод   = <bound method C.method of <__main__.C object at 0x...>>   совпало: True

3) какая ветка псевдокода сработала:
   data    -> ветка 1 (data descriptor): __dict__ экземпляра ПРОИГНОРИРОВАН
   nondata -> ветка 2 (instance variable): __dict__ экземпляра ПОБЕДИЛ
   plain   -> ветка 2 (instance variable)
   own     -> ветка 2 (instance variable)
   method  -> ветка 3 (non-data descriptor), функция сама дескриптор: True | __set__: False

4) вызовы __get__ НАШИХ дескрипторов (Data/NonData) — точка и псевдокод:
   data      точка=['Data.__get__']  псевдокод=['Data.__get__']
   nondata   точка=—  псевдокод=—
   plain     точка=—  псевдокод=—
   own       точка=—  псевдокод=—
   method    точка=—  псевдокод=—

5) точка зовёт __getattr__ после AttributeError: __getattr__(missing)
   псевдокод (и object.__getattribute__ напрямую) -> AttributeError: missing
   object.__getattribute__(w, 'missing') -> AttributeError: 'WithFallback' object has no attribute 'missing'

6) instance lookup против class lookup у одного и того же дескриптора:
   D().x -> obj=<__main__.D object at 0x...>, objtype=D
   D.x   -> obj=None, objtype=D
   вызовы (obj is None, objtype): [(False, 'D'), (True, 'D')]
   документация: object.__getattribute__ передаёт экземпляр,
   type.__getattribute__ подставляет None вместо экземпляра

Пункт 4 читается вместе с пунктом 3: __get__ дескриптора данных вызывается до того, как поиск вообще посмотрит в __dict__ экземпляра, и это видно по единственному вызову в строке data. У nondata вызова нет вовсе — до третьей ветки дело не дошло, потому что вторая уже вернула значение.

Пункт 5 — про то, чего в псевдокоде нет: ветки __getattr__. Она живёт этажом выше, в операторе точки; сам object.__getattribute__ её не знает.

Первоисточник

Doc/howto/descriptor.rst, строки 583–611 (тег v3.14.5) — код в getattribute_equivalent.py скопирован дословно, включая комментарии:

The logic for a dotted lookup is in object.__getattribute__. Here is a pure Python equivalent:

Перевод: «Логика поиска через точку находится в object.__getattribute__. Вот её эквивалент на чистом Python:»

def find_name_in_mro(cls, name, default):
    "Emulate _PyType_Lookup() in Objects/typeobject.c"
    for base in cls.__mro__:
        if name in vars(base):
            return vars(base)[name]
    return default

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = find_name_in_mro(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

Там же, строки 722–727, про отсутствующую ветку:

Note, there is no __getattr__ hook in the __getattribute__ code. That is why calling __getattribute__ directly or with super().__getattribute__ will bypass __getattr__ entirely. Instead, it is the dot operator and the getattr() function that are responsible for invoking __getattr__ whenever __getattribute__ raises an AttributeError.

Перевод: «Заметьте, в коде __getattribute__ нет крючка __getattr__. Именно поэтому прямой вызов __getattribute__ или вызов через super().__getattribute__ полностью обходит __getattr__. Вместо этого за вызов __getattr__ отвечают оператор точки и функция getattr() — всякий раз, когда __getattribute__ возбуждает AttributeError

И строки 806–829, итоговая формулировка:

The mechanism for descriptors is embedded in the __getattribute__ methods for object, type, and super. […] object.__getattribute__ and type.__getattribute__ make different calls to __get__. The first includes the instance and may include the class. The second puts in None for the instance and always includes the class.

Перевод: «Механизм дескрипторов встроен в методы __getattribute__ у object, type и super. […] object.__getattribute__ и type.__getattribute__ делают разные вызовы __get__. Первый передаёт экземпляр и может передать класс. Второй подставляет None вместо экземпляра и всегда передаёт класс.» Пункт 6 замера — прямая проверка этого абзаца.

C-исходник

Objects/object.c, функция _PyObject_GenericGetAttrWithDict. Порядок ветвей там тот же, но номера строк по версиям разные, и сам код между 3.13 и 3.14 переписан:

тег v3.13.7 тег v3.14.5
начало функции строка 1632 строка 1809
поиск по типу descr = _PyType_LookupRef(tp, name); (1659) _PyType_LookupStackRefAndVersion(tp, name, &cref.ref); (1841)
проверка на дескриптор данных if (f != NULL && PyDescr_IsData(descr)) (1664) то же (1847)
словарь экземпляра PyDict_GetItemRef(dict, name, &res) (1705) то же (1888)
дескриптор не-данных if (f != NULL) { res = f(descr, obj, ...) } (1720) то же (1903)
переменная класса if (descr != NULL) { res = descr; } (1729–1730) if (descr != NULL) { res = PyStackRef_AsPyObjectSteal(cref.ref); } (1912–1913)
AttributeError 1735–1741 1918–1924

Изменились работа со ссылками (PyStackRef) и способ обращения к кешу типов — порядок четырёх ветвей не изменился, и вывод обоих скриптов на 3.13 и 3.14 совпадает построчно.

Дескриптор на метаклассе

$ python3.13 metaclass_descriptor.py
PY 3.13.7

1) __set_name__ вызывается только у дескрипторов в теле СВОЕГО класса:
   __set_name__(на-метаклассе, owner=Meta, name=on_meta)
   __set_name__(на-классе, owner=C, name=on_class)
   для Loud на метаклассе owner=Meta, для Loud на классе owner=C

2) C.on_meta -> <на-метаклассе>
   на-метаклассе.__get__(obj=класс C, objtype=Meta)
   objtype=Meta: поиск шёл по type(C).__mro__, то есть по MRO метакласса

3) C().on_meta -> AttributeError: 'C' object has no attribute 'on_meta'
   вызовов __get__: нет
   поиск шёл по type(c).__mro__ = C.__mro__, а метакласса там нет:
   C.__mro__     : ['C', 'object']
   type(C).__mro__: ['Meta', 'type', 'object']

4) C().on_class -> <на-классе>
   на-классе.__get__(obj=экземпляр C, objtype=C)
   C.on_class   -> <на-классе>
   на-классе.__get__(obj=None, objtype=C)
   у экземпляра obj — сам экземпляр, у класса obj=None: это и есть разница вызовов

5) C.on_meta = 'новое' -> ["на-метаклассе.__set__('новое')"]
   значение НЕ попало в C.__dict__: False
   c.on_class = 'новое' -> ["на-классе.__set__('новое')"]
   значение НЕ попало в c.__dict__: False

6) дескриптор не-данных на метаклассе:
   E.nd -> <не-данных на метаклассе>
   после E.nd = 'перекрыто' -> перекрыто | в E.__dict__: True
   __dict__ КЛАССА перекрывает дескриптор не-данных метакласса,
   ровно как __dict__ экземпляра перекрывает дескриптор не-данных класса

7) имя `both` объявлено и на метаклассе, и на классе:
   F.both   -> <метакласс>
   F().both -> <класс>
   метакласс.__get__(obj=класс F, objtype=Meta2)
   класс.__get__(obj=экземпляр F, objtype=F)
   F.both ВЫИГРАЛ у F.__dict__['both']: Loud — дескриптор ДАННЫХ,
   а дескриптор данных на type(F) перекрывает словарь самого F —
   тем же правилом, каким дескриптор данных класса перекрывает vars(экземпляра).
   F.__dict__['both'] при этом никуда не делся: Loud

8) итог поиска:
   x.attr  -> ищется в type(x).__mro__ и в vars(x)
   C.attr  -> ищется в type(C).__mro__ (метаклассы) и в C.__mro__
   type(c) = C | type(C) = Meta | type(Meta) = type

Три вещи, ради которых написан файл.

Пункт 3 — самая частая ошибка ожидания: дескриптор на метаклассе у экземпляра не просто не срабатывает, его там нет. AttributeError, ноль вызовов __get__. Причина в одной строке псевдокода: objtype = type(obj). Для c это C, и в C.__mro__ метакласса нет.

Пункт 7 — то же правило приоритета, поднятое на этаж выше. F.both — это поиск атрибута у объекта F, тип которого Meta2; Loud на Meta2 — дескриптор данных, поэтому он перекрывает F.__dict__['both'] точно так же, как дескриптор данных класса перекрывает vars(экземпляра).

Пункт 6 — зеркальный случай: дескриптор не-данных на метаклассе перекрывается словарём класса. Пара 6/7 показывает, что правило приоритета одно, а «уровней» два, и оно применяется на обоих.

Первоисточник class lookup — Doc/howto/descriptor.rst, строки 776–786 (тег v3.14.5):

The logic for a dotted lookup such as A.x is in type.__getattribute__. The steps are similar to those for object.__getattribute__ but the instance dictionary lookup is replaced by a search through the class's method resolution order. If a descriptor is found, it is invoked with desc.__get__(None, A). The full C implementation can be found in type_getattro() and _PyType_Lookup() in Objects/typeobject.c.

Перевод: «Логика поиска через точку вида A.x находится в type.__getattribute__. Шаги похожи на шаги object.__getattribute__, но поиск в словаре экземпляра заменён поиском по порядку разрешения методов класса. Если дескриптор найден, он вызывается как desc.__get__(None, A). Полную реализацию на C можно найти в type_getattro() и _PyType_Lookup() в Objects/typeobject.c

Оговорка про версию

3.14 здесь — это 3.14.7. Ни одна строка вывода на 3.14 не отличается от 3.13.7, поэтому «поведение не изменилось» — это наблюдение с rc, а не с финального релиза. Номера строк в C-исходнике взяты с тегов v3.13.7 и v3.14.5, цитаты из Doc/ — с тега v3.14.5.

Script

165 lines
"""Псевдокод object.__getattribute__: где в поиске атрибута встроен протокол дескрипторов.

Один сюжет: питоновский эквивалент из Doc/howto/descriptor.rst выполняется
рядом с настоящим `object.__getattribute__` на одних и тех же объектах, и обе
ветви печатают одинаковый ответ. Так видно, что «дескриптор данных выигрывает у
__dict__ экземпляра» — не правило поверх поиска, а порядок строк ВНУТРИ поиска.

Функции find_name_in_mro и object_getattribute скопированы дословно из
Doc/howto/descriptor.rst (раздел «Invocation from an instance»).

Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import re
import sys


def noaddr(x):
    """Адреса объектов заменены, чтобы вывод был воспроизводим между запусками."""
    return re.sub(r"0x[0-9a-f]+", "0x...", str(x))


print("PY", sys.version.split()[0])
print()


# --- дословно из Doc/howto/descriptor.rst ----------------------------------
def find_name_in_mro(cls, name, default):
    "Emulate _PyType_Lookup() in Objects/typeobject.c"
    for base in cls.__mro__:
        if name in vars(base):
            return vars(base)[name]
    return default


def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = find_name_in_mro(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)
# --- конец дословной вставки -----------------------------------------------


trace = []


class Data:
    """Дескриптор ДАННЫХ: есть __set__, поэтому ветка 1."""

    def __get__(self, obj, objtype=None):
        trace.append("Data.__get__")
        return "значение дескриптора данных"

    def __set__(self, obj, value):
        trace.append("Data.__set__")


class NonData:
    """Дескриптор НЕ-данных: только __get__, поэтому ветка 3."""

    def __get__(self, obj, objtype=None):
        trace.append("NonData.__get__")
        return "значение дескриптора не-данных"


class C:
    data = Data()
    nondata = NonData()
    plain = "переменная класса"

    def method(self):
        return "метод"


c = C()
# Кладём в __dict__ экземпляра имена, совпадающие с именами на классе.
object.__setattr__(c, "__dict__", {})
c.__dict__["data"] = "ЭКЗЕМПЛЯР data"
c.__dict__["nondata"] = "ЭКЗЕМПЛЯР nondata"
c.__dict__["plain"] = "ЭКЗЕМПЛЯР plain"
c.__dict__["own"] = "только у экземпляра"

print("1) в __dict__ экземпляра лежат все четыре имени:", sorted(c.__dict__))
print()

print("2) точка против псевдокода — одно и то же значение:")
rows = []
for name in ("data", "nondata", "plain", "own", "method"):
    trace.clear()
    real = getattr(c, name)
    real_trace = list(trace)
    trace.clear()
    emu = object_getattribute(c, name)
    emu_trace = list(trace)
    rows.append((name, real, emu, real == emu, real_trace, emu_trace))
    print(f"   c.{name:9} = {noaddr(real)}")
    print(f"   псевдокод   = {noaddr(emu)}   совпало: {real == emu}")
print()

print("3) какая ветка псевдокода сработала:")
print("   data    -> ветка 1 (data descriptor): __dict__ экземпляра ПРОИГНОРИРОВАН")
print("   nondata -> ветка 2 (instance variable): __dict__ экземпляра ПОБЕДИЛ")
print("   plain   -> ветка 2 (instance variable)")
print("   own     -> ветка 2 (instance variable)")
print("   method  -> ветка 3 (non-data descriptor), функция сама дескриптор:",
      hasattr(type(C.__dict__['method']), '__get__'),
      "| __set__:", hasattr(type(C.__dict__['method']), '__set__'))
print()

print("4) вызовы __get__ НАШИХ дескрипторов (Data/NonData) — точка и псевдокод:")
for name, _, _, _, rt, et in rows:
    print(f"   {name:9} точка={rt or '—'}  псевдокод={et or '—'}")
print()

# --- 5. AttributeError и __getattr__ ---------------------------------------
# В псевдокоде нет ветки __getattr__ — и это записано в том же документе.
class WithFallback:
    def __getattr__(self, name):
        return f"__getattr__({name})"


w = WithFallback()
print("5) точка зовёт __getattr__ после AttributeError:", w.missing)
try:
    object_getattribute(w, "missing")
except AttributeError as e:
    print("   псевдокод (и object.__getattribute__ напрямую) -> AttributeError:", e)
try:
    object.__getattribute__(w, "missing")
except AttributeError as e:
    print("   object.__getattribute__(w, 'missing') -> AttributeError:", e)
print()

# --- 6. Класс ищет иначе: type.__getattribute__ ----------------------------
print("6) instance lookup против class lookup у одного и того же дескриптора:")
seen = []


class Loud:
    def __get__(self, obj, objtype=None):
        seen.append((obj is None, objtype.__name__ if objtype else None))
        return f"obj={obj!r}, objtype={objtype.__name__ if objtype else None}"


class D:
    x = Loud()


print("   D().x ->", noaddr(D().x))
print("   D.x   ->", noaddr(D.x))
print("   вызовы (obj is None, objtype):", seen)
print("   документация: object.__getattribute__ передаёт экземпляр,")
print("   type.__getattribute__ подставляет None вместо экземпляра")