Модули, пакеты, __name__ == '__main__'
Module = .py файл. Package = directory с __init__.py (или namespace package PEP 420). Import — это не copy-paste содержимого; это:
- Lookup в кэше
sys.modules, - Поиск файла по
sys.path, - Execute top-level кода,
- Cache результат в
sys.modules, - Bind имя в текущий namespace.
Этот урок раскрывает import machinery и связанные patterns: sys.path lookup order, if __name__ == '__main__': idiom, циклические импорты.
Что такое модуль
Каждый .py файл — module. При первом import f Python выполняет:
- Проверяет
sys.modules— если ключ'f'уже там, возвращает existing module object (caching). Это значит: повторныеimportбесплатны. - Иначе ищет файл по
sys.path— list директорий в порядке поиска. - Загружает файл и executes top-level код. При этом создаётся module object — special wrapper над глобальным namespace файла.
- Записывает в
sys.modules— чтобы повторные import’ы попали в cache. - 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.
sys.path lookup order
Типичный порядок директорий для поиска:
- Directory скрипта (script_dir, или
''для interactive shell — текущая директория). PYTHONPATH(env variable, разделена:на Linux/macOS,;на Windows).- Standard library (stdlib
Lib/). - site-packages (installed via pip / uv / poetry).
import sys
for p in sys.path:
print(p)
ОЧЕНЬ частый 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 f | executes f.py, binds name f в caller namespace |
import f as g | то же, но binds под именем g |
from f import x | executes f.py, binds только x (а не сам модуль) |
from f import x as y | то же, под именем y |
from f import * | binds все public names (определённые в __all__ или non-underscore) |
import f.sub | executes f потом f.sub; binds f (доступ через f.sub.attr) |
from f.sub import x | executes f, f.sub; binds только x |
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 запускает (по порядку):
mypackage/__init__.pymypackage/utils/__init__.pymypackage/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().
Каноничный 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.
Решения:
- Late import — переместить import внутрь функции, где он нужен. Тогда execute его произойдёт позже, когда оба модуля уже initialized.
# В функции вместо top-level:
def my_func():
from B import b_func # late import
return b_func()
-
Restructure — выделить общие части в C, и A, B импортируют из C (no cycle).
-
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-introduction —
importPython ↔spark.udf.register("name", func)в PySpark: оба паттерна добавляют именованную функцию в namespace (sys.modulesдля Python,SparkSession.catalogдля Spark). Различие: Pythonimportработает в одном процессе (cachesys.modulesлокальный); Spark UDF registration сериализует функцию через cloudpickle и пересылает на каждый executor — namespace распределённый.
Ключевые выводы
- Module =
.py; Package = directory с__init__.py(или namespace package PEP 420). - import = execute top-level + cache в sys.modules + bind name в caller scope. Повторные imports бесплатны (cache hit).
- sys.path lookup order: script_dir → PYTHONPATH → stdlib → site-packages. Никогда не shadow’те stdlib именами файлов.
if __name__ == '__main__':— каноничный pattern для dual-purpose script/library.- Circular imports — решаются late imports, restructuring, или TYPE_CHECKING-block для type hints.