Сообщить об ошибке.

Глобальное состояние модулей в Python

Временные оверрайды конфигурации

Содержание:

Кратко: в Python каждый модуль загружается один раз и живёт как единый объект в памяти. Все import config во всём проекте ссылаются на один и тот же модуль. Это позволяет менять конфигурацию глобально и даже временно - через getattr()/setattr() и контекстный менеджер.

Модуль как один объект в памяти

Когда вы пишете:

import config

Python делает две вещи:

  1. Ищет модуль config.py, загружает его и выполняет код внутри (только один раз за запуск программы).
  2. Кладёт созданный объект модуля в словарь sys.modules под ключом "config".

Дальше, при любом последующем import config:

  • Python не грузит модуль заново,
  • а просто достаёт тот же самый объект модуля из sys.modules.

Поэтому во всём проекте config - один и тот же объект в памяти.

Пример:

## config.py
MY_CONSTANT = "значение"

## module1.py
import config
print(id(config))

## module2.py
import config
print(id(config))

Оба print(id(config)) выведут одинаковые числа - это ID одного и того же объекта.

Глобальные изменения конфигурации через модуль

Пусть есть такой модуль:

## config.py
MY_CONSTANT = "значение"
DEBUG = False
TIMEOUT = 10

И в разных файлах:

## module1.py
import config
print(config.MY_CONSTANT)

## module2.py
import config
print(config.MY_CONSTANT)

Теперь если где-то вы сделаете:

## module1.py
import config
setattr(config, 'MY_CONSTANT', 'новое значение')

то в любом другом месте:

## module2.py
import config
print(config.MY_CONSTANT) # выведет: 'новое значение'

Это работает, потому что:

  • все импорты config ссылаются на один объект;
  • getattr() и setattr() работают с атрибутами этого объекта;
  • изменение атрибута на объекте видно через все ссылки на этот объект.

Важно отличие:

x = config.MY_CONSTANT # просто скопировали значение
x = "другое" # меняем локальную переменную, модуль не трогаем

Здесь config.MY_CONSTANT не меняется - поменялась только локальная переменная x.

Но:

config.MY_CONSTANT = "другое"
## или
setattr(config, "MY_CONSTANT", "другое")
  • уже меняет сам модуль, и это видят все части программы.

Зачем здесь getattr() и setattr()

Функции getattr() и setattr() удобны, когда имена атрибутов:

  • приходят динамически (из словаря, конфигурационного файла, флагов командной строки),
  • заранее неизвестны в момент написания кода.

Например, у вас есть словарь параметров:

overrides = {
    "DEBUG": True,
    "TIMEOUT": 3,
}

И вы хотите применить их к config:

for key, value in overrides.items():
    if hasattr(config, key):
        setattr(config, key, value)

Здесь:

  • hasattr(config, key) - проверяем, что такой атрибут в config существует;
  • getattr(config, key) - можем при необходимости прочитать старое значение;
  • setattr(config, key, value) - записываем новое.

Это сильно удобнее, чем писать вручную:

config.DEBUG = True
config.TIMEOUT = 3

особенно если список параметров расширяется.

Где это может пригодиться

Тесты и временные настройки

Классический кейс - тесты:

  • В боевом режиме DEBUG = False, TIMEOUT = 10.
  • В тесте нужно на время включить отладку или ускорить таймаут.
def test_something():
    old_debug = config.DEBUG
    try:
        config.DEBUG = True
  # ... запускаем код, который зависит от DEBUG ...
    finally:
        config.DEBUG = old_debug

Но вручную сохранять/откатывать неудобно - именно для этого пригодится контекстный менеджер.

CLI/конфиг-файлы

Пользователь запускает программу с флагами:

python main.py --debug --timeout=2

Вы парсите аргументы, получаете словарь:

overrides = {"DEBUG": True, "TIMEOUT": 2}

И одним общим кодом применяете их в модуль config через setattr().

Временные фичи/флаги

Можно иметь модуль feature_flags.py и временно включать/выключать флаг на время конкретной операции:

with apply_config_overrides({"NEW_PARSER_ENABLED": True}):
    run_parser()

Контекстный менеджер apply_config_overrides

Ваш код:

from contextlib import contextmanager
import config

@contextmanager
def apply_config_overrides(overrides: dict | None):
    """
    Временное переопределение parser.config.* на время выполнения задачи.
    Используем только существующие атрибуты; потом аккуратно откатываем.
    """
    if not overrides:
        yield
        return

    original: dict[str, object] = {}
    try:
        for key, value in overrides.items():
            if hasattr(config, key):
                original[key] = getattr(config, key)
                setattr(config, key, value)
        yield
    finally:
        for key, value in original.items():
            setattr(config, key, value)

Как это используется

with apply_config_overrides({"DEBUG": True, "TIMEOUT": 2}):
  # внутри этого блока config.DEBUG и config.TIMEOUT переопределены
    run_task()

## здесь значения в config уже восстановлены

То есть with создаёт временную зону действия настроек.

Что делает @contextmanager

@contextmanager (из contextlib) превращает обычную генераторную функцию в объект, который можно использовать в with.

С точки зрения протокола:

  • всё, что до первого yield - это __enter__(): подготовка (сохранить старые значения, записать новые);
  • всё, что в блоке finally после yield - это __exit__(): откат изменений, даже если внутри блока было исключение.
  1. Подготовка:

    original = {}
    for key, value in overrides.items():
        if hasattr(config, key):
            original[key] = getattr(config, key) # запомнили старое значение
            setattr(config, key, value) # написали новое
    
  2. Выполнение пользовательского кода:

    yield
    

    Здесь фактически выполняется тело with-блока.

  3. Откат настроек (всегда!):

    finally:
        for key, value in original.items():
            setattr(config, key, value)
    

Зачем это нужно

Основные плюсы такого подхода:

  1. Безопасный откатДаже если внутри with:

    with apply_config_overrides(...):
        do_something() # тут случилось исключение
    
    • всё равно выполнится finally, и конфигурация вернётся к исходному состоянию.
  2. Явные границы влиянияКод читаетcя очень ясно:

    with apply_config_overrides({"DEBUG": True}):
        run_debug_task()
    

    Видно, что DEBUG включён только на время run_debug_task().

  3. Не надо руками помнить старые значенияВсё обёрнуто в один аккуратный помощник, нет копипасты вида:

    old_debug = config.DEBUG
    config.DEBUG = True
    try:
        ...
    finally:
        config.DEBUG = old_debug
    
  4. Фильтрация только существующих атрибутовif hasattr(config, key): защищает от случайного создания мусорных атрибутов, которого вы не ожидали.

На что стоит обратить внимание

Глобальное состояние - это мощно, но опасно

Плюсы:

  • просто использовать;
  • легко переопределять настройки во всём проекте.

Минусы:

  • сложнее тестировать и отлаживать, если многие части кода меняют глобальную конфигурацию;
  • в многопоточных/асинхронных сценариях временные оверрайды могут неожиданно влиять на параллельные задачи.

from config import MY_CONSTANT vs import config

Если вы пишете:

from config import MY_CONSTANT

То вы копируете значение на момент импорта в локальное имя MY_CONSTANT.

Позже изменения в config.MY_CONSTANT не затронут локальную переменную:

from config import MY_CONSTANT

print(MY_CONSTANT) # "значение"
config.MY_CONSTANT = "x"
print(MY_CONSTANT) # всё ещё "значение"

Если же вы всегда используете import config и обращаетесь как config.MY_CONSTANT, то вы всегда смотрите на актуальное значение в модуле.

Итог

  • Модуль в Python - это единый объект, разделяемый всеми импортами.
  • getattr()/setattr() позволяют динамически менять конфиг, особенно удобно, когда имена параметров приходят из внешних источников.
  • Контекстный менеджер apply_config_overrides даёт аккуратный способ временно переопределять настройки модуля и гарантированно их откатывать.
  • Такой паттерн полезен для тестов, CLI-конфигов, временных фич-флагов - везде, где нужно "на время поменять мир, а потом вернуть как было".