Deep Engineering

MEASUREMENT

bench/imports/basics.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/modules-and-imports
How to run it
python3.11 bench/imports/basics.py
python3.12 bench/imports/basics.py
python3.13 bench/imports/basics.py
python3.14 bench/imports/basics.py

python3.13 bench/imports/cost.py
python3.14 bench/imports/cost.py

python3.11 bench/imports/practice.py
python3.12 bench/imports/practice.py
python3.13 bench/imports/practice.py
python3.14 bench/imports/practice.py

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

Замеры для урока «Модули и импорт»

Скрипт Что делает
basics.py наблюдения без времени: модуль выполняется один раз, from m import x копирует ссылку, __name__ при запуске и при импорте, циклический импорт, свой random.py рядом со скриптом, пакет и __init__.py; раздел 7 — встроенный модуль, пакет без __init__.py, границы кеша sys.modules, полный пример python -m пакет.модуль
cost.py цена повторного импорта (import json внутри функции) и первого импорта json, decimal, asyncio в свежем интерпретаторе
practice.py источник ответов двух практических задач урока
python3.11 bench/imports/basics.py
python3.12 bench/imports/basics.py
python3.13 bench/imports/basics.py
python3.14 bench/imports/basics.py

python3.13 bench/imports/cost.py
python3.14 bench/imports/cost.py

python3.11 bench/imports/practice.py
python3.12 bench/imports/practice.py
python3.13 bench/imports/practice.py
python3.14 bench/imports/practice.py

Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом версии (3.11.15, 3.12.3, 3.14.7). Каждый сюжет basics.py запускается в отдельном интерпретаторе во временной папке; путь к ней в выводе заменён на <папка>. Все прогоны — Linux x86_64, одна машина (2 ядра); сверено 14.10.2026. Числа cost.py — наблюдения этой конфигурации: на другой машине и версии меняются и числа, и их отношения, — мерить у себя. Раздел 7 basics.py добавлен по внешнему аудиту урока 14.10.2026; разделы 1–6 при перезапуске не изменились.

Что получилось

Один раз. Три import noisy подряд и ещё тысяча в цикле выполнили тело модуля один раз; importlib.reload выполнил его снова — в том же объекте модуля, а имя, взятое через from до перезагрузки, осталось старым объектом. Модуль лежит в sys.modules уже во время выполнения своего тела, и import noisy as again даёт тот же объект.

from копирует ссылку. После from settings import DEBUG присваивание settings.DEBUG = True не меняет DEBUG у импортирующего — он остался False. Список ITEMS общий: append через модуль виден, ITEMS is settings.ITEMS — True. Отсюда подмена в тестах: settings.DEBUG = True до первого import report (где from settings import DEBUG) report видит, после — нет; mock.patch('settings.DEBUG') до report не доходит, mock.patch('report.DEBUG') — доходит.

__name__. python tool.py и python -m tool печатают '__main__' и выполняют блок if __name__ == "__main__"; import tool печатает 'tool', и блок не выполняется. Скрипт, который импортирует сам себя по имени файла, выполняет тело дважды — как '__main__' и как 'selfimp', — и это два разных объекта модуля.

Циклический импорт (модули в пакете app/, на всех четырёх версиях одинаково):

  • from app.b import Y ↔ from app.a import X — ImportError: cannot import name 'X' from partially initialized module 'app.a' (most likely due to a circular import);
  • import app.a и app.a.X на уровне модуля b — AttributeError: cannot access submodule 'a' of module 'app' (most likely due to a circular import);
  • import app.a и app.a.X внутри функции — работает, app.b.y() -> 2.

Те же два файла прямо в папке запуска: на 3.11 и 3.12 текст ошибки тот же («partially initialized module 'a'»), а на 3.13 и 3.14 подсказка о цикле заменена подсказкой о переименовании: cannot import name 'X' from 'a' (consider renaming '<папка>/a.py' if it has the same name as a library you intended to import).

Свой random.py. Рядом со скриптом он заслоняет стандартный модуль: выполняется он, и random.randint — AttributeError. На 3.13 и 3.14 к тексту ошибки добавлена подсказка: (consider renaming '<папка>/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module); на 3.11 и 3.12 её нет.

Пакет. import pkg.sub сначала выполняет pkg/__init__.py, потом pkg/sub.py. import pkg подмодуль не загружает: hasattr(pkg, "sub") — False, pkg.sub — AttributeError. json/__init__.py свои подмодули импортирует сам: после import json в sys.modules есть json.decoder и json.encoder.

Раздел 7. sys встроен: sys.__spec__.origin — 'built-in', __file__ нет. _json в сборках 3.11.15 и 3.12.3 — модуль-расширение .so, в 3.13.7 и 3.14.7 — встроен (свойство сборки, единственная строка раздела, которая различается по версиям). Папка nspkg/ без __init__.py импортируется: nspkg.__file__ — None, __path__ — список из одной папки. После del sys.modules['noisy'] следующий import выполнил тело снова и дал новый объект. Модуль, упавший при первой загрузке, в sys.modules не остался, и повторный import выполнил его снова. Отдельный процесс через subprocess выполнил тело заново. python -m shop.cli: __name__ — '__main__', __package__ — 'shop', sys.path[0] — папка запуска, относительный from . import PRICE работает; python shop/cli.py — ImportError: attempted relative import with no known parent package.

Цена (медианы, 3.13.7): import json внутри функции стоит 84,3 нс сверх пустого вызова (3.14.7 — 102,3). Первый импорт в свежем интерпретаторе: json — 9 128,1 мкс, decimal — 3 595,0, asyncio — 54 409,2. Первый import json дороже повторного в 108 256 раз (3.14.7 — в 90 965).

Script

350 lines
"""Модули и импорт: что на самом деле делает `import`.

Времени здесь нет — только наблюдения. Цена первого и повторного импорта —
в cost.py.

Каждый сюжет собирает во временной папке маленькие модули и запускает их в
отдельном интерпретаторе: так у каждого сюжета чистый `sys.modules`, а
`__name__ == "__main__"` проверяется настоящим запуском скрипта. Путь
временной папки в выводе заменён на `<папка>` — иначе запись прогона менялась
бы от запуска к запуску.

ШЕСТЬ СЮЖЕТОВ:

1. МОДУЛЬ ВЫПОЛНЯЕТСЯ ОДИН РАЗ. Повторный `import` берёт готовый объект из
   `sys.modules`; заново тело выполняет только `importlib.reload` — в том же
   объекте модуля, а имена, взятые через from, остаются старыми. Модуль
   попадает в `sys.modules` ещё до выполнения тела.

2. `from m import x` КОПИРУЕТ ССЫЛКУ. Новое присваивание `m.x = …` имени,
   импортированного через from, не видно; изменение изменяемого объекта —
   видно. Отсюда подмена в тестах: до первого импорта она доходит, после —
   нет, а `mock.patch` надо ставить там, где имя ищется.

3. `__name__`. Один и тот же файл, запущенный как скрипт и импортированный.
   И скрипт, который импортирует сам себя: это два разных модуля.

4. ЦИКЛИЧЕСКИЙ ИМПОРТ. `from a import X` из недоимпортированного модуля
   падает; `import a` с обращением `a.X` внутри функции — работает.

5. СВОЙ ФАЙЛ С ИМЕНЕМ МОДУЛЯ СТАНДАРТНОЙ БИБЛИОТЕКИ. `random.py` рядом со
   скриптом заслоняет стандартный `random`.

6. ПАКЕТ. `import pkg.sub` сначала выполняет `pkg/__init__.py`; `import pkg`
   подмодуль сам не загружает. А `json/__init__.py` свои подмодули
   импортирует сам.

Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import os
import subprocess
import sys
import tempfile
import textwrap

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


def scene(files, *argv):
    """Создать файлы во временной папке и запустить там интерпретатор."""
    with tempfile.TemporaryDirectory() as tmp:
        tmp = os.path.realpath(tmp)
        for name, body in files.items():
            path = os.path.join(tmp, name)
            os.makedirs(os.path.dirname(path), exist_ok=True)
            with open(path, "w", encoding="utf-8") as f:
                f.write(textwrap.dedent(body))
        env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1")
        env.pop("PYTHONPATH", None)
        r = subprocess.run([sys.executable, *argv], cwd=tmp, env=env, capture_output=True, text=True)
        out = (r.stdout + r.stderr).replace(tmp, "<папка>")
    return out


def show(out, only_last_error=False):
    lines = out.rstrip("\n").split("\n")
    if only_last_error:
        # Из трассировки печатаем строки наших файлов и последнюю — саму ошибку.
        keep = [l for l in lines if not l.startswith((" ", "Traceback"))]
        lines = keep
    for line in lines:
        print("   " + line)


# ── 1 ─────────────────────────────────────────────────────────────────────
print("1) модуль выполняется один раз")
show(scene({
    "noisy.py": """
        import sys
        print("[тело noisy выполняется; noisy уже в sys.modules:", __name__ in sys.modules, "]")
        ITEMS = []
    """,
    "main.py": """
        import importlib
        import sys
        import noisy
        import noisy
        import noisy as again
        print("после трёх import:")
        print("noisy в sys.modules ->", "noisy" in sys.modules)
        print("again is noisy ->", again is noisy)
        print("sys.modules['noisy'] is noisy ->", sys.modules["noisy"] is noisy)
        for _ in range(1000):
            import noisy
        print("ещё 1000 import в цикле — тело не выполнялось")
        from noisy import ITEMS as old_items
        reloaded = importlib.reload(noisy)
        print("после reload — тело выполнилось снова")
        print("reload вернул тот же объект модуля ->", reloaded is noisy)
        print("имя из from до reload is noisy.ITEMS ->", old_items is noisy.ITEMS)
    """,
}, "main.py"))
print()

# ── 2 ─────────────────────────────────────────────────────────────────────
print("2) from m import x копирует ссылку, а не связывает имена")
show(scene({
    "settings.py": """
        DEBUG = False
        ITEMS = []
    """,
    "main.py": """
        import settings
        from settings import DEBUG, ITEMS
        settings.DEBUG = True
        settings.ITEMS.append("x")
        print("settings.DEBUG ->", settings.DEBUG)
        print("DEBUG          ->", DEBUG)
        print("settings.ITEMS ->", settings.ITEMS)
        print("ITEMS          ->", ITEMS)
        print("ITEMS is settings.ITEMS ->", ITEMS is settings.ITEMS)
    """,
}, "main.py"))
patch_files = {
    "settings.py": """
        DEBUG = False
    """,
    "report.py": """
        from settings import DEBUG

        def show():
            return DEBUG
    """,
}
print("   подмена settings.DEBUG = True до первого import report:")
show(scene(patch_files, "-c", "import settings; settings.DEBUG = True; import report; print('report.show() ->', report.show())"))
print("   подмена settings.DEBUG = True после import report:")
show(scene(patch_files, "-c", "import report, settings; settings.DEBUG = True; print('report.show() ->', report.show())"))
print("   mock.patch после import report:")
show(scene(patch_files, "-c", "\n".join([
    "from unittest import mock",
    "import report",
    "with mock.patch('settings.DEBUG', True): print(\"mock.patch('settings.DEBUG') -> report.show() =\", report.show())",
    "with mock.patch('report.DEBUG', True): print(\"mock.patch('report.DEBUG')   -> report.show() =\", report.show())",
])))
print()

# ── 3 ─────────────────────────────────────────────────────────────────────
print("3) __name__: один файл — запуск и импорт")
tool = {
    "tool.py": """
        print("__name__ =", repr(__name__))
        if __name__ == "__main__":
            print("блок if __name__ == '__main__' выполнен")
    """,
}
print("   python tool.py:")
show(scene(tool, "tool.py"))
print("   python -c 'import tool':")
show(scene(tool, "-c", "import tool"))
print("   python -m tool:")
show(scene(tool, "-m", "tool"))
print("   python selfimp.py — скрипт импортирует сам себя:")
show(scene({
    "selfimp.py": """
        print("тело выполняется, __name__ =", repr(__name__))
        COUNTER = []
        if __name__ == "__main__":
            import selfimp
            import sys
            print("selfimp is sys.modules['__main__'] ->", selfimp is sys.modules["__main__"])
    """,
}, "selfimp.py"))
print()

# ── 4 ─────────────────────────────────────────────────────────────────────
# Модули цикла лежат в пакете app/, как в настоящем проекте. Последний
# вариант — те же два файла прямо в папке запуска: на 3.13+ текст ошибки
# там другой (см. README).
print("4) циклический импорт")
print("   app/a: from app.b import Y; app/b: from app.a import X — import app.a:")
show(scene({
    "app/__init__.py": "",
    "app/a.py": """
        from app.b import Y
        X = 1
    """,
    "app/b.py": """
        from app.a import X
        Y = 2
    """,
}, "-c", "import app.a"), only_last_error=True)
print("   app/a: import app.b; app/b: import app.a и app.a.X на уровне модуля:")
show(scene({
    "app/__init__.py": "",
    "app/a.py": """
        import app.b
        X = 1
    """,
    "app/b.py": """
        import app.a
        Y = app.a.X + 1
    """,
}, "-c", "import app.a"), only_last_error=True)
print("   app/a: import app.b; app/b: import app.a и app.a.X внутри функции:")
show(scene({
    "app/__init__.py": "",
    "app/a.py": """
        import app.b
        X = 1
    """,
    "app/b.py": """
        import app.a

        def y():
            return app.a.X + 1
    """,
}, "-c", "import app.a, app.b; print('app.b.y() ->', app.b.y())"))
print("   те же a.py и b.py (from-вариант) прямо в папке запуска — import a:")
show(scene({
    "a.py": """
        from b import Y
        X = 1
    """,
    "b.py": """
        from a import X
        Y = 2
    """,
}, "-c", "import a"), only_last_error=True)
print()

# ── 5 ─────────────────────────────────────────────────────────────────────
print("5) свой random.py рядом со скриптом")
show(scene({
    "random.py": """
        print("[выполняется мой random.py]")
    """,
    "game.py": """
        import random
        print(random.randint(1, 6))
    """,
}, "game.py"), only_last_error=True)
print()

# ── 6 ─────────────────────────────────────────────────────────────────────
print("6) пакет: __init__.py и подмодуль")
pkg = {
    "pkg/__init__.py": """
        print("[pkg/__init__.py]")
    """,
    "pkg/sub.py": """
        print("[pkg/sub.py]")
        NAME = "sub"
    """,
}
print("   import pkg.sub:")
show(scene(pkg, "-c", "import pkg.sub; print('pkg.sub.NAME ->', pkg.sub.NAME)"))
print("   import pkg, затем pkg.sub:")
show(scene(pkg, "-c", "import pkg; print('hasattr(pkg, \"sub\") ->', hasattr(pkg, 'sub')); pkg.sub"), only_last_error=True)
print("   import json — подмодули загрузил json/__init__.py:")
show(scene({}, "-c", "import sys, json; print(\"'json.decoder' in sys.modules ->\", 'json.decoder' in sys.modules); print(\"'json.encoder' in sys.modules ->\", 'json.encoder' in sys.modules)"))
print()

# ── 7 ─────────────────────────────────────────────────────────────────────
# Добавлен по внешнему аудиту урока (14.10.2026). Разделы 1–6 выше не менялись.
#
# ГРАНИЦЫ ПРАВИЛ «МОДУЛЬ — ФАЙЛ .py», «ПАКЕТ — ПАПКА С __init__.py» И «ПОВТОРНО
# ВЫПОЛНЯЕТ ТОЛЬКО reload». Файл .py и папка с __init__.py — типичные случаи,
# а не определение: бывают встроенные модули без файла, модули-расширения на C
# и пакеты-пространства имён без __init__.py. Кеш sys.modules — словарь одного
# интерпретатора: запись можно удалить, и следующий import выполнит модуль
# снова; неудачная первая загрузка в кеше не остаётся; другой процесс
# выполняет модуль заново. И полный пример запуска модуля пакета через -m.
print("7) не только файл .py; границы кеша; python -m пакет.модуль")
print("   встроенный модуль и модуль-расширение:")
show(scene({}, "-c", "\n".join([
    "import sys, _json",
    "print('sys.__spec__.origin ->', repr(sys.__spec__.origin))",
    "print(\"hasattr(sys, '__file__') ->\", hasattr(sys, '__file__'))",
    "print('_json ->', 'встроен в интерпретатор' if _json.__spec__.origin == 'built-in' else 'модуль-расширение, файл .' + _json.__spec__.origin.rsplit('.', 1)[-1])",
])))
print("   пакет без __init__.py (namespace package):")
show(scene({
    "nspkg/mod.py": """
        print("[nspkg/mod.py]")
    """,
}, "-c", "\n".join([
    "import nspkg.mod, nspkg",
    "print('nspkg.__file__ ->', nspkg.__file__)",
    "print('list(nspkg.__path__) ->', list(nspkg.__path__))",
])))
noisy = {
    "noisy.py": """
        print("[тело noisy выполняется]")
    """,
}
print("   del sys.modules['noisy'] и снова import noisy:")
show(scene(noisy, "-c", "\n".join([
    "import sys",
    "import noisy",
    "first = noisy",
    "del sys.modules['noisy']",
    "import noisy",
    "print('новый объект модуля ->', noisy is not first)",
])))
print("   первая загрузка упала — import пробует снова:")
show(scene({
    "flaky.py": """
        import os
        print("[тело flaky выполняется]")
        if not os.path.exists("ready"):
            raise RuntimeError("первая загрузка упала")
    """,
}, "-c", "\n".join([
    "import sys",
    "try:",
    "    import flaky",
    "except RuntimeError as e:",
    "    print('RuntimeError:', e)",
    "print(\"'flaky' in sys.modules ->\", 'flaky' in sys.modules)",
    "open('ready', 'w').close()",
    "import flaky",
    "print(\"'flaky' in sys.modules ->\", 'flaky' in sys.modules)",
])))
print("   другой интерпретатор — свой sys.modules:")
show(scene(noisy, "-c", "\n".join([
    "import subprocess, sys",
    "import noisy",
    "subprocess.run([sys.executable, '-c', 'import noisy'])",
])))
SHOP = {
    "shop/__init__.py": """
        PRICE = 10
    """,
    "shop/cli.py": """
        import sys

        from . import PRICE

        print("__name__ =", repr(__name__))
        print("__package__ =", repr(__package__))
        print("sys.path[0] =", sys.path[0])
        print("PRICE =", PRICE)
    """,
}
print("   python -m shop.cli:")
show(scene(SHOP, "-m", "shop.cli"))
print("   python shop/cli.py:")
show(scene(SHOP, "shop/cli.py"), only_last_error=True)