MEASUREMENT
bench/slots/layout.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/python/memory/slots
- How to run it
Записи прогонов — в `runs/`: `memory.txt` и `layout.txt` для основной сборки 3.13, остальные с суффиксом версии. У `access.py` запись одна: время между версиями не сравнивается (см. корневой `bench/README.md`), и второй файл провоцировал бы ровно такое сравнение. ## Почему память меряется партией, а не объектом `sys.getsizeof` — это `tp_basicsize + tp_itemsize*ob_size`: размер самого объекта, без того, на что он ссылается. Словарь экземпляра лежит по ссылке, и в это число не входит. Для `__slots__` результат получается не просто неточным, а бесполезным, причём по-разному в разных версиях: | `sys.getsizeof` | 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | | --- | --- | --- | --- | --- | | без `__slots__` | 56 | 48 | 48 | 48 | | со `__slots__` | 56 | 56 | 56 | 56 | | что из этого следует | «разницы нет» | «слоты дороже» | «слоты дороже» | «слоты дороже» | | на самом деле (замер) | слоты дешевле на 40 | на 32 | на 40 | на 40 | На 3.12 и дальше число **перевёрнуто**: по нему слоты выходят дороже на 8 байт. На 3.11 оно просто молчит — одинаковое в обоих случаях. Ни в одной версии оно не даёт правильного ответа, и ни в одной не даёт его с одной и той же ошибкой. Честный ответ даёт `tracemalloc` вокруг создания партии в 200 000 экземпляров: он считает все запрошенные аллокации. Список-держатель отводится целиком **до** старта `tracemalloc`, поэтому в измерение не попадает и вычитать из суммы нечего — проверка 3 в блоке 5 `memory.py` меряет партию из уже созданного объекта и обязана дать ноль. Первый черновик скрипта вычитал «8 байт на ячейку списка»: так методика описана в плане статьи, и там она верна для варианта с `append`, где список растёт под измерением. Здесь он не растёт. Проверка и существует затем, чтобы такие расхождения не оставались на слово. ## Что измерено Класс с тремя атрибутами, байт на экземпляр: | | 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | | --- | --- | --- | --- | --- | | без `__slots__` | 96 | 88 | 96 | 96 | | со `__slots__` | 56 | 56 | 56 | 56 | | без слотов, после `vars(obj)` | 160 | 152 | 160 | 160 | | подкласс класса со слотами, без своих | 96 | 88 | **72** | 96 | Экономия — 40 байт (42 %) на трёх из четырёх сборок и 32 байта (36 %) на 3.12. **«Экономия выросла в 3.13» — формулировка, которая держится только на сравнении с 3.12.** Ряд не монотонный: 3.11 уже давал 96 байт, 3.12 сделал обычный экземпляр на 8 байт дешевле, 3.13 их вернул. В плане статьи это было записано как рост в 3.13, потому что 3.11 в таблице не было. ## Подкласс без своих слотов: 72 байта на 3.13 и 96 на 3.14 Самая известная ловушка темы ведёт себя в разных версиях по-разному, и разница не мелкая. Причина читается по одному флагу, `layout.py` печатает его: | | 3.13.13 | 3.14.7 | | --- | --- | --- | | обычный класс, `INLINE_VALUES` | да | да | | подкласс без своих слотов, `INLINE_VALUES` | **нет** | **да** | | байт на экземпляр | 72 | 96 | Флага `Py_TPFLAGS_INLINE_VALUES` до 3.13 не существует вовсе — в `object.h` версий 3.11 и 3.12 бит 2 не определён, и спрашивать его там бессмысленно. Первый черновик `layout.py` спрашивал и печатал «False», то есть выдавал за измеренный факт ответ про несуществующий флаг. Почему подкласс его не получает на 3.13 — написано в заголовке этой машины, `/usr/include/python3.13/internal/pycore_object.h`:
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
Замеры: __slots__ — сколько экономит и чем это правильно мерить
Тема, где почти каждый привычный индикатор отвечает неправильно, причём по-разному. Здесь три скрипта, и каждый ловит свой промах.
| скрипт | что показывает |
|---|---|
memory.py |
честная память на экземпляр через tracemalloc на партии; рядом — sys.getsizeof, который даёт ответ с ОБРАТНЫМ знаком; цена __weakref__, которую не видит basicsize; четыре версии подряд |
layout.py |
из чего эти байты складываются: флаги типа, basicsize, dictoffset, weaklistoffset, плюс дословные фрагменты заголовков CPython этой машины |
access.py |
ускоряет ли __slots__ доступ к атрибуту (нет) и во что обходится обращение к vars(obj) (в запись — почти вчетверо) |
Запуск:
for v in 3.11 3.12 3.13 3.14; do
echo "== $v"; python$v bench/slots/memory.py
done
for v in 3.11 3.12 3.13 3.14; do
echo "== $v"; python$v bench/slots/layout.py
done
python3.13 bench/slots/access.py
Записи прогонов — в runs/: memory.txt и layout.txt для основной сборки
3.13, остальные с суффиксом версии. У access.py запись одна: время между
версиями не сравнивается (см. корневой bench/README.md), и второй файл
провоцировал бы ровно такое сравнение.
Почему память меряется партией, а не объектом
sys.getsizeof — это tp_basicsize + tp_itemsize*ob_size: размер самого
объекта, без того, на что он ссылается. Словарь экземпляра лежит по ссылке, и
в это число не входит. Для __slots__ результат получается не просто
неточным, а бесполезным, причём по-разному в разных версиях:
sys.getsizeof |
3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 |
|---|---|---|---|---|
без __slots__ |
56 | 48 | 48 | 48 |
со __slots__ |
56 | 56 | 56 | 56 |
| что из этого следует | «разницы нет» | «слоты дороже» | «слоты дороже» | «слоты дороже» |
| на самом деле (замер) | слоты дешевле на 40 | на 32 | на 40 | на 40 |
На 3.12 и дальше число перевёрнуто: по нему слоты выходят дороже на 8 байт. На 3.11 оно просто молчит — одинаковое в обоих случаях. Ни в одной версии оно не даёт правильного ответа, и ни в одной не даёт его с одной и той же ошибкой.
Честный ответ даёт tracemalloc вокруг создания партии в 200 000 экземпляров:
он считает все запрошенные аллокации. Список-держатель отводится целиком
до старта tracemalloc, поэтому в измерение не попадает и вычитать из
суммы нечего — проверка 3 в блоке 5 memory.py меряет партию из уже
созданного объекта и обязана дать ноль.
Первый черновик скрипта вычитал «8 байт на ячейку списка»: так методика
описана в плане статьи, и там она верна для варианта с append, где список
растёт под измерением. Здесь он не растёт. Проверка и существует затем, чтобы
такие расхождения не оставались на слово.
Что измерено
Класс с тремя атрибутами, байт на экземпляр:
| 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | |
|---|---|---|---|---|
без __slots__ |
96 | 88 | 96 | 96 |
со __slots__ |
56 | 56 | 56 | 56 |
без слотов, после vars(obj) |
160 | 152 | 160 | 160 |
| подкласс класса со слотами, без своих | 96 | 88 | 72 | 96 |
Экономия — 40 байт (42 %) на трёх из четырёх сборок и 32 байта (36 %) на 3.12.
«Экономия выросла в 3.13» — формулировка, которая держится только на сравнении с 3.12. Ряд не монотонный: 3.11 уже давал 96 байт, 3.12 сделал обычный экземпляр на 8 байт дешевле, 3.13 их вернул. В плане статьи это было записано как рост в 3.13, потому что 3.11 в таблице не было.
Подкласс без своих слотов: 72 байта на 3.13 и 96 на 3.14
Самая известная ловушка темы ведёт себя в разных версиях по-разному, и разница
не мелкая. Причина читается по одному флагу, layout.py печатает его:
| 3.13.13 | 3.14.7 | |
|---|---|---|
обычный класс, INLINE_VALUES |
да | да |
подкласс без своих слотов, INLINE_VALUES |
нет | да |
| байт на экземпляр | 72 | 96 |
Флага Py_TPFLAGS_INLINE_VALUES до 3.13 не существует вовсе — в object.h
версий 3.11 и 3.12 бит 2 не определён, и спрашивать его там бессмысленно.
Первый черновик layout.py спрашивал и печатал «False», то есть выдавал за
измеренный факт ответ про несуществующий флаг.
Почему подкласс его не получает на 3.13 — написано в заголовке этой машины,
/usr/include/python3.13/internal/pycore_object.h:
static inline PyDictValues *
_PyObject_InlineValues(PyObject *obj)
{
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES);
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT);
assert(Py_TYPE(obj)->tp_basicsize == sizeof(PyObject));
return (PyDictValues *)((char *)obj + sizeof(PyObject));
}
Значения кладутся ровно по адресу obj + sizeof(PyObject), а у подкласса там
уже лежат слоты базы. Массив значений не заводится вовсе, и экземпляр выходит
дешевле обычного: указатель на словарь остаётся пустым, пока в словарь не
напишут. На 3.14 флаг у такого подкласса стоит, и цена возвращается к 96.
Заголовков 3.14 в этой машине нет. Поэтому про неё утверждается только то, что
прочитано флагом в рантайме и измерено; на её исходник здесь не ссылаются, и
layout.py говорит об этом в выводе.
Для сравнения — как то же место выглядело в 3.12,
/usr/include/python3.12/internal/pycore_object.h:
typedef union {
PyObject *dict;
/* Use a char* to generate a warning if directly assigning a PyDictValues */
char *values;
} PyDictOrValues;
Одно слово на «или словарь, или значения» — против отдельного указателя на словарь и отдельных inline values в 3.13.
__weakref__: тот же промах на другой величине
Слабая ссылка на экземпляр со слотами не создаётся, пока '__weakref__' не
назван в слотах явно. Сколько это стоит и что показывает basicsize:
| 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | |
|---|---|---|---|---|
| со слотами, байт | 56 | 56 | 56 | 56 |
и с '__weakref__', байт |
64 | 72 | 72 | 72 |
| цена | 8 | 16 | 16 | 16 |
разница в basicsize |
8 | 0 | 0 | 0 |
weaklistoffset |
40 | −32 | −32 | −32 |
С 3.12 цена вдвое выше, а basicsize перестал её показывать: ссылка уехала в
предзаголовок, до начала объекта. Отрицательный weaklistoffset — то место,
где это видно.
Время: что меряется и почему повторяется
access.py проверяет второе ходовое утверждение темы — «слоты ускоряют
доступ». На 3.13.13, лучшее из 7 раундов:
| со слотами | без слотов | после vars(obj) |
|
|---|---|---|---|
чтение o.a |
8,78 | 8,51 | 8,53 |
запись o.a = 5 |
8,33 | 8,36 | 32,37 |
Разница между слотами и inline values — около трёх процентов, и при чтении она
направлена не в пользу слотов. Утверждение «__slots__ ускоряет доступ»
замером не подтверждается. Зато подтверждается другое: обращение к
vars(obj) удорожает запись почти вчетверо (+287 %), а чтение не трогает.
Кратность здесь устойчивее числа. В одну сессию то же место дало +136 %, в другую — +296, +298 и +302 % в трёх прогонах подряд. Вывод от этого не зависит, и в статье он подан кратностью, а не числом.
Числа сравниваются только между собой: они получены одним процессом на одной
сборке. Между версиями время не сравнивается вовсе (корневой bench/README.md).
Операция повторяется 50 раз внутри одного витка timeit, и это не
украшение. Проверка 2 печатает оба режима рядом. Без амортизации: pass
12,36 нс, o.a со слотами 19,44, без слотов 19,56 — то есть на сигнал
приходится 36 % числа, остальное цикл, и «разница» между двумя последними
строками лежит внутри его шума. После амортизации дно метода — 1,43 нс против
измеряемых 8, и числа стали означать то, что должны.
Чем это меряли
Python 3.11.15 и 3.12.3 (GCC 13.3.0), 3.13.13 (GCC 13.3.0), 3.14.7 (Clang 22.1.3); Intel Xeon 2,80 ГГц, 2 vCPU. Байты между версиями сравнивать можно — это раскладка объекта. Время нельзя: у сборок разные компиляторы и разные флаги.
Script
315 lines"""Почему экономия от `__slots__` разная в разных версиях — и где это записано.
ЗАЧЕМ ЭТОТ СКРИПТ. `memory.py` показывает, СКОЛЬКО байт уходит на экземпляр, и
числа там разъезжаются по версиям так, что объяснить их «языком» нельзя:
обычный экземпляр стоит 96 Б на 3.11, 88 Б на 3.12 и снова 96 Б на 3.13 и 3.14.
Здесь печатается то, из чего эти числа складываются, — раскладка типа и флаги,
по которым интерпретатор решает, где держать атрибуты.
ГЛАВНОЕ, ЧТО ЗДЕСЬ ВИДНО. Подкласс класса со слотами, у которого своих
`__slots__` нет, — самая известная ловушка темы, — стоит РАЗНОГО в разных
версиях, и разница не мелкая: 96 / 88 / 72 / 96 байт. На 3.13 он оказывается
заметно дешевле обычного класса, а на 3.14 снова ровно как обычный. Причина
читается по одному флагу, и она же объясняет, почему на 3.13 так вышло.
ЧТО ЗДЕСЬ ПЕРВОИСТОЧНИК. Заголовки CPython, установленные в этой машине:
/usr/include/python3.12/internal/pycore_object.h
/usr/include/python3.13/internal/pycore_object.h
/usr/include/python3.{11,12,13}/object.h
Скрипт их ЧИТАЕТ и печатает нужные строки дословно, а не пересказывает. Для
3.14 заголовков в этой машине нет, и это сказано в выводе прямо: утверждения
про 3.14 опираются на флаг, прочитанный в рантайме, и на замер, но не на её
исходник.
ЗАПУСК:
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v bench/slots/layout.py; done
"""
import os
import sys
# Значения битов — из object.h соответствующей версии. INLINE_VALUES появился
# в 3.13; в 3.11 и 3.12 бит 2 не определён ВООБЩЕ, поэтому спрашивать его там
# бессмысленно, и скрипт этого не делает. Первый черновик спрашивал и печатал
# «False» — то есть выдавал за измеренный факт ответ про несуществующий флаг.
TPFLAGS_INLINE_VALUES = 1 << 2 # только 3.13+
TPFLAGS_MANAGED_WEAKREF = 1 << 3 # только 3.12+
TPFLAGS_MANAGED_DICT = 1 << 4 # 3.11+
VER = sys.version_info[:2]
HAS_INLINE_FLAG = VER >= (3, 13)
HAS_MANAGED_WEAKREF_FLAG = VER >= (3, 12)
class Plain:
def __init__(self):
self.a = self.b = self.c = 1
class Slotted:
__slots__ = ("a", "b", "c")
def __init__(self):
self.a = self.b = self.c = 1
class HeirWithoutSlots(Slotted):
"""Подкласс класса со слотами, у которого своих слотов нет."""
class HeirWithSlots(Slotted):
__slots__ = ()
"""Подкласс, который слоты сохранил: пустой кортеж — это не то же, что ничего."""
def rule(title):
print(f"\n=== {title} ===\n")
def describe(name, cls):
f = cls.__flags__
parts = [
f"basicsize={cls.__basicsize__:3d}",
f"dictoffset={cls.__dictoffset__:>3}",
f"MANAGED_DICT={'да ' if f & TPFLAGS_MANAGED_DICT else 'нет'}",
]
if HAS_INLINE_FLAG:
parts.append(f"INLINE_VALUES={'да ' if f & TPFLAGS_INLINE_VALUES else 'нет'}")
else:
parts.append("INLINE_VALUES=нет такого флага")
print(f" {name:<32} {' '.join(parts)}")
def block_types():
rule("БЛОК 1. РАСКЛАДКА ТИПОВ")
print(f" sizeof(PyObject) на этой сборке: {object().__sizeof__()} Б")
print()
describe("обычный класс", Plain)
describe("класс со __slots__", Slotted)
describe("подкласс без своих __slots__", HeirWithoutSlots)
describe("подкласс со __slots__ = ()", HeirWithSlots)
print()
print(" dictoffset = 0 означает, что словаря у экземпляра нет вовсе.")
print(" Отрицательный dictoffset — словарь лежит В ПРЕДЗАГОЛОВКЕ, до")
print(" начала объекта, а не после его полей.")
print()
print(" Обратите внимание на четвёртую строку: `__slots__ = ()` — это НЕ")
print(" «слотов нет», это «своих слотов нет, и словарь заводить не надо».")
print(" Разница между третьей и четвёртой строкой — вся ловушка целиком.")
def block_why():
rule("БЛОК 2. ПОЧЕМУ ПОДКЛАСС БЕЗ СВОИХ СЛОТОВ СТОИТ РАЗНОГО")
if not HAS_INLINE_FLAG:
print(f" На {sys.version.split()[0]} флага INLINE_VALUES не существует:")
print(" значения атрибутов не лежат внутри объекта, и вопрос,")
print(" разобранный ниже на 3.13 и 3.14, здесь не возникает.")
print()
print(" Что здесь есть: MANAGED_DICT у обычного класса и у подкласса")
print(" без своих слотов — и одинаковая цена у обоих (см. memory.py).")
return
plain_inline = bool(Plain.__flags__ & TPFLAGS_INLINE_VALUES)
heir_inline = bool(HeirWithoutSlots.__flags__ & TPFLAGS_INLINE_VALUES)
print(f" обычный класс, INLINE_VALUES: {'да' if plain_inline else 'нет'}")
print(f" подкласс без своих слотов, INLINE_VALUES: {'да' if heir_inline else 'нет'}")
print()
if plain_inline and not heir_inline:
print(" Вот и весь ответ. У подкласса basicsize больше sizeof(PyObject)")
print(" (в нём лежат слоты базы), а inline values в 3.13 кладутся ровно")
print(" по адресу obj + sizeof(PyObject) — места под них нет. Массив")
print(" значений не заводится вовсе, и экземпляр выходит ДЕШЕВЛЕ")
print(" обычного: словарь остаётся пустым указателем, пока в него не")
print(" напишут.")
elif plain_inline and heir_inline:
print(" Здесь ограничение снято: inline values достаются и подклассу,")
print(" у которого basicsize больше sizeof(PyObject). Массив значений")
print(" заводится, и подкласс снова стоит столько же, сколько обычный")
print(" класс, — дешевизна, которая была на 3.13, исчезла.")
else:
print(" Сочетание, которого не ожидалось при написании скрипта.")
print(" Смотреть исходник этой версии.")
def quote_header(path, needle, before=0, after=6, limit=1):
"""Печатает строки заголовка CPython дословно — это и есть первоисточник."""
if not os.path.exists(path):
return False
with open(path, encoding="utf-8", errors="replace") as fh:
lines = fh.readlines()
shown = 0
for i, line in enumerate(lines):
if needle in line:
lo, hi = max(0, i - before), min(len(lines), i + after)
print(f" {path}:{lo + 1}–{hi}")
for j in range(lo, hi):
print(f" {lines[j].rstrip()}")
shown += 1
if shown >= limit:
return True
return False
def block_sources():
rule("БЛОК 3. ЧТО НАПИСАНО В ЗАГОЛОВКАХ ЭТОЙ МАШИНЫ")
print(" 3.12 — одно слово на «или словарь, или значения»:")
print()
ok12 = quote_header("/usr/include/python3.12/internal/pycore_object.h",
"} PyDictOrValues;", before=4, after=1)
if not ok12:
print(" заголовков 3.12 в этой машине нет")
print()
print(" 3.13 — указатель на словарь отдельно, значения отдельно и ВНУТРИ объекта:")
print()
ok13a = quote_header("/usr/include/python3.13/internal/pycore_object.h",
"} PyManagedDictPointer;", before=3, after=1)
print()
ok13b = quote_header("/usr/include/python3.13/internal/pycore_object.h",
"_PyObject_InlineValues(PyObject *obj)", before=1, after=7)
if not (ok13a and ok13b):
print(" заголовков 3.13 в этой машине нет")
print()
print(" Третья строка последнего фрагмента — assert про basicsize — и есть")
print(" причина, по которой подкласс со слотами базы не получает inline")
print(" values на 3.13.")
print()
have_314 = os.path.exists("/root/.local/include/python3.14/internal/pycore_object.h")
print(f" Заголовки 3.14 в этой машине: {'есть' if have_314 else 'НЕТ'}.")
if not have_314:
print(" Поэтому про 3.14 здесь утверждается только то, что прочитано")
print(" флагом в рантайме и измерено в memory.py. Её исходник не читался,")
print(" и ссылаться на него нельзя.")
def block_weakref():
rule("БЛОК 4. WEAKREF: ТРЕТЬЕ МЕСТО, КУДА УХОДЯТ БАЙТЫ")
import weakref
print(" Со слотами ссылка на объект по слабой ссылке не создаётся, пока")
print(" '__weakref__' не назван в слотах явно:")
print()
try:
weakref.ref(Slotted())
print(" weakref.ref(экземпляр со слотами) прошло")
except TypeError as exc:
print(f" weakref.ref(экземпляр со слотами) TypeError: {exc}")
class SlottedWeak:
__slots__ = ("a", "b", "c", "__weakref__")
weakref.ref(SlottedWeak())
print(" со '__weakref__' в слотах прошло")
print()
if HAS_MANAGED_WEAKREF_FLAG:
print(f" MANAGED_WEAKREF у обычного класса "
f"{'да' if Plain.__flags__ & TPFLAGS_MANAGED_WEAKREF else 'нет'}")
print(f" MANAGED_WEAKREF у класса со слотами {'да' if Slotted.__flags__ & TPFLAGS_MANAGED_WEAKREF else 'нет'}")
print(f" basicsize со слотами без __weakref__ {Slotted.__basicsize__}")
print(f" basicsize со слотами и __weakref__ {SlottedWeak.__basicsize__}")
def block_cached_property():
rule("БЛОК 6. cached_property И СЛОТЫ")
import functools
class WithSlots:
__slots__ = ("a",)
def __init__(self):
self.a = 1
@functools.cached_property
def doubled(self):
return self.a * 2
class WithoutSlots:
def __init__(self):
self.a = 1
@functools.cached_property
def doubled(self):
return self.a * 2
print(" cached_property кладёт вычисленное в __dict__ экземпляра.")
print(" Со слотами класть некуда:")
print()
try:
print(f" без слотов, obj.doubled {WithoutSlots().doubled}")
except Exception as exc: # noqa: BLE001
print(f" без слотов, obj.doubled {type(exc).__name__}: {exc}")
try:
print(f" со слотами, obj.doubled {WithSlots().doubled}")
except Exception as exc: # noqa: BLE001
print(f" со слотами, obj.doubled {type(exc).__name__}: {exc}")
print()
print(" Это следствие приоритета, а не отдельный запрет: cached_property —")
print(" дескриптор НЕ данных, он рассчитывает, что словарь экземпляра его")
print(" перекроет при следующем обращении. Без словаря механизм не работает.")
def block_conflict():
rule("БЛОК 5. КОНФЛИКТ РАСКЛАДКИ ПРИ МНОЖЕСТВЕННОМ НАСЛЕДОВАНИИ")
class A:
__slots__ = ("x",)
class B:
__slots__ = ("y",)
print(" Два родителя, у каждого свой непустой __slots__:")
print()
try:
type("C", (A, B), {"__slots__": ()})
print(" class C(A, B) прошло — неожиданно")
except TypeError as exc:
print(f" class C(A, B) TypeError: {exc}")
print()
print(" А если у одного из родителей слоты пустые, конфликта нет:")
class Empty:
__slots__ = ()
try:
type("D", (A, Empty), {"__slots__": ()})
print(" class D(A, Empty) прошло")
except TypeError as exc:
print(f" class D(A, Empty) TypeError: {exc}")
print()
print(" Причина одна и та же: слот — это фиксированное смещение в")
print(" экземпляре, и два родителя с непустыми слотами требуют")
print(" несовместимых раскладок. Пустой кортеж смещений не требует.")
def main():
print("РАСКЛАДКА ЭКЗЕМПЛЯРА: ФЛАГИ, СМЕЩЕНИЯ И ЗАГОЛОВКИ")
print()
print(f"Python {sys.version.split()[0]}")
build = sys.version.split("[", 1)[1].rstrip("]") if "[" in sys.version else "?"
print(f"Сборка: {build}")
print()
print("Времени здесь нет: печатаются флаги, смещения и строки заголовков.")
block_types()
block_why()
block_sources()
block_weakref()
block_conflict()
block_cached_property()
print()
print("Все строки выше напечатаны этой программой; фрагменты заголовков")
print("прочитаны из файлов этой машины и не переписывались.")
if __name__ == "__main__":
main()