Які функції (лямбда) функції захоплення закривають?


249

Нещодавно я почав грати з Python, і мені подобалося щось особливе в тому, як працюють закриття. Розглянемо наступний код:

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

Він будує простий масив функцій, які беруть один вхід і повертають цей вхід, доданий числом. Функції побудовані в forциклі , де итератор iпрацює від 0до 3. Для кожного з цих чисел створюється lambdaфункція, яка захоплює iта додає її до входу функції. Останній рядок викликає другу lambdaфункцію з 3параметром. На мій подив, результат був 6.

Я очікував 4. Мої міркування були такі: у Python все є об'єктом, тому кожна змінна є важливим вказівником на нього. Під час створення lambdaзакриття для i, я очікував, що він збереже вказівник на цілий об'єкт, на який вказує даний момент i. Це означає, що при iпризначенні нового цілого об'єкта він не повинен здійснювати попередньо створені закриття. На жаль, огляд addersмасиву всередині налагоджувача показує, що це так. Всі lambdaфункції відносяться до останнього значенням i, 3, що призводить до adders[1](3)повернення 6.

Що змушує мене замислитися над наступним:

  • Що саме фіксує закриття?
  • Що є найелегантнішим способом переконати lambdaфункції захоплення поточного значення iтаким чином, що це не вплине при iзміні його значення?

35
У мене була проблема в коді інтерфейсу. Накинь мені горіхи. Хитрість полягає в тому, щоб пам’ятати, що петлі не створюють нової області.
detly

3
@TimMB Як iзалишається простір імен?
detly

3
@detly Ну, я збирався сказати, що print iпісля циклу не буде працювати. Але я перевірив це на собі і тепер бачу, що ти маєш на увазі - це працює. Я не мав уявлення, що змінні циклу затримуються за тілом циклу в python.
Тім МБ

1
@TimMB - Так, це я мав на увазі. Те ж саме для if, with, і tryт.д.
detly

13
Про це йдеться в офіційному поширеному питанні Python, у розділі Чому лямбди, визначені в циклі з різними значеннями, повертають один і той же результат? , як з поясненням, так і зі звичайним рішенням.
abarnert

Відповіді:


161

На ваше друге питання відповіли, але щодо вашого першого:

що саме фіксує закриття?

Оцінка в Python є динамічною та лексичною. Закриття завжди запам'ятовуватиме ім'я та область змінної, а не об'єкт, на який вона вказує. Оскільки всі функції у вашому прикладі створені в одній області застосування і використовують одне і те ж ім'я змінної, вони завжди посилаються на одну і ту ж змінну.

EDIT: Що стосується вашого іншого питання, як подолати це, вам приходять у голову два шляхи:

  1. Найбільш стислий, але не зовсім рівноцінний спосіб - той, який рекомендував Адрієн Пліссон . Створіть лямбда з додатковим аргументом і встановіть значення додаткового аргументу за замовчуванням об'єкту, який ви хочете зберегти.

  2. Трохи більше багатослівного, але менш хакітного було б створювати новий діапазон кожного разу, коли ви створюєте лямбда:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5

    Область тут створюється за допомогою нової функції (лямбда, для стислості), яка пов'язує свій аргумент і передає значення, яке потрібно прив’язати як аргумент. У реальному коді, однак, ви, швидше за все, будете мати звичайну функцію замість лямбда, щоб створити нову область:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]

1
Макс, якщо ви додасте відповідь на моє інше (простіше запитання), я можу позначити це як прийняту відповідь. Дякую!
Боаз

3
У Python є статичне масштабування, а не динамічне масштабування. Це просто всі змінні є посиланнями, тому коли ви встановлюєте змінну новому об'єкту, сама змінна (посилання) має те саме розташування, але вказує на щось інше. те саме відбувається в схемі, якщо ви set!. дивіться тут, що таке динамічна сфера дійсно: voidspace.org.uk/python/articles/code_blocks.shtml .
Клавдіу

6
Варіант 2 схожий на те, що функціональні мови назвали б "Curried function".
Crashworks

205

ви можете змусити захопити змінну за допомогою аргументу зі значенням за замовчуванням:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

ідея полягає в тому, щоб оголосити параметр (розумно названий i) і надати йому значення за замовчуванням змінної, яку ви хочете захопити (значення i)


7
+1 для використання значень за замовчуванням. Оцінювання, коли визначається лямбда, робить їх ідеальними для цього використання.
курінь

21
+1 також тому, що це рішення, затверджене офіційним поширеним запитанням .
abarnert

23
Це дивно. Однак поведінка Python за замовчуванням не є.
Сесіль Карі

1
Це просто не здається хорошим рішенням, хоча ... ви насправді змінюєте підпис функції лише для того, щоб захопити копію змінної. А також ті, хто викликає функцію, можуть зіпсуватись зі змінною i, правда?
Девід Калланан

@DavidCallanan ми говоримо про лямбда: тип спеціальної функції, яку ти зазвичай визначаєш у своєму власному коді, щоб підключити дірку, а не те, чим ти ділишся через весь sdk. якщо вам потрібен більш сильний підпис, ви повинні використовувати справжню функцію.
Адрієн Пліссон

33

Для повноти ще одна відповідь на ваше друге запитання: Ви можете використовувати часткове в модулі functools .

Імпортуючи додаток від оператора, як запропонував Кріс Лутц, прикладом стає:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

24

Розглянемо наступний код:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Я думаю, що більшість людей взагалі не знайдуть це заплутаним. Це очікувана поведінка.

Отже, чому люди думають, що це було б інакше, коли це робиться в циклі? Я знаю, що я помилявся сам, але не знаю, чому. Це петля? А може лямбда?

Зрештою, цикл - це лише скорочена версія:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

11
Це цикл, оскільки в багатьох інших мовах цикл може створити нову область.
detly

1
Ця відповідь хороша тим, що вона пояснює, чому доступ до однієї iзмінної використовується для кожної функції лямбда.
Девід Калланан

3

Відповідаючи на ваше друге запитання, найелегантнішим способом зробити це було б використовувати функцію, яка приймає два параметри замість масиву:

add = lambda a, b: a + b
add(1, 3)

Однак використовувати лямбда тут трохи нерозумно. Python дає нам operatorмодуль, який забезпечує функціональний інтерфейс для основних операторів. Ламбда вище має зайві накладні витрати лише для виклику оператора додавання:

from operator import add
add(1, 3)

Я розумію, що ти граєш, намагаючись вивчити мову, але не можу уявити ситуацію, я би використовував масив функцій, де дивацтва Дитона Питона заважає.

Якщо ви хочете, ви можете написати невеликий клас, який використовує ваш синтаксис індексації масиву:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

2
Кріс, звичайно, наведений вище код не має нічого спільного з моєю початковою проблемою. Він побудований для того, щоб проілюструвати мою думку простим способом. Звичайно безглуздо і безглуздо.
Боаз

3

Ось новий приклад, який висвітлює структуру даних та вміст закриття, щоб допомогти уточнити, коли контекст, що вкладається, "збережений".

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

Що є у закритті?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

Зокрема, my_str не знаходиться у закритті f1.

Що є у закритті f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Зауважте (з адрес пам'яті), що обидва закриття містять однакові об'єкти. Отже, можна починати думати про функцію лямбда як про посилання на область застосування. Однак, мій_str не знаходиться у закритті для f_1 чи f_2, і я не знаходиться у закритті для f_3 (не показано), що говорить про те, що самі об'єкти закриття є різними об'єктами.

Чи самі об'єкти закриття є одним і тим же об'єктом?

>>> print f_1.func_closure is f_2.func_closure
False

Примітка. Результат int object at [address X]>змусив мене думати, що закриття зберігає [адресу X] AKA посилання. Однак [адреса X] зміниться, якщо змінна буде перепризначена після оператора lambda.
Джефф
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.