MEASUREMENT
bench/attr-lookup/metaclass_descriptor.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 withsuper().__getattribute__will bypass__getattr__entirely. Instead, it is the dot operator and thegetattr()function that are responsible for invoking__getattr__whenever__getattribute__raises anAttributeError.
Перевод: «Заметьте, в коде __getattribute__ нет крючка __getattr__. Именно
поэтому прямой вызов __getattribute__ или вызов через
super().__getattribute__ полностью обходит __getattr__. Вместо этого за
вызов __getattr__ отвечают оператор точки и функция getattr() — всякий раз,
когда __getattribute__ возбуждает AttributeError.»
И строки 806–829, итоговая формулировка:
The mechanism for descriptors is embedded in the
__getattribute__methods forobject,type, andsuper. […]object.__getattribute__andtype.__getattribute__make different calls to__get__. The first includes the instance and may include the class. The second puts inNonefor 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.xis intype.__getattribute__. The steps are similar to those forobject.__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 withdesc.__get__(None, A). The full C implementation can be found intype_getattro()and_PyType_Lookup()inObjects/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
152 lines"""Дескриптор на метаклассе: почему он виден у класса и невидим у экземпляра.
Один сюжет: дескриптор ищется в MRO ТИПА того объекта, у которого берут
атрибут. Для экземпляра тип — это класс; для класса тип — это метакласс.
Отсюда несимметричность, которую обычно объясняют «магией метаклассов»:
дескриптор на метаклассе работает для `C.attr` и не существует для `C().attr`.
Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import sys
print("PY", sys.version.split()[0])
print()
calls = []
class Loud:
def __init__(self, label):
self.label = label
def __set_name__(self, owner, name):
calls.append(f"__set_name__({self.label}, owner={owner.__name__}, name={name})")
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
who = "None"
elif isinstance(obj, type):
who = f"класс {obj.__name__}"
else:
who = f"экземпляр {type(obj).__name__}"
calls.append(
f"{self.label}.__get__(obj={who}, "
f"objtype={objtype.__name__ if objtype else None})"
)
return f"<{self.label}>"
def __set__(self, obj, value):
calls.append(f"{self.label}.__set__({value!r})")
class Meta(type):
on_meta = Loud("на-метаклассе")
class C(metaclass=Meta):
on_class = Loud("на-классе")
print("1) __set_name__ вызывается только у дескрипторов в теле СВОЕГО класса:")
for line in calls:
print(" ", line)
print(" для Loud на метаклассе owner=Meta, для Loud на классе owner=C")
print()
c = C()
# --- 2. Атрибут метакласса виден у класса ---------------------------------
calls.clear()
value = C.on_meta
print("2) C.on_meta ->", value)
for line in calls:
print(" ", line)
print(" objtype=Meta: поиск шёл по type(C).__mro__, то есть по MRO метакласса")
print()
# --- 3. У экземпляра его нет вовсе ----------------------------------------
calls.clear()
try:
print("3) C().on_meta ->", c.on_meta)
except AttributeError as e:
print("3) C().on_meta ->", type(e).__name__ + ":", e)
print(" вызовов __get__:", calls or "нет")
print(" поиск шёл по type(c).__mro__ = C.__mro__, а метакласса там нет:")
print(" C.__mro__ :", [t.__name__ for t in C.__mro__])
print(" type(C).__mro__:", [t.__name__ for t in type(C).__mro__])
print()
# --- 4. Зеркальный случай: дескриптор на классе -----------------------------
calls.clear()
print("4) C().on_class ->", c.on_class)
for line in calls:
print(" ", line)
calls.clear()
print(" C.on_class ->", C.on_class)
for line in calls:
print(" ", line)
print(" у экземпляра obj — сам экземпляр, у класса obj=None: это и есть разница вызовов")
print()
# --- 5. Запись: __set__ метакласса перехватывает присваивание классу -------
calls.clear()
C.on_meta = "новое"
print("5) C.on_meta = 'новое' ->", calls)
print(" значение НЕ попало в C.__dict__:", "on_meta" in C.__dict__)
calls.clear()
c.on_class = "новое"
print(" c.on_class = 'новое' ->", calls)
print(" значение НЕ попало в c.__dict__:", "on_class" in vars(c))
print()
# --- 6. Что случится без __set__ (дескриптор не-данных) --------------------
class NonDataMeta(type):
class _ND:
def __get__(self, obj, objtype=None):
return "<не-данных на метаклассе>"
nd = _ND()
class E(metaclass=NonDataMeta):
pass
print("6) дескриптор не-данных на метаклассе:")
print(" E.nd ->", E.nd)
E.nd = "перекрыто"
print(" после E.nd = 'перекрыто' ->", E.nd, "| в E.__dict__:", "nd" in E.__dict__)
print(" __dict__ КЛАССА перекрывает дескриптор не-данных метакласса,")
print(" ровно как __dict__ экземпляра перекрывает дескриптор не-данных класса")
print()
# --- 7. Одно имя на обоих уровнях -----------------------------------------
class Meta2(type):
both = Loud("метакласс")
class F(metaclass=Meta2):
both = Loud("класс")
f = F()
calls.clear()
print("7) имя `both` объявлено и на метаклассе, и на классе:")
print(" F.both ->", F.both)
print(" F().both ->", f.both)
for line in calls:
print(" ", line)
print(" F.both ВЫИГРАЛ у F.__dict__['both']: Loud — дескриптор ДАННЫХ,")
print(" а дескриптор данных на type(F) перекрывает словарь самого F —")
print(" тем же правилом, каким дескриптор данных класса перекрывает vars(экземпляра).")
print(" F.__dict__['both'] при этом никуда не делся:", type(F.__dict__["both"]).__name__)
print()
# --- 8. Порядок из справочника целиком ------------------------------------
print("8) итог поиска:")
print(" x.attr -> ищется в type(x).__mro__ и в vars(x)")
print(" C.attr -> ищется в type(C).__mro__ (метаклассы) и в C.__mro__")
print(" type(c) =", type(c).__name__, "| type(C) =", type(C).__name__,
"| type(Meta) =", type(Meta).__name__)