Вопрос

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top