2026, Jan 12 00:02

Почему global в модуле Python не меняет импортированные имена

Разбираем, почему global в модуле Python ведёт себя иначе после import *: пространства имён модуля и __main__, мутация против присваивания, как видеть изменения.

Глобальные переменные в Python могут вводить в заблуждение, когда модуль импортируется в интерактивную сессию. Небольшой скрипт ведёт себя одним образом при запуске как файл и иначе — когда вы вручную вызываете его функцию после импорта со звёздочкой. Корень проблемы не в самом ключевом слове, а в пространствах имён и в том, к чему именно внутри модуля разрешается имя «global».

Воспроизведение поведения на минимальном примере

Рассмотрим короткий файл под названием modscope_demo.py. При запуске как скрипта он выводит ожидаемые состояния «до/после», потому что в этом контексте функция выполняется во время импорта.

items = []
def bump():
    global items
    items = [20, 30, 40]
print("before ", items)
bump()
print("after ", items)

Запуск из оболочки показывает сначала пустой список, затем — присвоенный новый. Импорт со звёздочкой также запускает верхнеуровневые выводы, поэтому на шаге импорта вы увидите схожий результат.

Сюрприз, когда вызываете функцию вручную

Теперь импортируйте имена в интерактивную сессию и вызовите функцию сами. Список, который вы видите в REPL, не меняется, хотя функция выполнилась.

>>> from modscope_demo import *
>>> items
[]
>>> bump()
>>> items
[]

На первый взгляд это выглядит неправильно, но на самом деле соответствует тому, как Python связывает имена между модулями.

Что на самом деле происходит

Внутри модуля global относится к собственному пространству имён модуля. Иными словами, global items внутри modscope_demo разрешается к имени items, хранящемуся в объекте модуля modscope_demo. Когда вы делаете from modscope_demo import *, Python копирует эти имена в пространство имён текущего модуля, которое в интерактивной сессии — это __main__. Два имени, __main__.items и modscope_demo.items, в какой-то момент могут указывать на один и тот же объект, но остаются разными именами в разных пространствах.

Присваивание разрывает общую ссылку. Функция bump присваивает имени items в модуле новый объект списка. Имя items в __main__ всё ещё указывает на исходный пустой список. Идентичность объекта можно увидеть с помощью id().

things = []
def bump():
    global things
    print(things, id(things))
    things = [20, 30, 40]
    print(things, id(things))
# вспомогательная функция, чтобы снова увидеть привязку на уровне модуля
def probe():
    print(things, id(things))

Если импортировать в REPL и вызвать bump, вы увидите, что id, напечатанный до присваивания, совпадает с id импортированного пустого списка, а id после присваивания — уже другой. Просмотр things в REPL по-прежнему показывает исходный объект, тогда как вызов probe из модуля демонстрирует новый. Имена раздельны; ранее была общей только ссылка на объект.

Мутация и присваивание: почему одно распространяется, а другое — нет

Если функция изменяет существующий объект вместо перепривязки имени к новому, оба пространства имён отражают изменения, потому что по-прежнему ссылаются на один и тот же список.

pool = []
def bump():
    global pool
    print(pool, id(pool))
    pool.extend([20, 30, 40])  # изменить существующий список на месте
    print(pool, id(pool))
def probe():
    print(pool, id(pool))

После импорта в REPL значения id до и после остаются одинаковыми, и список, наблюдаемый в интерактивной сессии, также меняется. Новый объект не создавался; исходный список был модифицирован на месте.

Посмотрим границы пространств имён напрямую

Разница становится нагляднее, если импортировать сам модуль и обращаться к его атрибутам явно, а не копировать имена в __main__.

# modscope_demo.py
box = []
def bump():
    global box
    box = [20, 30, 40]
>>> import modscope_demo
>>> modscope_demo.box
[]
>>> box = 5  # имя в __main__
>>> box
5
>>> modscope_demo.box  # имя в модуле — отдельное
[]
>>> modscope_demo.bump()
>>> box  # __main__ остаётся без изменений
5
>>> modscope_demo.box  # привязка в модуле изменилась
[20, 30, 40]

Как разобраться с этой путаницей

Такое поведение напрямую вытекает из правил областей видимости Python. Global внутри модуля нацеливается на словарь имён этого модуля. Импорт со звёздочкой создаёт отдельные имена в пространстве имён вызывающей стороны; это не алиасы, которые отслеживают будущие перепривязки внутри исходного модуля. Если нужно, чтобы вызывающий код наблюдал изменения, выполняемые в модуле с помощью присваивания, обращайтесь к объекту через пространство имён модуля, например modscope_demo.items или modscope_demo.box. Если цель — обновить содержимое уже существующего изменяемого объекта так, чтобы все ссылки это увидели, изменяйте его на месте вместо создания нового и перепривязки имени.

Почему это важно

Понимание различий между именами и объектами, а также между пространствами имён модулей и __main__ помогает избежать тонких багов в интерактивной работе, в тестах и при связке модулей. Это также объясняет, почему функция, «использующая global», кажется безрезультатной после импорта со звёздочкой, и почему изменение объекта на месте даёт ожидаемое общее поведение.

Выводы

Глобальные имена внутри модуля принадлежат этому модулю. Импорт со звёздочкой копирует эти имена в __main__, создавая вторую привязку, которая может разойтись при присваивании в модуле. Если хотите видеть актуальное состояние модуля, обращайтесь к нему через объект модуля. Если нужно, чтобы несколько ссылок отразили обновление, изменяйте общий объект на месте вместо перепривязки имени.