Как осуществляются замыкания? - PullRequest
20 голосов
/ 30 июня 2010

"Изучение Python, 4-е изд." упоминает, что:

переменная области видимости ищется, когда вложенные функции позже называются ..

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

def makeActions():
    acts = []
    for i in range(5): # Tries to remember each i
        acts.append(lambda x: i ** x) # All remember same last i!
return acts

makeActions()[n] одинаково для каждого n, поскольку переменная i каким-то образом просматривается во время вызова. Как Python ищет эту переменную? Не должно ли оно вообще существовать, потому что makeActions уже вышел? Почему Python не делает то, что интуитивно предлагает код, и не определяет каждую функцию, заменяя i ее текущим значением в цикле for во время выполнения цикла?

Ответы [ 5 ]

8 голосов
/ 30 июня 2010

Я думаю, что совершенно очевидно, что происходит, когда вы думаете о i как имя , а не какое-то значение .Ваша лямбда-функция делает что-то вроде «возьми x: посмотри значение i, посчитай i ** x» ... поэтому, когда ты на самом деле запускаешь функцию, она смотрит i только тогда так i is 4.

Вы также можете использовать текущий номер, но вы должны заставить Python связать его с другим именем:

def makeActions():
    def make_lambda( j ):
        return lambda x: j * x # the j here is still a name, but now it wont change anymore

    acts = []
    for i in range(5):
        # now you're pushing the current i as a value to another scope and 
        # bind it there, under a new name
        acts.append(make_lambda(i))
    return acts

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

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

i = 5 
myList = [i, i, i] 
i = 6
print(myList) # myList is still [5, 5, 5].

Вы сказали, что изменили меня на 6 это не то, что на самом деле произошло: i=6 означает «у меня есть значение, 6, и я хочу назвать его i».Тот факт, что вы уже использовали i в качестве имени, не имеет значения для Python, он просто переназначит имя , а не изменит его значение (работает только с переменными).

Можно сказать, что в myList = [i, i, i] любое значение i, на которое в данный момент указывает (число 5), получает три новых имени: mylist[0], mylist[1], mylist[2].То же самое происходит и при вызове функции: аргументам присваиваются новые имена.Но это, вероятно, идет вразрез с любой интуицией о списках ...

Это может объяснить поведение в следующем примере: вы присваиваете mylist[0]=5, mylist[1]=5, mylist[2]=5 - неудивительно, что они не меняются, когдаВы переназначаете i.Если бы i было чем-то изменяемым, например, списком, то изменение i отразилось бы и на всех записях в myList, потому что у вас просто есть разных имен для одного и того же значения !

Простой факт, что вы можете использовать mylist[0] слева от =, доказывает, что это действительно имя.Мне нравится вызывать = оператор присваивания имени : он принимает имя слева и выражение справа, затем оценивает выражение (функция вызова, ищет значения за именами) до тех пор, покаимеет значение и, наконец, дает имя значению. ничего не меняет .

Для комментариев Маркса о компиляции функций:

Ну, ссылки (и указатели) имеют смысл только тогда, когда у нас есть какая-то адресуемая память.Значения хранятся где-то в памяти, и ссылки ведут вас туда.Использование ссылки означает переход в это место в памяти и что-то с ним делать.Проблема в том, что нет этих понятий используется Python!

В Python VM нет понятия памяти - значения плавают где-то в пространстве , а имена - это маленькие тегиподключен к ним (с помощью маленькой красной нити).Имена и значения существуют в разных мирах!

Это имеет большое значение при компиляции функции.Если у вас есть ссылки, вы знаете расположение памяти объекта, на который ссылаетесь.Тогда вы можете просто заменить ссылку на это место.Имена с другой стороны не имеют местоположения, поэтому вам нужно (во время выполнения) следовать этой маленькой красной строке и использовать то, что находится на другом конце.Вот как Python компилирует функции: где бы ни было имя в коде, он добавляет инструкцию, которая выяснит, что означает это имя.

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

Когда вы используете имя, компилятор Python попытается выяснить, к какому пространству имен оно принадлежит.В результате получается инструкция для загрузки этого имени из найденного им пространства имен.

Что возвращает вас к исходной проблеме: в lambda x:x**i i компилируется как поиск в пространстве имен makeActions (потому что там использовался i). Python понятия не имеет и не заботится о значении, стоящем за ним (оно даже не должно быть действительным именем). Тот код, который запускает i, ищется в его оригинальном пространстве имен и дает более или менее ожидаемое значение.

5 голосов
/ 30 июня 2010

Что происходит при создании замыкания:

  • Закрытие строится с указателем на кадр (или примерно блок ), который оно создаетбыл создан в: в этом случае блоке for.
  • Закрытие фактически предполагает совместное владение этим кадром, увеличивая счетчик ссылок кадра и сохраняя указатель на этот кадр в закрытии.Этот кадр, в свою очередь, хранит ссылки на кадры, в которые он был заключен, для переменных, которые были захвачены далее в стеке.
  • Значение i в этом кадре продолжает изменяться до тех пор, пока цикл forвыполняется - каждое присвоение i обновляет привязку i в этом кадре.
  • Как только цикл for завершается, кадр удаляется из стека, но он не выбрасывается, как мог быобычно быть!Вместо этого оно сохраняется, потому что ссылка на кадр все еще активна.Однако в этот момент значение i больше не обновляется.
  • Когда вызывается замыкание, оно выбирает любое значение i в родительском кадре во время вызова.Так как в цикле for вы создаете замыканий, но на самом деле не вызываете их, значение i при вызове будет последним значением, которое у него было после завершения всего цикла.
  • Будущие вызовы makeActions создадут разные кадры.В этом случае вы не будете повторно использовать предыдущий кадр цикла for или обновлять значение i предыдущего кадра.

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

Чтобы получить нужный эффект, вам нужночтобы создать новый кадр для каждого значения i, которое вы хотите захватить, и необходимо создать каждую лямбду со ссылкой на этот новый кадр.Вы не получите это от самого блока for, но вы можете получить это от вызова вспомогательной функции, которая установит новый кадр.См. Ответ THC4k для одного возможного решения в этом направлении.

1 голос
/ 30 июня 2010

Я думал, что при выходе из функции все ее локальные ссылки исчезают.

За исключением тех местных жителей, которые закрыты в закрытии. Они не исчезают, даже когда функция, к которой они относятся, вернулась.

1 голос
/ 30 июня 2010

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

0 голосов
/ 30 июня 2010

Интуитивно можно подумать, что i будет захвачено в его текущем состоянии, но это не так.Думайте о каждом слое как о словаре пар имя-значение.

    Level 1:
        acts
        i
    Level 2:
        x

Каждый раз, когда вы создаете замыкание для внутренней лямбды, вы захватываете ссылку на первый уровень.Я могу только предположить, что во время выполнения будет выполняться поиск переменной i, начиная с уровень 2 и переходя к уровень 1 .Поскольку вы не выполняете эти функции немедленно, все они будут использовать окончательное значение i.

Эксперты?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...