Как закрытия реализованы?

"Изучая Python, 4-й Ed". упоминания, что:

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

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

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 с ее текущим значением в для цикла, когда цикл работает?

21
задан user2357112 supports Monica 21 March 2019 в 05:22
поделиться

5 ответов

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

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

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

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

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

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

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

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

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

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

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

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

9
ответ дан 29 November 2019 в 22:02
поделиться

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

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

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

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

5
ответ дан 29 November 2019 в 22:02
поделиться

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

1
ответ дан 29 November 2019 в 22:02
поделиться

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

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

1
ответ дан 29 November 2019 в 22:02
поделиться

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

    Level 1:
        acts
        i
    Level 2:
        x

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

Эксперты?

0
ответ дан 29 November 2019 в 22:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: