Learning Platform
Глоссарий Troubleshooting
Урок 04.05 · 18 мин
Средний
ModulesImportsys.pathsys.modulesPackagesnamespace

Модули, пакеты, __name__ == '__main__'

Module = .py файл. Package = directory с __init__.py (или namespace package PEP 420). Import — это не copy-paste содержимого; это:

  1. Lookup в кэше sys.modules,
  2. Поиск файла по sys.path,
  3. Execute top-level кода,
  4. Cache результат в sys.modules,
  5. Bind имя в текущий namespace.

Этот урок раскрывает import machinery и связанные patterns: sys.path lookup order, if __name__ == '__main__': idiom, циклические импорты.


Что такое модуль

Каждый .py файл — module. При первом import f Python выполняет:

  1. Проверяет sys.modules — если ключ 'f' уже там, возвращает existing module object (caching). Это значит: повторные import бесплатны.
  2. Иначе ищет файл по sys.path — list директорий в порядке поиска.
  3. Загружает файл и executes top-level код. При этом создаётся module object — special wrapper над глобальным namespace файла.
  4. Записывает в sys.modules — чтобы повторные import’ы попали в cache.
  5. Bind name f в текущий namespace вызывающего.
import sys
print(sys.path)             # list of directories search order
print(list(sys.modules.keys())[:10])  # already-loaded modules

Cite: Lib/importlib/_bootstrap.py — Python implementation import machinery; Python/import.c — C portion.

import f — что происходит шаг за шагом
Шаг 1sys.modules['f']?Cache lookup. sys.modules — это dict mapping имени модуля на module object. Если уже здесь — возвращаем мгновенно (idempotent imports).
miss
Шаг 2sys.path searchПеребор директорий из sys.path. Order: script_dir, PYTHONPATH, stdlib, site-packages. Первое совпадение по имени выигрывает.
found
Шаг 3execute top-levelСоздание module object, исполнение всего кода файла последовательно. Definitions (def, class) создают именованные объекты в module namespace.
done
Шаг 4cache + bindЗапись в sys.modules['f'] (для будущих imports). Bind name 'f' в namespace caller'а — теперь f.attr доступно.

sys.path lookup order

Типичный порядок директорий для поиска:

  1. Directory скрипта (script_dir, или '' для interactive shell — текущая директория).
  2. PYTHONPATH (env variable, разделена : на Linux/macOS, ; на Windows).
  3. Standard library (stdlib Lib/).
  4. site-packages (installed via pip / uv / poetry).
import sys
for p in sys.path:
    print(p)
WARNING

ОЧЕНЬ частый pitfall: вы создали random.py в текущей директории — теперь import random загружает ваш файл, а не stdlib. Никогда не называйте файлы как stdlib modules: random, string, time, json, os, sys, email, etc. Это shadowing — крайне сложно дебажить.

Решение если случилось: python -c "import random; print(random.__file__)" покажет, какой файл загружается. Если ваш — переименовать.


import variants — таблица

FormЧто делает
import fexecutes f.py, binds name f в caller namespace
import f as gто же, но binds под именем g
from f import xexecutes f.py, binds только x (а не сам модуль)
from f import x as yто же, под именем y
from f import *binds все public names (определённые в __all__ или non-underscore)
import f.subexecutes f потом f.sub; binds f (доступ через f.sub.attr)
from f.sub import xexecutes f, f.sub; binds только x
WARNING

from f import * — anti-pattern в production code. Загромождает namespace, скрывает источник имени, ломает IDE introspection. Используйте только в REPL для эксперимента.


Packages — directories с __init__.py

Package — это directory, содержащий __init__.py. Этот файл — точка входа в package; executes при первом import package.

mypackage/
    __init__.py     # executed on `import mypackage`
    core.py
    utils/
        __init__.py
        helpers.py

import mypackage.utils.helpers запускает (по порядку):

  1. mypackage/__init__.py
  2. mypackage/utils/__init__.py
  3. mypackage/utils/helpers.py

Все три module-objects записываются в sys.modules.

__init__.py часто пустой (просто marker, что directory — package). Иногда определяет package-level API:

# mypackage/__init__.py
from .core import main_function, MainClass
from .utils.helpers import helper_function

__all__ = ['main_function', 'MainClass', 'helper_function']
__version__ = '1.0.0'

После этого from mypackage import main_function работает напрямую — клиент не должен знать о внутренней структуре core / utils.


Namespace packages — PEP 420

Python 3.3+: directory без __init__.py тоже может быть package — namespace package. Несколько директорий с одним именем (например в разных PYTHONPATH-entries) merge’ятся в один логический package.

Use case — plugin systems: каждый плагин — отдельная installed package, добавляющая subpackage к общему namespace без conflict’ов.

Differences от regular package:

  • Нет __init__.py — нет точки инициализации.
  • Может быть «split» across multiple parent directories.
  • Implementation detail: regular package содержит __path__ фиксированный; namespace package — _NamespacePath с lazy lookup.

См. PEP 420 — Implicit Namespace Packages.


__name__ == '__main__' idiom

Каждый module имеет специальный атрибут __name__. Значение зависит от того, как module загружается:

  • При normal import (import mymodule) — __name__ == 'mymodule'.
  • При запуске как script (python myfile.py) — __name__ == '__main__'.

Это позволяет файлу служить и как library, и как script:

# myutil.py
def helper():
    return 42

def main():
    print('Running myutil.py as script')
    print('helper() returned:', helper())

if __name__ == '__main__':
    main()

При import myutil блок if __name__ == '__main__': не исполняется — только определения. При python myutil.py — исполняется main().

TIP

Каноничный pattern для Python CLI tools и scripts: if __name__ == '__main__': main() в конце файла. Делает file dual-purpose: importable library + executable script. Рекомендация PEP 8.


Циклические импорты — проблема и обход

A.py импортирует B, B.py импортирует A — circular import. Симптом: ImportError или AttributeError про partially-initialized module.

Корень: при import A Python создаёт пустой module object для A в sys.modules, начинает executing A.py. Если в начале A.py встречает import B, и B.py делает from A import some_function — на этот момент some_function ещё не определена в A (top-level код A не закончил выполнение). Получаем AttributeError.

Решения:

  1. Late import — переместить import внутрь функции, где он нужен. Тогда execute его произойдёт позже, когда оба модуля уже initialized.
# В функции вместо top-level:
def my_func():
    from B import b_func   # late import
    return b_func()
  1. Restructure — выделить общие части в C, и A, B импортируют из C (no cycle).

  2. Type-only imports — для type hints используйте if TYPE_CHECKING: block (PEP 484), который не runtime’ится:

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from B import BClass

def f(b: 'BClass') -> None:   # forward reference как string
    ...

Cross-course context

Cross-course → Spark: 03/07 udfs-introductionimport Python ↔ spark.udf.register("name", func) в PySpark: оба паттерна добавляют именованную функцию в namespace (sys.modules для Python, SparkSession.catalog для Spark). Различие: Python import работает в одном процессе (cache sys.modules локальный); Spark UDF registration сериализует функцию через cloudpickle и пересылает на каждый executor — namespace распределённый.


Ключевые выводы

  1. Module = .py; Package = directory с __init__.py (или namespace package PEP 420).
  2. import = execute top-level + cache в sys.modules + bind name в caller scope. Повторные imports бесплатны (cache hit).
  3. sys.path lookup order: script_dir → PYTHONPATH → stdlib → site-packages. Никогда не shadow’те stdlib именами файлов.
  4. if __name__ == '__main__': — каноничный pattern для dual-purpose script/library.
  5. Circular imports — решаются late imports, restructuring, или TYPE_CHECKING-block для type hints.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Что хранится в `sys.modules`?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6