Пайтон: замкнувшее замыкание
Есть немало вещей, которые мне не нравятся в Пайтоне. Не так давно мы на работе наткнулись ещё на одну.
У нас есть код, который выглядит примерно так (можно написать и одну функцию, но так нагляднее):
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()Раскомментируйте строчку со вторым импортом и получите ошибку.
Все эти ошибки возникают
Я не придумал хороших причин так делать с точки зрения языка (может их
Комментарии 42
Я читал, что Гвидо принципиально не реализовывал операторы ++ и -- (хотя вряд ли это было бы очень сложно), чтобы было меньше возможности написать запутанный код. Так что не удивлюсь, что и здесь та же причина.
Но ведьошибка-то не там случилась. Кстати, авторизация через ЖЖ работает.
Если функция outer вызывается в редком случае, то и ошибка будет в редком случае.
Комментарий для fulc.ru:
Но сейчас именно так и происходит. Попробуй:
some_outer_list_variable.append('test')
Я имел ввиду, что программа может быть устроена так, что вызов будет очень редким.
Комментарий для fulc.ru:
это чрезвычайно удобно, регулярно использую это в яваскрипте : )
Чем это тогда отличается от использования глобальных переменных?
Комментарий для fulc.ru:
Точно ли ты запускаешь весь свой код? Или сделал большое изменение предпочитаешь оттестировать его со стороны пользователя и считаешь, что всё ок?
Комментарий для Евгения Степанищева:
Я могу банально опечататься в названии переменной, поэтому как минимум запускаю все, что понаписал. (И не перестаю удивляться, когда большое изменение работает сразу.) И опечатка будет в любом случае сложнее отлавливаемой, потому что я ее не замечу за хитрым if'ом.
Комментарий для fulc.ru:
Использование переменных верхнего уровня это не зло, зло — захламление глобального scope.
Сюрприз! Тем, что они не глобальные, а насколько угодно локальные. Если у меня в пятой вложенности объявлена переменная, которая хранит состояние, изменяемое в шестой вложенности, то где же тут глобальность?
http://docs.python.org/py3k/reference/simp … -statement
[1]: Да, я знаю, что им никто не пользуется
Комментарий для Евгения Степанищева:
Да это очевидно, однако Python ооочень далёк от моего идеала.
А какой язык ближе к идеалу (на мой взгляд ответ — JavaScript, но, может, есть другая версия)?
Комментарий для Евгения Степанищева:
Это приводит к нечитаемости кода. При активном использовании глобальных переменных (особенно на запись) становится невозможно понять, что делает та или иная функция, по ее названию и передаваемым параметрам.
Комментарий для arty.name:
Если рассматривать кусок кода, соответствующий пятому уровню вложенности, то эта переменная будет в этом смысле глобальной.
В классе подразумевается, что методы обращаются к свойствам объекта. А в пайтоне объект даже явно передается свойствам в качестве параметра.
вы, похоже, не желаете в суть смотреть, а ориентируетесь на
Я говорил о том, что если хочется изменить переменную внешней области видимости, она должна быть явно передана в параметрах. Просто так изменять ее — очень плохо. Да, питон позволяет сделать append внешнему списку, но тогда автор сам виноват в том, что создал трудноотлаживаемый код, это не проблема интерпретатора.
о лёгкости отладки у вас тоже очень странные представления : )
кстати, я бы посмотрел, как вы «исправите» вот такой «сложный» код:
var a = 1;
document.addEventListener('click', function(){
a++;
alert(a);
}, false);
Мои представления по крайней мере аргументированы
Любой код простой, когда он занимает пять строчек, так что сарказм не к месту.
Я бы заменил каллбек на метод класса, который бы работал с аттрибутами своего класса. Так было бы сразу видно, где может измениться переменная «a». (В реальном коде я бы ни при каких обстоятельствах не назвал переменную «a», но для вас это, скорее всего, покажется очень странным.)
ваши представления аргументированы правилом, которое, в свою очередь, не аргументировано ; )
что касается предложенного решения: вы на пустом месте хотите городить класс со свойством и методом. При этом сам факт, что переменная может быть изменена не в том месте, где она объявлена, никуда не девается. Более того, ситуация ухудшается: в моём варианте переменная явно локальная для scope, и может быть изменена только в нём и его вложенном scope. В вашем варианте со свойством класса изменить переменную может любой метод класса, будь он хоть на 10000 строк.
есть очень справедливое высказывание: «classes are poor man's closures». Впрочем, не стану отрицать и другое высказывание: «closures are poor man's classes»
Класс определять там же, где сейчас «а». И метод у него один — increment, который передается в callback. Откуда там 10000 строк возьмется?
Аргументировано тем, что это вы сейчас знаете, какие переменные как где используете, а тому, кто будет поддерживать этот код, придется разбираться в деталях, вместо того, чтобы понять, как работает код, из названий функций и их параметров. Отсюда также следует, что функцию с 6 уровнями вложенности лучше разбить на подфункции, выполняющие простые действия, которые можно описать в их названиях. Это напрямую связано и с отладкой: для такой функции легко написать тесты, проверяя нужную часть функционала. Такой аргумент сойдет?
В общем, я за читаемый код с простыми и понятными действиями, и поэтому считаю правильным, если язык не позволяет выполнить
Комментарий для fulc.ru:
Когда твой OpenID не авторизует мой сайт или не отвечает, то я считаю, что этот пользователь — аноним.
Ну, универсального идеала нет. Мне
Комментарий для fulc.ru:
хм, я надеялся на лучшее, но вывсё-таки и правда на пустом месте наворачиваете класс
в этом классы от замыканий ничем не отличаются: везде можно написать имена функций и переменных
не увлекайтесь 6 уровнем вложенности, я таким примером всего лишь показывал вам, что это не глобальные переменные
то, что функция описана в замыкании, не означает, тестирующий код не сможет вызвать её снаружи
пожалуйста, пишите на яве и не приставайте к яваскриптерам : )
когда вам захотелось класс с одним методом создавать, я сразу яву вспомнил, у вас с ней наверняка душевное родство : )
Комментарий для arty.name:
Я с тем же успехом могу сказать, что вы на пустом месте наворачиваете замыкание.
За исключением того, что логика работы класса прозрачнее, и методы забирают низкоуровневую обвязку себе.
С точки зрения читаемости — глобальные в том смысле, что передаются в функцию неявно.Где-то в маленьких блоках кода все будет понятно, где-то — нет, причем такое усложнение не будет ни чем оправдано.
В реальной жизни код будет посложнее того пятистрочного, уверяю. Метод мне был нужен только, чтобы явно передать в callback.
Я писал именно про это. Если параметры передавать явно, а функции делать простыми и короткими, такой ситуации не возникнет.
Еще раз посмотрел на ваш код.
Если он в реальном проекте в точности такой, как в примере -- вопросов нет, можно оставить таким (но переименовать переменную «a»). Но я считал, что это упрощенная версия, поэтому:
* Если a++ скрывает за собой
* Если за переменной «a» скрываются несколько переменных, то их нужно поместить в класс, потому что очень легко в другом месте программы (даже если оно пока что не существует) забыть про существование части из них.
* Если за alert(a) скрывается более сложное отображение результатов пользователю, его тоже нужно завернуть в отдельную функцию, отвечающую за это.
В этих допущениях, а также если внешняя функция не содержит другого кода, можно оставить замыкание, оно не повлияет на читаемость. Если же между объявлением a и внутренней функцией находится много другого кода, факт передачи a в нее становится неочевиден читателю, и нужно рефакторить внешнюю функцию.
В рассуждениях также неявно присутствует тезис «код должен быть легко читаемым и легко тестируемым». С чем из вышеперечисленного не согласны?
Комментарий для fulc.ru:
замыкание не нужно наворачивать, оно уже есть, а для класса нужно писать заметно больше символов
дело привычки
не уверяйте, код мне приходилось писать ; ) очень часто замыкания используются именно в таких крохотных кусочках кода
вот вам более реальный пример кода:
var events = [];
eventsById.map(function(event){
events.push(event);
});
этот код очень легко читаем, не имеет потребности в тестировании, и если бы вы его переписали с использованием классов, я б вас уволил, не задумываясь. Даже если этот код станет на пару строчек сложнее, всё равно я не соглашусь с приведёнными вами пунктами.
Комментарий для arty.name:
В такой формулировке это не то, чего нужно избегать. Если давать подробные названия функциям и переменным вместо «a», «obj», «cur», количество символов тоже возрастет.
Тогда читатель не забудет, как в замыкание передалась та или иная переменная.
что-то другое -- сорри, я JS не очень хорошо знаю.)
Этот реальный код я бы, конечно, в класс не оборачивал, но и замыканием тоже бы не стал делать. eventsById -- это массив? Почему бы не скопировать массив в цикле, зачем использовать функцию для отображения map, которая создает массив, который в дальнейшем перейдет сразу в garbage collector? Если уж на то пошло, то почему не
var events = eventsById.map(function(event){
return event;
})
? (Если имелось в виду
Или почему хотя бы не eventsById.map(events.push)? Это то, о чем я раньше говорил — передача в callback метода.
ну вот напишите класс с методом и свойством, и посмотрите, насколько увеличится количество токенов (не символов), и насколько усложнится понимание кода
map из объекта создаёт объект, а нужно из объекта массив
собственно, поэтому я и не буду дальше с вами спорить
Удобная позиция, если учесть, что мы не о JS спорили. В пайтоне такое сработает, например.