Перейти к содержанию
Educora
Продвинутый20 мин22 / 42

Пакеты, модули и виртуальные окружения

Организуй проект профессионально: модули и пакеты, `__init__.py`, относительные импорты, `if __name__ == '__main__'`, `venv`, `pip`, `requirements.txt`, `pyproject.toml` и ограничения версий.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять понятия модуля, пакета, sys.path и sys.modules
  • Использовать if __name__ == '__main__', чтобы файл работал и как модуль, и как скрипт
  • Создавать виртуальное окружение через venv, устанавливать пакеты через pip и фиксировать зависимости
  • Читать requirements.txt и pyproject.toml и понимать ограничения версий

Одному проекту нужен Django 4, другому — Django 5: установишь пакет на весь компьютер, и один из проектов обязательно сломается. Коллега запускает твой код и получает ModuleNotFoundError, потому что установленных тобой библиотек у него нет. Настоящие проекты следуют трём правилам: код делится на модули и пакеты, у каждого проекта своё виртуальное окружение, а зависимости записаны в файле вместе с версиями.

Модули и пакеты

Модуль — любой файл .py. Когда ты пишешь import pricing, Python ищет файл в папках из списка sys.path, один раз выполняет его код верхнего уровня и сохраняет объект модуля в sys.modules — последующие импорты берут готовый объект. Пакет — папка с модулями; в обычном пакете есть файл __init__.py, который выполняется при первом импорте пакета. Имена записываются через точку: shop.pricing — модуль pricing в пакете shop. Типичный проект выглядит так:

Text
shop/
├── pyproject.toml
├── requirements.txt
├── .venv/                 (not in git)
├── src/
│   └── shop/
│       ├── __init__.py
│       ├── pricing.py
│       └── cli.py
└── tests/
    └── test_pricing.py
«src layout»: код пакета лежит в src/, а тесты — отдельно в tests/.

Пример ниже создаёт маленький пакет во временной папке и импортирует его тремя разными способами (строки sys.modules.pop нужны только для того, чтобы пример можно было запускать повторно):

Python
import importlib
import sys
import tempfile
from pathlib import Path

root = Path(tempfile.mkdtemp())
(root / 'shop').mkdir()
(root / 'shop' / '__init__.py').write_text('from .pricing import final_price\n')
(root / 'shop' / 'pricing.py').write_text(
    'VAT = 0.18\n'
    'print("running pricing.py as", __name__)\n'
    'def final_price(net):\n'
    '    return round(net * (1 + VAT), 2)\n'
)
sys.path.insert(0, str(root))
sys.modules.pop('shop', None)
sys.modules.pop('shop.pricing', None)
importlib.invalidate_caches()

import shop
import shop.pricing
from shop import final_price
print(final_price(100), shop.pricing.VAT)
print(__name__)
▸ Ожидаемый результат
running pricing.py as shop.pricing
118.0 0.18
__main__

pricing.py выполнился только один раз, несмотря на три импорта. Внутри модуля __name__ — его полное имя (shop.pricing), а в файле, запущенном напрямую, — '__main__'. Строка from .pricing import final_price в __init__.py — относительный импорт: точка означает «этот пакет». Благодаря ей пользователи могут писать from shop import final_price, не зная внутреннего устройства пакета.

if __name__ == '__main__': скрипт или модуль

Файл может быть и импортируемым модулем, и запускаемым скриптом. Условие if __name__ == '__main__': выполняет код скрипта, только когда файл запущен напрямую, а не при импорте. Команда python -m shop.cli находит модуль как часть пакета и запускает его как скрипт, поэтому импорты внутри пакета продолжают работать:

Python
import sys
from shop.pricing import final_price

def main() -> int:
    for arg in sys.argv[1:]:
        print(f'{arg} -> {final_price(float(arg))}')
    return 0

if __name__ == '__main__':
    sys.exit(main())
Файл src/shop/cli.py
Terminal
$ python -m shop.cli 10 25.5
10 -> 11.8
25.5 -> 30.09

Виртуальные окружения: venv и pip

Без виртуального окружения все проекты делят одну папку site-packages, и версии пакетов конфликтуют. Виртуальное окружение — папка внутри проекта (обычно .venv) со своим интерпретатором Python и своими пакетами. Её создаёт стандартный модуль venv, а пакеты в неё устанавливает pip:

  1. 1
    Создай

    В папке проекта: python -m venv .venv.

  2. 2
    Активируй

    Windows: .venv\Scripts\activate; macOS и Linux: source .venv/bin/activate. В начале командной строки появится (.venv).

  3. 3
    Установи

    python -m pip install requests — пакет попадает только в это окружение.

  4. 4
    Зафиксируй

    python -m pip freeze > requirements.txt записывает в файл точные версии установленных пакетов.

  5. 5
    Воспроизведи

    Коллега создаёт своё окружение и выполняет python -m pip install -r requirements.txt.

  6. 6
    Выйди

    Команда deactivate возвращает обычную командную строку.

Terminal
$ python -m venv .venv
$ source .venv/bin/activate
(.venv) $ python -m pip list
Package Version
------- -------
pip     25.2
(.venv) $ python -m pip install requests
Collecting requests
...
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2024.8.30 charset-normalizer-3.4.0 idna-3.10 requests-2.32.3 urllib3-2.2.3
(.venv) $ python -m pip freeze
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
requests==2.32.3
urllib3==2.2.3
Новое окружение пустое; requests приносит с собой свои зависимости. Номера версий будут зависеть от дня установки.

requirements.txt и pyproject.toml

requirements.txt — простой список: по одному пакету в строке, при желании с ограничением версии. Для приложений принято закреплять точные версии (==), чтобы программа на любом компьютере работала с одними и теми же библиотеками:

Text
# requirements.txt
requests==2.32.3
fastapi>=0.110,<1.0
python-dotenv~=1.0
ЗаписьЗначение
==2.32.3ровно эта версия
>=0.110,<1.0диапазон: 0.110 или новее, но ниже 1.0
~=1.4совместимый выпуск: >=1.4 и <2.0
(нет)любая версия — рискованно

Главный файл современного проекта — **pyproject.toml**: в нём записаны имя, версия, поддерживаемые версии Python, зависимости, инструменты только для разработки и скрипты командной строки. Команда python -m pip install -e . в папке проекта устанавливает пакет в «редактируемом» режиме: после изменений кода переустанавливать ничего не нужно, а команда shop из [project.scripts] сразу работает.

Text
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "shop"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["requests>=2.31"]

[project.optional-dependencies]
dev = ["pytest>=8"]

[project.scripts]
shop = "shop.cli:main"
Файл pyproject.toml (формат TOML). Инструменты разработки устанавливаются командой python -m pip install -e ".[dev]".
Задание

Напиши parse_requirements(text): она возвращает словарь {пакет: ограничение} по тексту requirements.txt. Пропускай пустые строки и комментарии, начинающиеся с #; если ограничения нет, значение — 'any version'.

Задание · Python
import re

requirements = '''
# web
requests==2.32.3
fastapi>=0.110,<1.0

python-dotenv~=1.0
numpy
'''

def parse_requirements(text):
    result = {}
    # go through the lines, skip blanks and comments,
    # split each line into the package name and the specifier
    return result

for name, spec in parse_requirements(requirements).items():
    print(f'{name}: {spec}')
▸ Ожидаемый результат
requests: ==2.32.3
fastapi: >=0.110,<1.0
python-dotenv: ~=1.0
numpy: any version
Задание

Сортировать версии как строки неправильно: '0.10.0' оказывается раньше '0.9.3'. Напиши функцию version_key, чтобы версии сортировались правильно и печаталась самая новая.

Задание · Python
versions = ['0.10.0', '0.9.3', '1.0.0', '0.1.0', '0.9.12']

def version_key(v):
    # turn '0.10.0' into something that compares correctly
    return v

print(sorted(versions))
print(sorted(versions, key=version_key))
print('latest:', max(versions, key=version_key))
▸ Ожидаемый результат
['0.1.0', '0.10.0', '0.9.12', '0.9.3', '1.0.0']
['0.1.0', '0.9.3', '0.9.12', '0.10.0', '1.0.0']
latest: 1.0.0

Главное

  • Модуль — файл .py, пакет — папка с модулями; код модуля выполняется один раз, при первом импорте, и кэшируется в sys.modules.
  • __name__ равно '__main__' в файле, запущенном напрямую; if __name__ == '__main__': не даёт коду скрипта выполняться при импорте.
  • У каждого проекта своё виртуальное окружение: python -m venv .venv, активация, python -m pip install ....
  • Зависимости записываются в requirements.txt или pyproject.toml; ==, >=,< и ~= ограничивают версии.
  • .venv не попадает в git: окружение всегда пересоздаётся по файлу зависимостей.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Что делает команда python -m venv .venv?