Deep Engineering

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()