Как работают лексические замыкания?
-
04-07-2019 - |
Вопрос
Пока я исследовал проблему с лексическими замыканиями в коде Javascript, я столкнулся с этой проблемой в Python:
flist = []
for i in xrange(3):
def func(x): return x * i
flist.append(func)
for f in flist:
print f(2)
Обратите внимание, что этот пример сознательно избегает lambda
.Он печатает «4 4 4», что удивительно.Я бы ожидал «0 2 4».
Этот эквивалентный код Perl делает все правильно:
my @flist = ();
foreach my $i (0 .. 2)
{
push(@flist, sub {$i * $_[0]});
}
foreach my $f (@flist)
{
print $f->(2), "\n";
}
Печатается «0 2 4».
Можете ли вы объяснить разницу?
Обновлять:
Проблема не является с i
будучи глобальным.Это отображает то же поведение:
flist = []
def outer():
for i in xrange(3):
def inner(x): return x * i
flist.append(inner)
outer()
#~ print i # commented because it causes an error
for f in flist:
print f(2)
Как показывает закомментированная строка, i
на тот момент неизвестно.Тем не менее, он печатает «4 4 4».
Решение
Python фактически ведет себя как определено. Созданы три отдельные функции , но каждая из них имеет замыкание среды, в которой они определены - в данном случае, глобальной среды (или среды внешней функции, если цикл находится внутри другой функции). Однако это именно та проблема - в этой среде i мутирован , а все замыкания ссылаются на один и тот же i .
Вот лучшее решение, которое я могу придумать - создать создатель функции и вместо этого вызвать that . Это приведет к созданию разных сред для каждой из созданных функций, с разными значениями i для каждой.
flist = []
for i in xrange(3):
def funcC(j):
def func(x): return x * j
return func
flist.append(funcC(i))
for f in flist:
print f(2)
Это то, что происходит, когда вы смешиваете побочные эффекты и функциональное программирование. Р>
Другие советы
Функции, определенные в цикле, продолжают обращаться к одной и той же переменной i
, пока ее значение изменяется. В конце цикла все функции указывают на одну и ту же переменную, которая содержит последнее значение в цикле: эффект - это то, что сообщается в примере.
Чтобы оценить i
и использовать его значение, общий шаблон должен установить его как параметр по умолчанию: значения параметров по умолчанию оцениваются, когда выполняется оператор def
, и, таким образом, значение переменной цикла заморожено.
Следующее работает как положено:
flist = []
for i in xrange(3):
def func(x, i=i): # the *value* of i is copied in func() environment
return x * i
flist.append(func)
for f in flist:
print f(2)
Вот как вы это делаете, используя библиотеку functools
(которая, я не уверен, была доступна на момент постановки вопроса).
from functools import partial
flist = []
def func(i, x): return x * i
for i in xrange(3):
flist.append(partial(func, i))
for f in flist:
print f(2)
Выводит 0 2 4, как и ожидалось.
посмотрите на это:
for f in flist:
print f.func_closure
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
Это означает, что все они указывают на один и тот же экземпляр переменной i, который после завершения цикла будет иметь значение 2.
Удобочитаемое решение:
for i in xrange(3):
def ffunc(i):
def func(x): return x * i
return func
flist.append(ffunc(i))
Происходит следующее: переменная i захватывается, и функции возвращают значение, к которому она привязана в момент вызова.В функциональных языках такая ситуация никогда не возникает, так как меня бы не оттолкнули.Однако в случае с Python, а также, как вы видели, с Lisp, это уже не так.
Разница с вашим примером схемы связана с семантикой цикла do.Scheme фактически создает новую переменную i каждый раз в цикле, а не повторно использует существующую привязку i, как в других языках.Если вы используете другую переменную, созданную вне цикла, и измените ее, вы увидите то же самое поведение в схеме.Попробуйте заменить цикл на:
(let ((ii 1)) (
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* ii x)) flist))
(set! ii i))
))
Взглянем здесь для дальнейшего обсуждения этого.
[Изменить] Возможно, лучший способ описать это — представить цикл do как макрос, который выполняет следующие шаги:
- Определите лямбду, принимая один параметр (i) с телом, определяемым телом цикла,
- Немедленный вызов этой лямбды с соответствующими значениями i в качестве параметра.
то есть.эквивалент приведенному ниже Python:
flist = []
def loop_body(i): # extract body of the for loop to function
def func(x): return x*i
flist.append(func)
map(loop_body, xrange(3)) # for i in xrange(3): body
i больше не является переменной из родительской области, а представляет собой совершенно новую переменную в своей собственной области (т.параметр для лямбды), и таким образом вы получаете наблюдаемое поведение.В Python нет этой неявной новой области видимости, поэтому тело цикла for просто использует переменную i.
Я до сих пор не совсем убежден, почему в некоторых языках это работает по-разному, а по-другому. В Common Lisp это похоже на Python:
(defvar *flist* '())
(dotimes (i 3 t)
(setf *flist*
(cons (lambda (x) (* x i)) *flist*)))
(dolist (f *flist*)
(format t "~a~%" (funcall f 2)))
печатает "6 6 6" (обратите внимание, что здесь список составляет от 1 до 3 и построен в обратном порядке). В то время как в Scheme он работает как в Perl:
(define flist '())
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* i x)) flist)))
(map
(lambda (f)
(printf "~a~%" (f 2)))
flist)
Печатает " 6 4 2 "
И, как я уже упоминал, Javascript находится в лагере Python / CL. Похоже, здесь есть решение о реализации, к которому разные языки подходят по-разному. Мне бы очень хотелось понять, каково именно решение.
Проблема в том, что все локальные функции связаны с одной и той же средой и, следовательно, с одной и той же переменной i
. Решение (обходной путь) заключается в создании отдельных сред (стековых фреймов) для каждой функции (или лямбды):
t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]
>>> t[1](2)
2
>>> t[2](2)
4
Переменная i
является глобальной, значение которой равно 2 при каждом вызове функции f
.
Я был бы склонен реализовать ваше поведение следующим образом:
>>> class f:
... def __init__(self, multiplier): self.multiplier = multiplier
... def __call__(self, multiplicand): return self.multiplier*multiplicand
...
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]
Ответ на ваше обновление : это не глобальность i
per se , которая вызывает такое поведение, а тот факт, что это переменная из охватывающей области, которая имеет фиксированное значение во время вызова f. Во втором примере значение i
берется из области действия функции kkk
, и ничего не меняется, если вы вызываете функции из flist код>.
Причины такого поведения уже были объяснены, и было опубликовано несколько решений, но я думаю, что это самое питоническое (помните, все в Python является объектом!):
flist = []
for i in xrange(3):
def func(x): return x * func.i
func.i=i
flist.append(func)
for f in flist:
print f(2)
Ответ Клаудиу довольно хороший, он использует генератор функций, но, честно говоря, ответ Пиро - это хак, поскольку он превращает меня в «скрытого». аргумент со значением по умолчанию (он будет работать нормально, но это не " pythonic ").