Как работают лексические замыкания?
пока я исследовал проблему, которую я имел с лексическими замыканиями в коде 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".
9 ответов
Python фактически ведет себя так, как определено. три отдельные функции создаются, но у каждого из них есть закрытие среды, в которой они определены в - в этом случае глобальная среда (или среда внешней функции, если цикл помещен внутри другой функции). Это точно проблема, хотя в этой среде, Я мутировал, и закрытие всех обратитесь к тому же i.
здесь лучше решение, которое я могу придумать - создать функцию creater и вызвать это вместо. Это заставит различных средах для каждой из созданных функций, с Я в каждом из них.
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. Схема эффективно создает новую переменную i каждый раз через цикл, вместо того, чтобы повторно использовать существующую привязку 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) с телом, определенным телом цикла,
- немедленный вызов этой лямбды с соответствующими значениями i в качестве ее параметра.
ie. эквивалент приведенного ниже 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.
Я до сих пор не совсем уверен, почему на некоторых языках это работает так, а на некоторых по-другому. В общем 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 и построен в обратном порядке"). В то время как в схеме он работает как в 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]
ответ на ваше обновление: это не globalness из i
per se что вызывает это поведение, это тот факт, что это переменная из охватывающей области, которая имеет фиксированное значение в течение времени, когда вызывается f. Во втором например, значение i
берется из области kkk
функция, и ничего не меняется, когда вы вызываете функции на flist
.
обоснование поведения уже было объяснено, и было опубликовано несколько решений, но я думаю, что это самый pythonic (помните, все в 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 довольно хорош, используя функциональный генератор, но ответ пиро-это хак, Если честно, поскольку он превращает i в "скрытый" аргумент со значением по умолчанию (он будет работать нормально, но это не "питон").