Кратко: в Python каждый модуль загружается один раз и живёт как единый объект в памяти. Все import config во всём проекте ссылаются на один и тот же модуль. Это позволяет менять конфигурацию глобально и даже временно - через getattr()/setattr() и контекстный менеджер.
Когда вы пишете:
import config
Python делает две вещи:
config.py, загружает его и выполняет код внутри (только один раз за запуск программы).sys.modules под ключом "config".Дальше, при любом последующем import config:
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
Но вручную сохранять/откатывать неудобно - именно для этого пригодится контекстный менеджер.
Пользователь запускает программу с флагами:
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__(): откат изменений, даже если внутри блока было исключение.Подготовка:
original = {} for key, value in overrides.items(): if hasattr(config, key): original[key] = getattr(config, key) # запомнили старое значение setattr(config, key, value) # написали новое
Выполнение пользовательского кода:
yield
Здесь фактически выполняется тело with-блока.
Откат настроек (всегда!):
finally: for key, value in original.items(): setattr(config, key, value)
Основные плюсы такого подхода:
Безопасный откатДаже если внутри with:
with apply_config_overrides(...): do_something() # тут случилось исключение
finally, и конфигурация вернётся к исходному состоянию.Явные границы влиянияКод читаетcя очень ясно:
with apply_config_overrides({"DEBUG": True}): run_debug_task()
Видно, что DEBUG включён только на время run_debug_task().
Не надо руками помнить старые значенияВсё обёрнуто в один аккуратный помощник, нет копипасты вида:
old_debug = config.DEBUG config.DEBUG = True try: ... finally: config.DEBUG = old_debug
Фильтрация только существующих атрибутов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, то вы всегда смотрите на актуальное значение в модуле.
getattr()/setattr() позволяют динамически менять конфиг, особенно удобно, когда имена параметров приходят из внешних источников.apply_config_overrides даёт аккуратный способ временно переопределять настройки модуля и гарантированно их откатывать.