Часть 1: История проблемы и эволюция решений

1. Эпоха хаоса (Python < 2.4)

Проблема: В начале был только один глобальный путь site-packages. Если Проект А требовал Django 1.0, а Проект Б — Django 2.0, вы были обречены. Установка новой версии ломала старый проект. Это называлось Dependency Hell.

Что за site-packages?

site-packages — это целевая папка (каталог) в Python, предназначенная для хранения сторонних библиотек и пакетов, устанавливаемых пользователем (когда вы выполняете pip install <package_name>, библиотека попадает именно в site-packages). Она входит в стандартный путь поиска модулей (sys.path), что позволяет импортировать установленные пакеты в любом скрипте.

Локация зависит от ОС:

на Windows: Lib/site-packages,

на Linux/macOS: /usr/local/lib/python3.x/dist-packages или site-packages)

Решение: Никакого. Или тупо не обновлять пакеты.

2. Появление virtualenv (2007 год, Ian Bicking)

Идея: «А что, если мы обманем интерпретатор?» До этого момента не было стандартного способа изоляции. virtualenv стал первым популярным инструментом. Как это работало:

  • Скрипт копировал бинарный файл python в новую папку.

  • Создавал структуру папок lib/pythonX.X/site-packages.

  • Главный трюк: он патчил (изменял) код самого исполняемого файла или создавал обертку, которая при старте жестко задавала переменную sys.prefix и sys.path, игнорируя глобальные настройки.

  • Минус: Это был сторонний инструмент, который нужно было устанавливать через pip. Он работал сложно и медленно при создании.

3. Стандартизация: venv (Python 3.3+, PEP 405)

Прорыв: Разработчики поняли, что изоляция нужна всем, и включили механизм прямо в ядро Python.

Суть изменения: Вместо того чтобы копировать тяжелый бинарник интерпретатора (что делал virtualenv), venv создает символические ссылки (symlinks) на системный python.

  • В Linux/macOS: .venv/bin/python -> /usr/bin/python3.9.

  • В Windows: создается небольшой лаунчер .exe, который перенаправляет вызов.

Ключевой файл: pyvenv.cfg. Это текстовый файл, который появился вместе с venv. Он говорит интерпретатору: «Эй, ты сейчас запускаешься в режиме виртуального окружения. Твой домашний каталог вот здесь, и пожалуйста, не смотри в глобальные папки».

Часть 2: Как это работает «под капотом» (Архитектура)

Когда вы запускаете python внутри виртуального окружения, происходит следующая цепочка событий:

1.  Запуск исполняемого файла:

    Вы вызываете .venv/bin/python.

    - Если это симлинк: ОС прозрачно перенаправляет вызов на реальный /usr/bin/python3.

    - Если это скрипт/лаунчер: он запускает реальный интерпретатор с особыми флагами.

2.  Инициализация интерпретатора (C-level):

    Интерпретатор запускается и сразу же проверяет контекст.

    - Он видит, что исполняемый файл лежит внутри папки, содержащей pyvenv.cfg.

    - Он читает этот конфиг. Там есть строка home = /usr/bin.

3.  Построение sys.path:

    Это самый важный момент. Обычно Python строит путь так:

    1.  Директория скрипта.

    2.  Стандартные библиотеки (lib/python3.9).

    3.  Глобальные site-packages (/usr/local/lib...).

    Но в режиме venv логика меняется:

    1.  Директория скрипта.

    2.  Стандартные библиотеки (берутся из home в конфиге).

    3.  ЛОКАЛЬНЫЕ site-packages (.venv/lib/python3.9/site-packages).

    4.  Глобальные site-packages ИСКЛЮЧАЮТСЯ полностью.

    Именно исключение пункта 4 обеспечивает изоляцию. Интерпретатор физически не знает о существовании пакетов, установленных глобально.

4.  Переменные окружения:

Скрипт активации (activate) просто меняет $PATH в вашей оболочке (bash/zsh), чтобы при вводе python попадало в .venv/bin. Но даже без активации, если вы запустите .venv/bin/python полным путем, изоляция сработает благодаря внутренней логике CPython.

Часть 3: Практика — Пишем свой виртуальный "окружитель".

Давай представим, что мы хотим реализовать упрощенный аналог venv на чистом Python. Нам нужно сделать три вещи:

1.  Создать структуру папок.

2.  Создать файл конфигурации pyvenv.cfg.

3.  Создать скрипт-обертку (или симлинк), который заставит интерпретатор понять, что он в изоляции.

Примечание: В реальности создание исполняемых файлов и симлинков зависит от ОС, поэтому код будет эмулировать логику создания структуры и конфига, а также покажет, как интерпретатор читает этот конфиг.

Шаг 1: Структура проекта для самописного venv

my_custom_venv/
├── pyvenv.cfg           <-- Сердце изоляции
├── bin/                 <-- Сюда положим ссылку на python
│   └── python -> /usr/bin/python3
└── lib/
    └── python3.11/
        └── site-packages/ <-- Сюда будем ставить пакеты

Шаг 2: Код генератора (mini_venv.py)

Этот скрипт создаст окружение. Он демонстрирует, что именно важно для работы.

import os

import sys
import shutil
from pathlib import Path

def create_mini_venv(env_name):
    base_path = Path(env_name)

    # 1. Определяем пути текущего системного Python
    # sys.executable -> путь к бинарнику (например, /usr/bin/python3.11)
    # sys.prefix -> базовая установка (например, /usr)
    system_python = sys.executable
    system_prefix = sys.prefix

    # Вычисляем версию (упрощенно)
    version_str = f"python{sys.version_info.major}.{sys.version_info.minor}"

    # 2. Создаем структуру папок
    bin_dir = base_path / "bin"
    lib_dir = base_path / "lib" / version_str / "site-packages"

    os.makedirs(bin_dir, exist_ok=True)
    os.makedirs(lib_dir, exist_ok=True)

    print(f"📁 Создана структура папок в {base_path}")

    # 3. Создаем критически важный файл pyvenv.cfg
    # Именно наличие этого файла сигнализирует интерпретатору о режиме venv
    cfg_content = f"""
                  home = {os.path.dirname(system_python)}
                  include-system-site-packages = false
                  version = {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}
                  executable = {system_python}
                  command = {system_python} -m venv {base_path}
                  """
    (base_path / "pyvenv.cfg").write_text(cfg_content)
    
    print("⚙️  Создан pyvenv.cfg (ключ к изоляции)")

    # 4. Создаем "исполняемый файл" внутри venv
    # В Unix/Linux мы используем симлинк. В Windows нужен .exe лаунчер (здесь упрощено).
    venv_python = bin_dir / "python"

    if os.name == 'nt': # Windows
        # Для Windows реальная реализация сложнее (нужен exe),
        # но для демонстрации создадим bat-файл
        bat_content = f'@echo off\n"{system_python}" %*'
        (bin_dir / "python.bat").write_text(bat_content)
        
        print("🪟 Создан python.bat (эмуляция для Windows)")
    else:
        # Unix/Linux/Mac: создаем симлинк
        if venv_python.exists() or venv_python.is_symlink():
            venv_python.unlink()
            
        os.symlink(system_python, venv_python)
        print("🐧 Создан симлинк bin/python")

    # 5. Создаем скрипт активации (просто меняет PATH в shell)
    activate_script = f"""
                      export VIRTUAL_ENV="{base_path.resolve()}"
                      export PATH="$VIRTUAL_ENV/bin:$PATH"
                      unset PYTHONHOME
                      """
    (bin_dir / "activate").write_text(activate_script)
    
    print("🚀 Создан скрипт activate")

    print("\n✅ Готово! Чтобы проверить:")
    print(f"   source {bin_dir}/activate")
    print(f"   which python  # должен показывать путь внутри {env_name}")
    print(f"   python -c 'import sys; print(sys.path)' # не должно содержать глобальных site-packages")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Использование: python mini_venv.py <имя_папки>")
    else:
        create_mini_venv(sys.argv[1])

Шаг 3: Проверка работы.

Если вы запустите этот скрипт, он создаст папку. Что произойдет, если написать командуpython в терминале?

1.  Вы делаете source bin/activate. Shell меняет $PATH.

2.  Вы пишете python. Shell находит my_custom_venv/bin/python.

3.  Это симлинк на настоящий /usr/bin/python3.

4.  Настоящий интерпретатор стартует.

5.  Магия CPython: Перед выполнением любого кода, интерпретатор смотрит вверх по дереву папок от своего местоположения (bin/).

6.  Он находит файл pyvenv.cfg в родительской папке.

7.  Он парсит строку include-system-site-packages = false.

8.  Он не добавляет глобальные пути в sys.path.

9.  Он добавляет только my_custom_venv/lib/python3.X/site-packages.

Результат: Если вы попробуете import requests, а он установлен только глобально, вы получите ModuleNotFoundError. Изоляция достигнута без изменения кода самого интерпретатора, только благодаря наличию конфига и структуре папок.

Часть 4: Где здесь Poetry?

Poetry в этой схеме играет роль надстройки управления состоянием.

1.  Создание: Poetry может вызвать вышеописанную логику (или нативный venv), чтобы создать папку.

2.  Установка: Когда вы делаете poetry add requests:

- Poetry находит путь к интерпретатору внутри вашего venv (читая конфиг или используя sys.prefix).

- Вызывает pip install (или свой собственный инсталлер), явно указывая целевую директорию: --target /path/to/venv/lib/....

- Poetry не ставит пакеты глобально.

3.  Lock: Poetry гарантирует, что версии в site-packages соответствуют poetry.lock.

Резюме

Виртуальное окружение — это не отдельная программа и не виртуальная машина. Это конвенция (соглашение) между:

  1. Файловой системой (специфическая структура папок).

  2. Конфигурационным файлом (pyvenv.cfg).

  3. Логикой запуска интерпретатора Python (который запрограммирован искать этот файл и менять поведение при его наличии).

Если бы вы удалили файл pyvenv.cfg из готового виртуального окружения и запустили бы bin/python, интерпретатор перестал бы считать себя "виртуальным" и начал бы видеть глобальные пакеты, хотя физически файлы лежали бы в старой папке. Именно конфиг является переключателем режима изоляции.