Подтвердить что ты не робот

Как осуществляется закрытие?

"Изучение 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 во время выполнения цикла?

4b9b3361

Ответ 1

Я думаю, что это довольно очевидно, что происходит, когда вы думаете о i как имя, а не какое-то значение. Ваша лямбда-функция делает что-то вроде "возьмите x: найдите значение i, вычислите я ** x"... поэтому, когда вы действительно запускаете функцию, она смотрит вверх i именно тогда, поэтому i есть 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] в левой части =, доказывает, что это действительно имя. Мне нравится называть = оператор присваивания имени: он берет имя слева и выражение справа, затем вычисляет выражение (функцию вызова, ищет значения позади имен), пока оно не имеет значение и, наконец, дает имя к значению. Он не меняет ничего.

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

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

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

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

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

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

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

Ответ 2

Что происходит, когда вы создаете закрытие:

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

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

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

Ответ 3

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

Ответ 4

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

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

Ответ 5

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

    Level 1:
        acts
        i
    Level 2:
        x

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

Эксперты?