Это сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

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

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

У нас есть код, который выглядит примерно так (можно написать и одну функцию, но так нагляднее):

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 комментария
Vladimir Moskva (fulc.ru) 2011

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

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

Vladimir Moskva (fulc.ru) 2011

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

u1itka 2011

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

Akademic (akademic.name) 2011

Комментарий для fulc.ru:

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для fulc.ru:

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

Но сейчас именно так и происходит. Попробуй:

some_outer_list_variable.append(’test’)

И я не понял, почему будет ошибка в редком случае.

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для u1itka:

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

Но ведь ошибка-то не там случилась. Кстати, авторизация через ЖЖ работает.

jankkhvej (jankkhvej.blogspot.com) 2011

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для jankkhvej.blogspot.com:

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для akademic.name:

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

Artemy Tregubenko (arty.name) 2011

Комментарий для fulc.ru:

при изменении переменной внутри она будет лезть во внешний скоуп и изменять ее там

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для fulc.ru:

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

Точно ли ты запускаешь весь свой код? Или сделал большое изменение предпочитаешь оттестировать его со стороны пользователя и считаешь, что всё ок?

Vladimir Moskva (fulc.ru) 2011

Комментарий для Евгения Степанищева:

Точно ли ты запускаешь весь свой код? Или сделал большое изменение предпочитаешь оттестировать его со стороны пользователя и считаешь, что всё ок?

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

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

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для fulc.ru:

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

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

Artemy Tregubenko (arty.name) 2011

Комментарий для fulc.ru:

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

megaflop 2011

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

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

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

jankkhvej (jankkhvej.blogspot.com) 2011

Комментарий для Евгения Степанищева:

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

Да это очевидно, однако Python ооочень далёк от моего идеала.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для jankkhvej.blogspot.com:

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для Евгения Степанищева:

Использование переменных верхнего уровня это не зло

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

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

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

Artemy Tregubenko (arty.name) 2011

Комментарий для fulc.ru:

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

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

Artemy Tregubenko (arty.name) 2011

Комментарий для fulc.ru:

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

desh 2011

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

Александр Карпинский 2011

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

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

Artemy Tregubenko (arty.name) 2011

Комментарий для fulc.ru:

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

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

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

fulc.ru 2011

Комментарий для arty.name:

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

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

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

fulc.ru 2011

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

arty.name/ 2011

Комментарий для fulc.ru:

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

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

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для http://arty.name/:

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

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

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для fulc.ru:

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

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

jankkhvej (jankkhvej.blogspot.com) 2011

Комментарий для Евгения Степанищева:

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

Artemy Tregubenko (arty.name) 2011

Комментарий для fulc.ru:

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

хм, я надеялся на лучшее, но вы всё-таки и правда на пустом месте наворачиваете класс

понять, как работает код, из названий функций и их параметров

в этом классы от замыканий ничем не отличаются: везде можно написать имена функций и переменных

 лучше разбить на подфункции

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

 для такой функции легко написать тесты

то, что функция описана в замыкании, не означает, тестирующий код не сможет вызвать её снаружи

считаю правильным, если язык не позволяет выполнить какой-то финт

пожалуйста, пишите на яве и не приставайте к яваскриптерам : )
когда вам захотелось класс с одним методом создавать, я сразу яву вспомнил, у вас с ней наверняка душевное родство : )

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

Я с тем же успехом могу сказать, что вы на пустом месте наворачиваете замыкание.

в этом классы от замыканий ничем не отличаются: везде можно написать имена функций и переменных

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

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

С точки зрения читаемости — глобальные в том смысле, что передаются в функцию неявно. Где-то в маленьких блоках кода все будет понятно, где-то — нет, причем такое усложнение не будет ни чем оправдано.

пожалуйста, пишите на яве и не приставайте к яваскриптерам : )
когда вам захотелось класс с одним методом создавать, я сразу яву вспомнил, у вас с ней наверняка душевное родство : )

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

Vladimir Moskva (fulc.ru) 2011

Вдруг я забыл как переменная попадает в функцию

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

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

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

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

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

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

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

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

Artemy Tregubenko (arty.name) 2011

Комментарий для fulc.ru:

Я с тем же успехом могу сказать, что вы на пустом месте наворачиваете замыкание.

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

логика работы класса прозрачнее

дело привычки

код будет посложнее того пятистрочного, уверяю

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

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

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

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

для класса нужно писать заметно больше символов

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

очень часто замыкания используются именно в таких крохотных кусочках кода

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

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

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

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

Artemy Tregubenko (arty.name) 2011

для класса нужно писать заметно больше символов

ну вот напишите класс с методом и свойством, и посмотрите, насколько увеличится количество токенов (не символов), и насколько усложнится понимание кода

функцию для отображения map, которая создает массив

map из объекта создаёт объект, а нужно из объекта массив

events.push

из-за того, что вы плохо знаете яваскрипт, вы не понимаете, что это не сработает

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

Vladimir Moskva (fulc.ru) 2011

Комментарий для arty.name:

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