Пайтон: замкнувшее замыкание
Есть немало вещей, которые мне не нравятся в Пайтоне. Не так давно мы на работе наткнулись ещё на одну.
У нас есть код, который выглядит примерно так (можно написать и одну функцию, но так нагляднее):
def outer(useless = None):
def inner():
if useless is None:
print "Pichal'ka"
return inner
outer()()
Ничего необычного, одна функция определяется внутри другой, использует переменную используя переменную из внешней функции. Всё логично, всё работает.
Теперь меняем код:
def outer(useless = None):
def inner():
if useless is None:
print "Pichal'ka"
# тут много кода, начало мы уже потеряли
if False: useless = None
return inner
outer()()
Запускаем код и видим (Пайтон 2.6.5):
Traceback (most recent call last):
File "sample.py", line 10, in <module>
outer()()
File "sample.py", line 3, in inner
if useless is None:
UnboundLocalError: local variable 'useless' referenced before assignment
Какими бы благородными мотивами не руководствовались создатели Пайтона, мне такое поведение не нравится. Вдруг я забыл как переменная попадает в функцию, присвоил ей что-нибудь и получай ошибку в каком-нибудь редком случае.
Избавиться от эффекта можно, изменив прототип внутренней функции на «inner(useless=useless)».
То же, кстати, происходит при импорте модуля:
import random
def outer():
print random.choice
#import random
outer()
Раскомментируйте строчку со вторым импортом и получите ошибку.
Все эти ошибки возникают из-за того, что интерпретатор специальным образом анализирует тело функции перед её запуском и только те переменные которые не задаются внутри функции связываются с лексическим окружением функции.
Я не придумал хороших причин так делать с точки зрения языка (может их кто-то из читателей тут меня поправит?), поэтому считаю, что так сделали для упрощения интерпретатора.
По-моему, будет совсем неправильно, если при изменении переменной внутри она будет лезть во внешний скоуп и изменять ее там. Это с тем же успехом могло бы породить трудноотлавливаемую ошибку. Передавать переменные через параметры, когда это возможно, в любом случае правильнее.
Я читал, что Гвидо принципиально не реализовывал операторы ++ и -- (хотя вряд ли это было бы очень сложно), чтобы было меньше возможности написать запутанный код. Так что не удивлюсь, что и здесь та же причина.
И я не понял, почему будет ошибка в редком случае. Как только ты переопределяешь глобальную переменную, интерпретатор начинает возвращать ошибку, причем не в момент переопределения (который мог бы быть редким), а в самом начале обработки функции в любом случае.
Если бы в сообщении об ошибке выводились координаты строки, где эта переменная определяется внутри функции, было бы намного понятнее.
Комментарий для fulc.ru:
Если функция outer вызывается в редком случае, то и ошибка будет в редком случае.
Комментарий для fulc.ru:
Но сейчас именно так и происходит. Попробуй:
some_outer_list_variable.append(’test’)
Я имел ввиду, что программа может быть устроена так, что вызов будет очень редким.
Комментарий для u1itka:
Но ведь ошибка-то не там случилась. Кстати, авторизация через ЖЖ работает.
Теперь я точно буду изучать Python только при оооочень большой необходимости. Это же ужас. Я ничего не понял.
Комментарий для jankkhvej.blogspot.com:
Идеальных языков нет. Или я их ещё не видел.
Комментарий для akademic.name:
Надо функцию хотя бы раз запустить, чтобы отладить. Так про любую возможную (не синтаксическую) ошибку можно сказать, что она теоретически может быть редкой.
Комментарий для fulc.ru:
это чрезвычайно удобно, регулярно использую это в яваскрипте : )
Комментарий для fulc.ru:
Точно ли ты запускаешь весь свой код? Или сделал большое изменение предпочитаешь оттестировать его со стороны пользователя и считаешь, что всё ок?
Комментарий для Евгения Степанищева:
Я могу банально опечататься в названии переменной, поэтому как минимум запускаю все, что понаписал. (И не перестаю удивляться, когда большое изменение работает сразу.) И опечатка будет в любом случае сложнее отлавливаемой, потому что я ее не замечу за хитрым if’ом.
Комментарий для arty.name:
Чем это тогда отличается от использования глобальных переменных?
Комментарий для fulc.ru:
Использование переменных верхнего уровня это не зло, зло — захламление глобального scope.
Комментарий для fulc.ru:
Сюрприз! Тем, что они не глобальные, а насколько угодно локальные. Если у меня в пятой вложенности объявлена переменная, которая хранит состояние, изменяемое в шестой вложенности, то где же тут глобальность?
В третьем Питоне [1] ситуация немного улучшена. Перед таким использованием useless пишем nonlocal useless, и всё работает:
http://docs.python.org/py3k/reference/simple_stmts.html#the-nonlocal-statement
[1]: Да, я знаю, что им никто не пользуется ;-)
Комментарий для Евгения Степанищева:
Да это очевидно, однако Python ооочень далёк от моего идеала.
Комментарий для jankkhvej.blogspot.com:
А какой язык ближе к идеалу (на мой взгляд ответ — JavaScript, но, может, есть другая версия)?
Комментарий для Евгения Степанищева:
Это приводит к нечитаемости кода. При активном использовании глобальных переменных (особенно на запись) становится невозможно понять, что делает та или иная функция, по ее названию и передаваемым параметрам.
Комментарий для arty.name:
Если рассматривать кусок кода, соответствующий пятому уровню вложенности, то эта переменная будет в этом смысле глобальной.
Комментарий для fulc.ru:
ну-ну, вы так ещё свойства класса назовите глобальными для кода метода класса
Комментарий для arty.name:
В классе подразумевается, что методы обращаются к свойствам объекта. А в пайтоне объект даже явно передается свойствам в качестве параметра.
Комментарий для fulc.ru:
вы, похоже, не желаете в суть смотреть, а ориентируетесь на какие-то посторонние признаки и странные правила. Дело ваше
Владимир неявно указывает на очень важную проблему, которая, однако, гораздо глубже, чем глобальные области видимости. Эта проблема — присваивание.
Кажется в треде появился функциональщик :)
Комментарий для arty.name:
Я говорил о том, что если хочется изменить переменную внешней области видимости, она должна быть явно передана в параметрах. Просто так изменять ее — очень плохо. Да, питон позволяет сделать append внешнему списку, но тогда автор сам виноват в том, что создал трудноотлаживаемый код, это не проблема интерпретатора.
Комментарий для fulc.ru:
о лёгкости отладки у вас тоже очень странные представления : )
кстати, я бы посмотрел, как вы «исправите» вот такой «сложный» код:
var a = 1;
document.addEventListener(’click’, function(){
a++;
alert(a);
}, false);
Комментарий для arty.name:
Мои представления по крайней мере аргументированы чем-то большим, чем «у вас странные представления».
Любой код простой, когда он занимает пять строчек, так что сарказм не к месту.
Я бы заменил каллбек на метод класса, который бы работал с аттрибутами своего класса. Так было бы сразу видно, где может измениться переменная «a». (В реальном коде я бы ни при каких обстоятельствах не назвал переменную «a», но для вас это, скорее всего, покажется очень странным.)
А почему «аноним»? OpenID сломался?
Комментарий для fulc.ru:
ваши представления аргументированы правилом, которое, в свою очередь, не аргументировано ; )
что касается предложенного решения: вы на пустом месте хотите городить класс со свойством и методом. При этом сам факт, что переменная может быть изменена не в том месте, где она объявлена, никуда не девается. Более того, ситуация ухудшается: в моём варианте переменная явно локальная для scope, и может быть изменена только в нём и его вложенном scope. В вашем варианте со свойством класса изменить переменную может любой метод класса, будь он хоть на 10000 строк.
есть очень справедливое высказывание: «classes are poor man’s closures». Впрочем, не стану отрицать и другое высказывание: «closures are poor man’s classes»
Комментарий для http://arty.name/:
Класс определять там же, где сейчас «а». И метод у него один — increment, который передается в callback. Откуда там 10000 строк возьмется?
Аргументировано тем, что это вы сейчас знаете, какие переменные как где используете, а тому, кто будет поддерживать этот код, придется разбираться в деталях, вместо того, чтобы понять, как работает код, из названий функций и их параметров. Отсюда также следует, что функцию с 6 уровнями вложенности лучше разбить на подфункции, выполняющие простые действия, которые можно описать в их названиях. Это напрямую связано и с отладкой: для такой функции легко написать тесты, проверяя нужную часть функционала. Такой аргумент сойдет?
В общем, я за читаемый код с простыми и понятными действиями, и поэтому считаю правильным, если язык не позволяет выполнить какой-то финт (всех проблем это само собой не решает).
Комментарий для fulc.ru:
Когда твой OpenID не авторизует мой сайт или не отвечает, то я считаю, что этот пользователь — аноним.
Комментарий для Евгения Степанищева:
Ну, универсального идеала нет. Мне чём-то идеален Pascal, в чём-то Perl. Perl, даже, наверное больше идеален. Хотя в ООП они оба сосут, и я бы не стал писать на них ничего больше 25-30 тыс строк.
Комментарий для fulc.ru:
хм, я надеялся на лучшее, но вы всё-таки и правда на пустом месте наворачиваете класс
в этом классы от замыканий ничем не отличаются: везде можно написать имена функций и переменных
не увлекайтесь 6 уровнем вложенности, я таким примером всего лишь показывал вам, что это не глобальные переменные
то, что функция описана в замыкании, не означает, тестирующий код не сможет вызвать её снаружи
пожалуйста, пишите на яве и не приставайте к яваскриптерам : )
когда вам захотелось класс с одним методом создавать, я сразу яву вспомнил, у вас с ней наверняка душевное родство : )
Комментарий для arty.name:
Я с тем же успехом могу сказать, что вы на пустом месте наворачиваете замыкание.
За исключением того, что логика работы класса прозрачнее, и методы забирают низкоуровневую обвязку себе.
С точки зрения читаемости — глобальные в том смысле, что передаются в функцию неявно. Где-то в маленьких блоках кода все будет понятно, где-то — нет, причем такое усложнение не будет ни чем оправдано.
В реальной жизни код будет посложнее того пятистрочного, уверяю. Метод мне был нужен только, чтобы явно передать в callback.
Я писал именно про это. Если параметры передавать явно, а функции делать простыми и короткими, такой ситуации не возникнет.
Комментарий для arty.name:
Еще раз посмотрел на ваш код.
Если он в реальном проекте в точности такой, как в примере -- вопросов нет, можно оставить таким (но переименовать переменную «a»). Но я считал, что это упрощенная версия, поэтому:
* Если a++ скрывает за собой какую-то более сложную обработку, то надо вынести ее в отдельную функцию, выполняющую вычисления и не выполняющую вывод результатов пользователю.
* Если за переменной «a» скрываются несколько переменных, то их нужно поместить в класс, потому что очень легко в другом месте программы (даже если оно пока что не существует) забыть про существование части из них.
* Если за alert(a) скрывается более сложное отображение результатов пользователю, его тоже нужно завернуть в отдельную функцию, отвечающую за это.
В этих допущениях, а также если внешняя функция не содержит другого кода, можно оставить замыкание, оно не повлияет на читаемость. Если же между объявлением a и внутренней функцией находится много другого кода, факт передачи a в нее становится неочевиден читателю, и нужно рефакторить внешнюю функцию.
В рассуждениях также неявно присутствует тезис «код должен быть легко читаемым и легко тестируемым». С чем из вышеперечисленного не согласны?
Комментарий для fulc.ru:
замыкание не нужно наворачивать, оно уже есть, а для класса нужно писать заметно больше символов
дело привычки
не уверяйте, код мне приходилось писать ; ) очень часто замыкания используются именно в таких крохотных кусочках кода
вот вам более реальный пример кода:
var events = [];
eventsById.map(function(event){
events.push(event);
});
этот код очень легко читаем, не имеет потребности в тестировании, и если бы вы его переписали с использованием классов, я б вас уволил, не задумываясь. Даже если этот код станет на пару строчек сложнее, всё равно я не соглашусь с приведёнными вами пунктами.
Комментарий для arty.name:
В такой формулировке это не то, чего нужно избегать. Если давать подробные названия функциям и переменным вместо «a», «obj», «cur», количество символов тоже возрастет.
Тогда читатель не забудет, как в замыкание передалась та или иная переменная.
Этот реальный код я бы, конечно, в класс не оборачивал, но и замыканием тоже бы не стал делать. eventsById -- это массив? Почему бы не скопировать массив в цикле, зачем использовать функцию для отображения map, которая создает массив, который в дальнейшем перейдет сразу в garbage collector? Если уж на то пошло, то почему не
var events = eventsById.map(function(event){
return event;
})
? (Если имелось в виду что-то другое -- сорри, я JS не очень хорошо знаю.)
Комментарий для arty.name:
Или почему хотя бы не eventsById.map(events.push)? Это то, о чем я раньше говорил — передача в callback метода.
ну вот напишите класс с методом и свойством, и посмотрите, насколько увеличится количество токенов (не символов), и насколько усложнится понимание кода
map из объекта создаёт объект, а нужно из объекта массив
из-за того, что вы плохо знаете яваскрипт, вы не понимаете, что это не сработает
собственно, поэтому я и не буду дальше с вами спорить
Комментарий для arty.name:
Удобная позиция, если учесть, что мы не о JS спорили. В пайтоне такое сработает, например.