Deep Engineering

MEASUREMENT

bench/imports/cost.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

101 lines
"""Цена импорта: первый `import` против повторного.

ЗАЧЕМ. «import внутри функции — это медленно» и «импорт выполняется один раз»
звучат как противоречие. Замер его снимает: первый импорт модуля —
это поиск файла, чтение, компиляция или загрузка .pyc и выполнение тела,
счёт идёт на миллисекунды; повторный — поиск в словаре `sys.modules` и
связывание имени, счёт идёт на десятки наносекунд.

ЧТО МЕРЯЕТСЯ:

  1. повторный импорт — функция с `import json` внутри против функции,
     которая берёт `json` из глобальных имён модуля; нс на вызов. Модуль к
     этому моменту уже загружен.

  2. первый импорт — `import json`, `import decimal` и `import asyncio` в
     свежем интерпретаторе: время самого оператора import, мкс. Каждый
     модуль — в своём новом процессе, ROUNDS раз. Файлы .pyc к этому времени
     уже есть (первый, «разогревающий» запуск в счёт не идёт), то есть это
     импорт без компиляции — так он и выглядит в работающем проекте.

КАК. Формы меряются вперемежку, ROUNDS кругов; печатаются минимум, медиана
и максимум по кругам.

Запускать: python3.13 / 3.14. Числа — этой машины; переносятся отношения.
"""
import json  # noqa: F401 — загружаем заранее: раздел 1 меряет повторный импорт
import statistics
import subprocess
import sys
import time

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

ROUNDS = 9


def summary(name, xs):
    print(f"   {name:<26}{min(xs):>10.1f}{statistics.median(xs):>10.1f}{max(xs):>10.1f}")
    return statistics.median(xs)


# ── 1: повторный импорт ──────────────────────────────────────────────────
print("1) повторный импорт: import json внутри функции, нс на вызов")
N = 200_000


def with_import():
    import json

    return json


def with_global():
    return json


def empty():
    return None


def loop(fn):
    t0 = time.perf_counter()
    for _ in range(N):
        fn()
    return (time.perf_counter() - t0) / N * 1e9


forms = [("пустая функция", empty), ("json из глобальных", with_global), ("import json внутри", with_import)]
samples = {name: [] for name, _ in forms}
for _ in range(ROUNDS):
    for name, fn in forms:
        samples[name].append(loop(fn))
print(f"   {'форма':<26}{'мин':>10}{'медиана':>10}{'макс':>10}")
med1 = {name: summary(name, samples[name]) for name, _ in forms}
repeat_ns = med1["import json внутри"] - med1["пустая функция"]
print(f"   import json сверх пустой функции по медианам: {repeat_ns:.1f} нс")
print()

# ── 2: первый импорт ─────────────────────────────────────────────────────
print("2) первый импорт в свежем интерпретаторе, мкс на оператор import")
CHILD = "import time; t = time.perf_counter(); import {m}; print((time.perf_counter() - t) * 1e6)"
MODULES = ["json", "decimal", "asyncio"]


def first_import(module):
    out = subprocess.run([sys.executable, "-c", CHILD.format(m=module)], capture_output=True, text=True, check=True)
    return float(out.stdout)


for m in MODULES:  # разогрев: создать .pyc, если их нет
    first_import(m)
samples = {m: [] for m in MODULES}
for _ in range(ROUNDS):
    for m in MODULES:
        samples[m].append(first_import(m))
print(f"   {'модуль':<26}{'мин':>10}{'медиана':>10}{'макс':>10}")
med2 = {m: summary(f"import {m}", samples[m]) for m in MODULES}
ratio = med2["json"] * 1000 / repeat_ns
print(f"   первый import json / повторный по медианам: {ratio:.0f}")