That's no moon... that's a budong!

 Subscribe in a reader

О замыканиях в Python

Tags: python, closure, scheme

В pythonua@c.j.r xa4a поднял интересный вопрос о замыканиях в Python.

Код, ставший камнем преткновения:

l = []
for i in range(2):
    for j in range(2):
        l.append(lambda: i + j)

По идее, список l должен содержать анонимные функции, возвращающие 0, 1, 1, 2 (порядок в данном случае не важен).

Проверяем в ipython:

In [1]: l = []
In [2]: for i in range(2):
   ...:     for j in range(2):
   ...:         l.append(lambda: i + j)
In [3]: for x in range(4):
   ...:     l[x]()
Out[3]: 2
Out[3]: 2
Out[3]: 2
Out[3]: 2

Облом.

Как одно из решений, была предложена конструкция следующего вида:

l = []
for i in range(2):
    for j in range(2):
        l.append(lambda i=i, j=j: i + j)

Проверяем.

In [3]: for x in range(4):
   ...:     l[x]()
   ...:     
   ...:     
Out[3]: 0
Out[3]: 1
Out[3]: 1
Out[3]: 2

Работает.

Это пример простого и понятного всем неправильного решения.

Почему решение неправильное? Потому что в первом случае в список добавляются анонимные функции с 0 (нулем) аргументов и двумя свободными переменными. Во втором случае в список попадают уже анонимные функции от 2-х аргументов, для которых (аргументов) указаны значения по умолчанию. Для понятности второй вариант анонимной функции можно переписать так:

lambda n=i, m=j: n + m

То есть "мы шли на Одессу, а вышли к Херсону".

Перед тем, как рассмотреть правильное решение, рассмотрим почему получается то, что получается.

In [1]: l = []
In [2]: i = 3
In [3]: f = lambda: i + 3
In [4]: f()
Out[4]: 6
In [5]: i = 5
In [6]: f()
Out[6]: 8

Очевидно, что i в анонимной функции и i снаружи от нее -- смотрят в одно и то же место памяти, хотя я ждал, что i внутри функции переопределит i снаружи.

Что делать?

Правильное решение:

def lsum(n, m):
    return lambda: n + m

l = []
for i in range(2):
    for j in range(2):
        l.append(lsum(i, j))

При использовании дополнительной функции значения i и j копируются, после её завершения ссылки на копии остаются только внутри анонимной функции, а циклы доступа к копии не имеют.

Проверяем:

In [4]: for x in range(4):
   ...:     l[x]()
Out[4]: 0
Out[4]: 1
Out[4]: 1
Out[4]: 2

В PLT Scheme ситуация аналогичная:

(define i 3)
(define f (lambda () i))
(f)

3

(set! i 5)
(f)

5

но на фоне предпочтитения рекурсии циклам неожиданности возникают значительно реже.

Мораль придумайте сами.

Published: 2009, May 07
Leschinsky Oleg