PythonПрограммированиеPython Developer

Что вызывает игнорирование присваиваний в словарь, возвращаемый функцией `locals()` в **Python**, в теле функции, но они сохраняются на уровне модуля?

Проходите собеседования с ИИ помощником Hintsage

Ответ на вопрос.

В CPython, референсной реализации Python, поведение locals() различается в зависимости от области выполнения из-за стратегий оптимизации. На уровне модуля locals() возвращает сам глобальный словарь пространства имен, который является авторитетным местом хранения переменных, поэтому любые изменения сразу отражаются в окружении. Однако внутри функции CPython использует оптимизацию, называемую "быстрые локальные переменные", храня переменные в массиве фиксированного размера указателей PyObject*, индексированном по байт-коду, а не в хэш-таблице. Когда locals() вызывается внутри функции, CPython создает новый словарь и заполняет его значениями из этого быстрого локального массива, создавая временный снимок. Следовательно, запись в этот словарь обновляет только эфемерное отображение, оставляя неизменным основной быстрый локальный массив, поэтому функция продолжает использовать оригинальные значения переменных.

Ситуация из жизни

Команда разработчиков создавала динамический инструмент отладки, который позволял разработчикам внедрять временные утилитарные переменные в середину области выполнения работающей функции через интерфейс удаленной отладки. Первоначальная реализация захватывала locals() на точке останова, внедряла вспомогательные объекты в возвращаемый словарь и ожидала, что работающая функция сможет получить доступ к этим вспомогательным объектам в последующих строках.

Первый подход пытался напрямую изменить словарь, возвращаемый locals(), полагаясь на то, что это живая ссылка на пространство имен функции. Плюсы: Это не требовало изменений в сигнатурах функции и казалось синтаксически простым. Минусы: Это завершалось неявно, потому что CPython рассматривает этот словарь как только для чтения; изменения игнорировались, оставляя фактические локальные переменные неизменными.

Вторая стратегия заключалась в том, чтобы внедрить временное состояние в globals() вместо этого, используя глобальное пространство имен в качестве общего доски объявлений. Плюсы: Этот метод сохранял данные на протяжении всего приложения и был доступен повсюду без передачи аргументов. Минусы: Он вводил серьезные проблемы с безопасностью потоков, загрязнял глобальное пространство имен временными данными отладки и нарушал принципы инкапсуляции, открывая внутреннее состояние для всего процесса.

Окончательное решение заключалось в переработке инструментированных функций для принятия явного аргумента словаря context, через который отладчик мог передавать изменяемое состояние. Плюсы: Этот подход является явным, безопасным для потоков и работает идентично в CPython, PyPy и Jython, соблюдая принцип Python, что явное лучше, чем неявное. Минусы: Это потребовало изменения сигнатур функций и мест вызова, что было связано с большими первоначальными переработками, чем в других подходах.

Команда приняла стратегию передачи явного context. Это устранило зависимость от специфических для CPython деталей реализации, предотвратило загрязнение пространства имен и привело к созданию стабильного, кросс-платформенного инструмента отладки.

Что обычно упускают кандидаты

Почему locals() ведет себя иначе внутри выражения списка по сравнению со стандартным циклом for на уровне модуля?

В Python 3 выражения списков вводят свою собственную локальную область, подобно вложенной функции, чтобы предотвратить утечку переменных из переменной цикла в окружающее пространство имен. Когда locals() вызывается внутри выражения, он возвращает словарь для этой временной области, а не для окружающей функции или модуля. Более того, как и в обычных функциях, этот словарь представляет собой снимок быстрых локальных переменных, если выражение реализовано как отдельный объект кода, так что записи в него не сохраняются. В отличие от этого, на уровне модуля locals() является псевдонимом для globals(), который является живым словарем модуля. Это различие критично, потому что разработчики часто предполагают, что выражения делят то же самое локальное пространство имен, что и их содержащий блок, что приводит к путанице при попытках отладки или внедрения переменных внутрь.

Можно ли заставить запись в быстрые локальные переменные, изменив объект фрейма через sys._getframe(), и какие риски это несет?

Опытные пользователи могут получить доступ к текущему фрейму выполнения, используя sys._getframe(), и изменить frame.f_locals, который CPython представляет как записываемое отображение. В некоторых версиях присваивание frame.f_locals может привести к записи обратно в быстрый локальный массив с использованием внутренних API, таких как PyFrame_LocalsToFast, но это поведение зависит от реализации, подвержено изменениям в версиях и не является частью спецификации языка. Риски включают в себя повреждение памяти, если счетчики ссылок не управляются правильно, непоследовательное поведение, когда оптимизатор игнорирует обновленные значения, потому что уже закэшировал их в регистрах или массиве, и полную неполадку в других реализациях Python, таких как PyPy, которые вообще не используют архитектуру быстрого локального массива. Зависимость от этой техники вводит неопределенное поведение и делает код невозможным для поддержки в разных версиях Python.

Как наличие exec() или eval() с явными локальными переменными влияет на оптимизацию быстрых локальных переменных в функции?

Если тело функции содержит вызов exec() или eval(), который ссылается на локальное пространство имен, CPython не может гарантировать, что переменные будут доступны только через оптимизированный быстрый локальный массив; выполняемая строка может динамически вводить или удалять переменные. Чтобы это учесть, компилятор отключает оптимизацию быстрых локальных переменных для этой функции, переходя к хранению всех локальных переменных в стандартном словаре, который проверяется для каждого обращения. В этом "неоптимизированном" режиме locals() возвращает этот фактический словарь, делая его живым, изменяемым представлением, где изменения сохраняются немедленно. Это объясняет, почему код с использованием exec() часто работает медленнее и почему locals() может казаться работающим "правильно" (дозволяя записи) в таких функциях, тогда как в оптимизированных функциях это не так.