Как работают лексические замыкания?

пока я исследовал проблему, которую я имел с лексическими замыканиями в коде 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 как о макросе, который выполняет следующие шаги:

  1. определить лямда принимая один параметр (i) с телом, определенным телом цикла,
  2. немедленный вызов этой лямбды с соответствующими значениями 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 в "скрытый" аргумент со значением по умолчанию (он будет работать нормально, но это не "питон").