Как работают лексические закрытия?
Пока я изучал проблему, связанную с лексическими замыканиями в коде 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".
Ответы
Ответ 1
Python действительно ведет себя как определено. Создаются три отдельные функции, но каждый из них имеет закрытие среды, в которой они определены в - в этом случае глобальная среда (или внешняя функциональная среда, если цикл помещается внутри другой функции). Это в точности проблема, однако - в этой среде я мутируется, а замыкания на всех относятся к одному и тому же i.
Вот лучшее решение, которое я могу придумать - создайте создатель функции и вызовите , что. Это заставит различные среды для каждой из созданных функций с разными 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)
Это то, что происходит, когда вы смешиваете побочные эффекты и функциональное программирование.
Ответ 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)
Ответ 3
Вот как вы это делаете, используя библиотеку 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, как и ожидалось.
Ответ 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))
Ответ 5
Что происходит, так это то, что переменная я захватывается, а функции возвращают значение, к которому она привязана во время ее вызова. В функциональных языках такая ситуация никогда не возникает, поскольку я не буду отскакивать. Однако с помощью python, а также, как вы видели с помощью lisp, это уже не так.
Разница с примером вашей схемы связана с семантикой цикла do. Схема эффективно создает новую переменную я каждый раз через цикл, вместо того, чтобы повторно использовать существующую привязку i, как и к другим языкам. Если вы используете другую переменную, созданную внешним для цикла и мутирующую ее, вы увидите то же поведение в схеме. Попробуйте заменить свой цикл:
(let ((ii 1)) (
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* ii x)) flist))
(set! ii i))
))
Посмотрите здесь для дальнейшего обсуждения этого вопроса.
[Edit] Возможно, лучший способ описать это - подумать о цикле do как макросе, который выполняет следующие шаги:
- Определите лямбда, взяв единственный параметр (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.
Ответ 6
Я все еще не совсем убежден, почему на некоторых языках это работает в одном направлении и по-другому. В 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. Похоже, здесь есть решение об осуществлении, которое различными языками подходит по-разному. Мне очень хотелось бы понять, в чем именно решение.
Ответ 7
Проблема заключается в том, что все локальные функции привязаны к одной и той же среде и, следовательно, к той же переменной i
. Решение (обходное решение) заключается в создании отдельных сред (стековых фреймов) для каждой функции (или лямбда):
t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]
>>> t[1](2)
2
>>> t[2](2)
4
Ответ 8
Переменная 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
.
Ответ 9
Обоснование поведения уже объяснено, и несколько решений были опубликованы, но я думаю, что это самый пифонический (помните, что все в 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)
Ответ Claudiu довольно хорош, используя генератор функций, но пиро-ответ - это взлом, если честно, поскольку он превращает я в "скрытый" аргумент со значением по умолчанию (он будет работать нормально, но это не так ", вещий" ).