Как изменяется область видимости переменной цикла в списковом включении по сравнению с обычным циклом?
В Python 3 переменная цикла внутри спискового включения находится в отдельной области видимости и не изменяет одноимённую переменную снаружи. В обычном цикле for переменная цикла остаётся в окружающей области видимости после завершения цикла.
Такое поведение списковых включений появилось как решение проблемы загрязнения внешней области видимости переменными, использованными только для построения коллекции. В Python 2 переменная цикла спискового включения могла оставаться доступной снаружи, поэтому перенос такого кода на Python 3 иногда меняет наблюдаемое поведение.
Отдельная область видимости также согласуется с общей моделью генераторных выражений и других видов comprehensions: служебные переменные не должны случайно конфликтовать с переменными окружающего кода.
Ошибочное предположение о том, что списковое включение ведёт себя как обычный for, приводит к двум типам проблем. Код может случайно затереть внешнее значение при использовании цикла или, наоборот, начать выдавать ошибку после замены цикла на списковое включение, если позднее он рассчитывал на доступность переменной цикла.
Особенно опасен такой эффект в большом методе: имя выглядит одинаково, но его время жизни и область видимости зависят от формы записи. Это затрудняет рефакторинг и может приводить к скрытым ошибкам при переносе кода между версиями Python.
Списковое включение в Python 3 выполняется в собственной неявной области видимости. Переменная после for является локальной для этого включения, поэтому одноимённое внешнее значение не изменяется. При этом включение может читать переменные из внешней области видимости, если они не перекрыты его собственными именами.
Обычный цикл for не создаёт отдельную область видимости. Его переменная цикла записывается в текущую область видимости и обычно сохраняет последнее присвоенное значение после завершения цикла. Если итераций не было, имя может вообще не появиться, если оно не было определено ранее.
В первом случае внешнее value остаётся строкой, потому что value внутри включения — другая переменная. Во втором случае for записывает последнее значение в окружающую область видимости, поэтому после цикла value равно 2.
Изоляция относится к переменной цикла и другим локальным именам включения, но не означает полной изоляции от внешнего кода: чтение внешних переменных разрешено. Поэтому включение может использовать настройки, функции и значения из окружающей области, если его собственные имена их не затеняют.
В обработчике отчёта переменная row ранее использовалась для последующей проверки последней обработанной записи. Разработчик заменил обычный цикл на списковое включение, рассчитывая сохранить прежнее поведение. После замены внешняя row перестала изменяться, и проверка стала обращаться к старому значению.
Рассматривались три варианта:
Выбрали третий вариант: списковое включение отвечает только за построение списка, а последняя обработанная запись сохраняется отдельным присваиванием. Это устранило зависимость от побочного эффекта области видимости и сделало код устойчивым к дальнейшему рефакторингу.
Да. Отдельная область видимости не является изоляцией от внешних значений: имена, которые не назначаются внутри включения, могут разрешаться во внешней области. Например, коэффициент расчёта или вызываемая функция могут быть определены снаружи. Изменение внешнего объекта возможно, если включение вызывает его изменяющий метод, но это уже вопрос изменяемости объекта, а не утечки переменной цикла.
Да, каждое включение создаёт свою область видимости для своих переменных цикла. Внешнее включение может предоставить имя внутреннему через замыкание, если внутреннее включение только читает его. Поэтому вложенные comprehensions не должны использовать одинаковые имена без необходимости: формально они не обязаны конфликтовать, но такое именование ухудшает читаемость и облегчает логические ошибки.
Нет. Списковое включение сразу вычисляет все элементы и возвращает готовый список, хотя его переменная цикла всё равно не вытекает наружу. Генераторное выражение создаёт ленивый итератор: основная часть вычислений выполняется при последующей итерации, а локальные переменные сохраняются в состоянии генератора. Поэтому при замене спискового включения генератором нужно учитывать не только область видимости, но и момент вычисления, потребление памяти и возможность одноразового прохода.