Как работают лексические замыкания?
Пока я исследовал проблему с лексическими замыканиями в коде 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.
Вот лучшее решение, которое я могу придумать - создать создатель функций и вызвать его вместо этого. Это заставит разные среды для каждой из созданных функций, с разными 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. Схема эффективно создает новую переменную 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 в качестве параметра.
то есть. эквивалент нижеследующего питона:
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.
Проблема в том, что все локальные функции связаны с одной и той же средой и, следовательно, с i
переменная. Решение (обходной путь) заключается в создании отдельных сред (стековых фреймов) для каждой функции (или лямбда):
t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]
>>> t[1](2)
2
>>> t[2](2)
4
Я до сих пор не совсем убежден, почему в некоторых языках это работает по-другому, а по-другому. В 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 и построен в обратном порядке "). В то время как в схеме он работает как в 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
является глобальным, значение которого равно 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
По сути, это является причиной того, что это переменная из охватывающей области, которая имеет фиксированное значение во время вызова 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)
Ответ Клаудиу довольно хороший, он использует генератор функций, но, честно говоря, ответ Пиро - хак, поскольку он превращает меня в "скрытый" аргумент со значением по умолчанию (он будет работать нормально, но не "питонно"),
Мне не понравилось, как создавались решения выше
wrappers
в петле. Примечание: python 3.xx
flist = []
def func(i):
return lambda x: x * i
for i in range(3):
flist.append(func(i))
for f in flist:
print f(2)