Почему удаление глобальной переменной с именем __builtins__ не позволяет только REPL получать доступ к встроенным функциям? - PullRequest
0 голосов
/ 07 сентября 2018

У меня есть скрипт на Python со следующим содержимым:

# foo.py

__builtins__ = 3
del __builtins__

print(int)  # <- this still works

Любопытно, что выполнение этого сценария с флагом -i запрещает только доступу REPL к встроенным функциям:

aran-fey@starlight ~> python3 -i foo.py 
<class 'int'>
>>> print(int)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'print' is not defined

Почему скрипт может получить доступ к встроенным функциям, а REPL - нет?

Ответы [ 2 ]

0 голосов
/ 13 сентября 2018

CPython не ищет __builtins__ каждый раз, когда ему нужно выполнить поиск встроенной переменной. У каждого объекта фрейма есть элемент f_builtins, содержащий его встроенную переменную dict, и поиск встроенной переменной проходит через него.

f_builtins устанавливается при создании объекта фрейма. Если новый кадр имеет родительского кадра (f_back) или глобальную переменную dict , отличную от его родительского кадра, то при инициализации объекта кадра __builtins__ будет установлен f_builtins. (Если новый кадр разделяет глобальный dict со своим родительским кадром, то он наследует f_builtins своего родителя.) Это единственный способ __builtins__ участвовать во встроенном поиске переменных. Вы можете увидеть код, который обрабатывает это в _PyFrame_New_NoTrack.

Когда вы удаляете __builtins__ внутри скрипта, это не влияет на f_builtins. Остальная часть кода, выполняемого в стеке фрейма скрипта, все еще видит встроенные функции. Когда сценарий завершается и -i переводит вас в интерактивный режим, каждая интерактивная команда получает новый кадр стека (без родительского элемента), и поиск __builtins__ повторяется. Это когда удаленное __builtins__ наконец имеет значение.

0 голосов
/ 13 сентября 2018

Контекст выполнения другой. В рамках REPL мы работаем построчно ( R ead, E val, P rint, L oop), что позволяет изменять глобальную область выполнения между каждым шагом. Но среда выполнения, выполняющая модуль, должна загрузить код модулей, а затем выполнить его в области видимости.

В CPython пространство имен встроенных файлов, связанное с выполнением блока кода, определяется путем поиска имени __builtins__ в глобальном пространстве имен; это должно быть связано со словарем или модулем (в последнем случае используется словарь модуля). Когда в модуле __main__, __builtins__ является встроенным модулем builtins, в противном случае __builtins__ связан со словарем самого модуля builtins. В обоих контекстах вашего вопроса мы находимся в модуле __main__.

Важно то, что CPython просматривает встроенные только один раз , прямо перед тем, как он начнет выполнять ваш код. В REPL это происходит каждый раз, когда выполняется новый оператор. Но при выполнении скрипта Python все содержимое скрипта представляет собой одну единицу. Вот почему удаление встроенных элементов в середине скрипта не имеет никакого эффекта.

Чтобы более точно воспроизвести этот контекст внутри REPL, вы не должны вводить код модуля построчно, а вместо этого использовать составной оператор:

>>> if 1:
...     del __builtins__
...     print(123)
... 
123
>>> print(123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'print' is not defined

Естественно, вы, вероятно, теперь задаетесь вопросом, как удалить встроенные функции из скрипта. Ответ должен быть очевидным: вы не можете сделать это, связав имя, но вы можете сделать это с помощью мутации:

# foo2.py
__builtins__.__dict__.clear()
print(int)  # <- NameError: name 'print' is not defined

В качестве последнего замечания, тот факт, что имя __builtins__ связано вообще, является подробностью реализации CPython, и это явно задокументировано:

Пользователи не должны трогать __builtins__; это строго деталь реализации.

Не полагайтесь на __builtins__ в отношении чего-либо серьезного, если вам нужен доступ к этой области, правильный путь к import builtins и идти оттуда.

...