Пайтон: замкнувшее замыкание

Есть немало вещей, которые мне не нравятся в Пайтоне. Не так давно мы на работе наткнулись ещё на одну.

У нас есть код, который выглядит примерно так (можно написать и одну функцию, но так нагляднее):
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()
Раскомментируйте строчку со вторым импортом и получите ошибку.

Все эти ошибки возникают из-за того, что интерпретатор специальным образом анализирует тело функции перед её запуском и только те переменные которые не задаются внутри функции связываются с лексическим окружением функции.

Я не придумал хороших причин так делать с точки зрения языка (может их кто-то из читателей тут меня поправит?), поэтому считаю, что так сделали для упрощения интерпретатора.
4 марта 2011 00:23

Vladimir Moskva (fulc.ru)
4 марта 2011, 00:53

По-моему, будет совсем неправильно, если при изменении переменной внутри она будет лезть во внешний скоуп и изменять ее там. Это с тем же успехом могло бы породить трудноотлавливаемую ошибку. Передавать переменные через параметры, когда это возможно, в любом случае правильнее.

Я читал, что Гвидо принципиально не реализовывал операторы ++ и -- (хотя вряд ли это было бы очень сложно), чтобы было меньше возможности написать запутанный код. Так что не удивлюсь, что и здесь та же причина.

Vladimir Moskva (fulc.ru)
4 марта 2011, 01:08

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

u1itka (инкогнито)
4 марта 2011, 07:26

Если бы в сообщении об ошибке выводились координаты строки, где эта переменная определяется внутри функции, было бы намного понятнее.

Akademic (akademic.name)
4 марта 2011, 08:57, ответ предназначен Vladimir Moskva (fulc.ru):

Если функция outer вызывается в редком случае, то и ошибка будет в редком случае.

bolk (bolknote.ru)
4 марта 2011, 09:41, ответ предназначен Vladimir Moskva (fulc.ru):

По-моему, будет совсем неправильно, если при изменении переменной внутри она будет лезть во внешний скоуп и изменять ее там.
Но сейчас именно так и происходит. Попробуй:

some_outer_list_variable.append('test')
И я не понял, почему будет ошибка в редком случае.
Я имел ввиду, что программа может быть устроена так, что вызов будет очень редким.

bolk (bolknote.ru)
4 марта 2011, 09:42, ответ предназначен u1itka

Если бы в сообщении об ошибке выводились координаты строки, где эта переменная определяется внутри функции, было бы намного понятнее.
Но ведь ошибка-то не там случилась. Кстати, авторизация через ЖЖ работает.

jankkhvej (jankkhvej.blogspot.com)
4 марта 2011, 09:44

Теперь я точно буду изучать Python только при оооочень большой необходимости. Это же ужас. Я ничего не понял.

bolk (bolknote.ru)
4 марта 2011, 10:22, ответ предназначен jankkhvej (jankkhvej.blogspot.com):

Идеальных языков нет. Или я их ещё не видел.

Vladimir Moskva (fulc.ru)
4 марта 2011, 11:35, ответ предназначен Akademic (akademic.name):

Надо функцию хотя бы раз запустить, чтобы отладить. Так про любую возможную (не синтаксическую) ошибку можно сказать, что она теоретически может быть редкой.

Artemy Tregubenko (arty.name)
4 марта 2011, 12:06, ответ предназначен Vladimir Moskva (fulc.ru):

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

bolk (bolknote.ru)
4 марта 2011, 13:00, ответ предназначен Vladimir Moskva (fulc.ru):

Надо функцию хотя бы раз запустить, чтобы отладить. Так про любую возможную (не синтаксическую) ошибку можно сказать, что она теоретически может быть редкой.
Точно ли ты запускаешь весь свой код? Или сделал большое изменение предпочитаешь оттестировать его со стороны пользователя и считаешь, что всё ок?

Vladimir Moskva (fulc.ru)
4 марта 2011, 13:22, ответ предназначен bolk (bolknote.ru):

Точно ли ты запускаешь весь свой код? Или сделал большое изменение предпочитаешь оттестировать его со стороны пользователя и считаешь, что всё ок?
Я могу банально опечататься в названии переменной, поэтому как минимум запускаю все, что понаписал. (И не перестаю удивляться, когда большое изменение работает сразу.) И опечатка будет в любом случае сложнее отлавливаемой, потому что я ее не замечу за хитрым if'ом.

Vladimir Moskva (fulc.ru)
4 марта 2011, 13:23, ответ предназначен Artemy Tregubenko (arty.name):

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

bolk (bolknote.ru)
4 марта 2011, 13:29, ответ предназначен Vladimir Moskva (fulc.ru):

Чем это тогда отличается от использования глобальных переменных?
Использование переменных верхнего уровня это не зло, зло — захламление глобального scope.

Artemy Tregubenko (arty.name)
4 марта 2011, 13:29, ответ предназначен Vladimir Moskva (fulc.ru):

Сюрприз! Тем, что они не глобальные, а насколько угодно локальные. Если у меня в пятой вложенности объявлена переменная, которая хранит состояние, изменяемое в шестой вложенности, то где же тут глобальность?

megaflop (инкогнито)
4 марта 2011, 14:14

В третьем Питоне [1] ситуация немного улучшена. Перед таким использованием useless пишем nonlocal useless, и всё работает:

http://docs.python.org/py3k/reference/simple_stmts.html#the-nonlocal-statement

[1]: Да, я знаю, что им никто не пользуется ;-)

jankkhvej (jankkhvej.blogspot.com)
4 марта 2011, 14:46, ответ предназначен bolk (bolknote.ru):

Идеальных языков нет. Или я их ещё не видел.
Да это очевидно, однако Python ооочень далёк от моего идеала.

bolk (bolknote.ru)
4 марта 2011, 15:36, ответ предназначен jankkhvej (jankkhvej.blogspot.com):

А какой язык ближе к идеалу (на мой взгляд ответ — JavaScript, но, может, есть другая версия)?

Vladimir Moskva (fulc.ru)
4 марта 2011, 17:56, ответ предназначен bolk (bolknote.ru):

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

Vladimir Moskva (fulc.ru)
4 марта 2011, 18:03, ответ предназначен Artemy Tregubenko (arty.name):

Если у меня в пятой вложенности объявлена переменная, которая хранит состояние, изменяемое в шестой вложенности, то где же тут глобальность?
Если рассматривать кусок кода, соответствующий пятому уровню вложенности, то эта переменная будет в этом смысле глобальной.

Artemy Tregubenko (arty.name)
4 марта 2011, 19:11, ответ предназначен Vladimir Moskva (fulc.ru):

ну-ну, вы так ещё свойства класса назовите глобальными для кода метода класса

Vladimir Moskva (fulc.ru)
4 марта 2011, 20:54, ответ предназначен Artemy Tregubenko (arty.name):

В классе подразумевается, что методы обращаются к свойствам объекта. А в пайтоне объект даже явно передается свойствам в качестве параметра.

Artemy Tregubenko (arty.name)
4 марта 2011, 21:44, ответ предназначен Vladimir Moskva (fulc.ru):

вы, похоже, не желаете в суть смотреть, а ориентируетесь на какие-то посторонние признаки и странные правила. Дело ваше

desh (инкогнито)
5 марта 2011, 19:41

Владимир неявно указывает на очень важную проблему, которая, однако, гораздо глубже, чем глобальные области видимости. Эта проблема - присваивание.

Александр Карпинский (инкогнито)
5 марта 2011, 22:12

Кажется в треде появился функциональщик :)

Vladimir Moskva (fulc.ru)
6 марта 2011, 01:28, ответ предназначен Artemy Tregubenko (arty.name):

Я говорил о том, что если хочется изменить переменную внешней области видимости, она должна быть явно передана в параметрах. Просто так изменять ее - очень плохо. Да, питон позволяет сделать append внешнему списку, но тогда автор сам виноват в том, что создал трудноотлаживаемый код, это не проблема интерпретатора.

Artemy Tregubenko (arty.name)
6 марта 2011, 06:59, ответ предназначен Vladimir Moskva (fulc.ru):

о лёгкости отладки у вас тоже очень странные представления : )

кстати, я бы посмотрел, как вы «исправите» вот такой «сложный» код:

    var a = 1;
    document.addEventListener('click', function(){
        a++;
        alert(a);
    }, false);

fulc.ru (инкогнито)
6 марта 2011, 18:43, ответ предназначен Artemy Tregubenko (arty.name):

Мои представления по крайней мере аргументированы чем-то большим, чем "у вас странные представления".

Любой код простой, когда он занимает пять строчек, так что сарказм не к месту.

Я бы заменил каллбек на метод класса, который бы работал с аттрибутами своего класса. Так было бы сразу видно, где может измениться переменная "a". (В реальном коде я бы ни при каких обстоятельствах не назвал переменную "a", но для вас это, скорее всего, покажется очень странным.)

fulc.ru (инкогнито)
6 марта 2011, 20:24

А почему "аноним"? OpenID сломался?

http://arty.name/ (инкогнито)
6 марта 2011, 20:25, ответ предназначен fulc.ru:

ваши представления аргументированы правилом, которое, в свою очередь, не аргументировано ; )

что касается предложенного решения: вы на пустом месте хотите городить класс со свойством и методом. При этом сам факт, что переменная может быть изменена не в том месте, где она объявлена, никуда не девается. Более того, ситуация ухудшается: в моём варианте переменная явно локальная для scope, и может быть изменена только в нём и его вложенном scope. В вашем варианте со свойством класса изменить переменную может любой метод класса, будь он хоть на 10000 строк.

есть очень справедливое высказывание: «classes are poor man's closures». Впрочем, не стану отрицать и другое высказывание: «closures are poor man's classes»

Vladimir Moskva (fulc.ru)
6 марта 2011, 21:40, ответ предназначен http://arty.name/

Класс определять там же, где сейчас "а". И метод у него один - increment, который передается в callback. Откуда там 10000 строк возьмется?

Аргументировано тем, что это вы сейчас знаете, какие переменные как где используете, а тому, кто будет поддерживать этот код, придется разбираться в деталях, вместо того, чтобы понять, как работает код, из названий функций и их параметров. Отсюда также следует, что функцию с 6 уровнями вложенности лучше разбить на подфункции, выполняющие простые действия, которые можно описать в их названиях. Это напрямую связано и с отладкой: для такой функции легко написать тесты, проверяя нужную часть функционала. Такой аргумент сойдет?

В общем, я за читаемый код с простыми и понятными действиями, и поэтому считаю правильным, если язык не позволяет выполнить какой-то финт (всех проблем это само собой не решает).

bolk (bolknote.ru)
7 марта 2011, 13:22, ответ предназначен Vladimir Moskva (fulc.ru):

А почему "аноним"? OpenID сломался?
Когда твой OpenID не авторизует мой сайт или не отвечает, то я считаю, что этот пользователь — аноним.

jankkhvej (jankkhvej.blogspot.com)
7 марта 2011, 16:22, ответ предназначен bolk (bolknote.ru):

Ну, универсального идеала нет. Мне чём-то идеален Pascal, в чём-то Perl. Perl, даже, наверное больше идеален. Хотя в ООП они оба сосут, и я бы не стал писать на них ничего больше 25-30 тыс строк.

Artemy Tregubenko (arty.name)
7 марта 2011, 19:49, ответ предназначен Vladimir Moskva (fulc.ru):

Класс определять там же, где сейчас "а". И метод у него один - increment, который передается в callback. Откуда там 10000 строк возьмется?
хм, я надеялся на лучшее, но вы всё-таки и правда на пустом месте наворачиваете класс
понять, как работает код, из названий функций и их параметров
в этом классы от замыканий ничем не отличаются: везде можно написать имена функций и переменных
 лучше разбить на подфункции
не увлекайтесь 6 уровнем вложенности, я таким примером всего лишь показывал вам, что это не глобальные переменные
 для такой функции легко написать тесты
то, что функция описана в замыкании, не означает, тестирующий код не сможет вызвать её снаружи
считаю правильным, если язык не позволяет выполнить какой-то финт
пожалуйста, пишите на яве и не приставайте к яваскриптерам : )
когда вам захотелось класс с одним методом создавать, я сразу яву вспомнил, у вас с ней наверняка душевное родство : )

Vladimir Moskva (fulc.ru)
8 марта 2011, 23:08, ответ предназначен Artemy Tregubenko (arty.name):

Я с тем же успехом могу сказать, что вы на пустом месте наворачиваете замыкание.
в этом классы от замыканий ничем не отличаются: везде можно написать имена функций и переменных
За исключением того, что логика работы класса прозрачнее, и методы забирают низкоуровневую обвязку себе.
не увлекайтесь 6 уровнем вложенности, я таким примером всего лишь показывал вам, что это не глобальные переменные
С точки зрения читаемости - глобальные в том смысле, что передаются в функцию неявно. Где-то в маленьких блоках кода все будет понятно, где-то - нет, причем такое усложнение не будет ни чем оправдано.
пожалуйста, пишите на яве и не приставайте к яваскриптерам : )
когда вам захотелось класс с одним методом создавать, я сразу яву вспомнил, у вас с ней наверняка душевное родство : )
В реальной жизни код будет посложнее того пятистрочного, уверяю. Метод мне был нужен только, чтобы явно передать в callback.

Vladimir Moskva (fulc.ru)
8 марта 2011, 23:25

Вдруг я забыл как переменная попадает в функцию
Я писал именно про это. Если параметры передавать явно, а функции делать простыми и короткими, такой ситуации не возникнет.

Vladimir Moskva (fulc.ru)
8 марта 2011, 23:51, ответ предназначен Artemy Tregubenko (arty.name):

Еще раз посмотрел на ваш код.

Если он в реальном проекте в точности такой, как в примере -- вопросов нет, можно оставить таким (но переименовать переменную "a"). Но я считал, что это упрощенная версия, поэтому:

  * Если a++ скрывает за собой какую-то более сложную обработку, то надо вынести ее в отдельную функцию, выполняющую вычисления и не выполняющую вывод результатов пользователю.

  * Если за переменной "a" скрываются несколько переменных, то их нужно поместить в класс, потому что очень легко в другом месте программы (даже если оно пока что не существует) забыть про существование части из них.

  * Если за alert(a) скрывается более сложное отображение результатов пользователю, его тоже нужно завернуть в отдельную функцию, отвечающую за это.

В этих допущениях, а также если внешняя функция не содержит другого кода, можно оставить замыкание, оно не повлияет на читаемость. Если же между объявлением a и внутренней функцией находится много другого кода, факт передачи a в нее становится неочевиден читателю, и нужно рефакторить внешнюю функцию.

В рассуждениях также неявно присутствует тезис "код должен быть легко читаемым и легко тестируемым". С чем из вышеперечисленного не согласны?

Artemy Tregubenko (arty.name)
9 марта 2011, 08:26, ответ предназначен Vladimir Moskva (fulc.ru):

Я с тем же успехом могу сказать, что вы на пустом месте наворачиваете замыкание.
замыкание не нужно наворачивать, оно уже есть, а для класса нужно писать заметно больше символов
логика работы класса прозрачнее
дело привычки
код будет посложнее того пятистрочного, уверяю
не уверяйте, код мне приходилось писать ; ) очень часто замыкания используются именно в таких крохотных кусочках кода

вот вам более реальный пример кода:

    var events = [];
    eventsById.map(function(event){
        events.push(event);
    });

этот код очень легко читаем, не имеет потребности в тестировании, и если бы вы его переписали с использованием классов, я б вас уволил, не задумываясь. Даже если этот код станет на пару строчек сложнее, всё равно я не соглашусь с приведёнными вами пунктами.

Vladimir Moskva (fulc.ru)
9 марта 2011, 22:34, ответ предназначен Artemy Tregubenko (arty.name):

для класса нужно писать заметно больше символов
В такой формулировке это не то, чего нужно избегать. Если давать подробные названия функциям и переменным вместо "a", "obj", "cur", количество символов тоже возрастет.
очень часто замыкания используются именно в таких крохотных кусочках кода
Тогда читатель не забудет, как в замыкание передалась та или иная переменная.

Этот реальный код я бы, конечно, в класс не оборачивал, но и замыканием тоже бы не стал делать. eventsById -- это массив? Почему бы не скопировать массив в цикле, зачем использовать функцию для отображения map, которая создает массив, который в дальнейшем перейдет сразу в garbage collector? Если уж на то пошло, то почему не
 
  var events = eventsById.map(function(event){
    return event;
  })

? (Если имелось в виду что-то другое -- сорри, я JS не очень хорошо знаю.)

Vladimir Moskva (fulc.ru)
10 марта 2011, 00:04, ответ предназначен Artemy Tregubenko (arty.name):

Или почему хотя бы не eventsById.map(events.push)? Это то, о чем я раньше говорил - передача в callback метода.

Artemy Tregubenko (arty.name)
10 марта 2011, 08:35

для класса нужно писать заметно больше символов
ну вот напишите класс с методом и свойством, и посмотрите, насколько увеличится количество токенов (не символов), и насколько усложнится понимание кода
функцию для отображения map, которая создает массив
map из объекта создаёт объект, а нужно из объекта массив
events.push
из-за того, что вы плохо знаете яваскрипт, вы не понимаете, что это не сработает

собственно, поэтому я и не буду дальше с вами спорить

Vladimir Moskva (fulc.ru)
11 марта 2011, 01:05, ответ предназначен Artemy Tregubenko (arty.name):

Удобная позиция, если учесть, что мы не о JS спорили. В пайтоне такое сработает, например.

Ваше имя или адрес блога (можно OpenID):

Текст вашего комментария, не HTML:

Кому бы вы хотели ответить (или кликните на его аватару)