А ви можете вирішити ці три (оманливо) прості задачі на Python?

Alex Alex 12 липня 2020
А ви можете вирішити ці три (оманливо) прості задачі на Python?

З самого початку свого шляху як розробника програмного забезпечення я дуже любив порпатися в нутрощах мов програмування. Мені завжди було цікаво, як влаштована та чи інша конструкція, як працює та чи інша команда, що під капотом у синтаксичного цукру і т. п. Нещодавно мені на очі потрапила цікава стаття з прикладами того, як не завжди очевидно працюють mutable - і immutable-об'єкти в Python. На мій погляд, ключове — це те, як змінюється поведінка коду в залежності від типу даних що використовується, при збереженні ідентичної семантики та використовуваних мовних конструкціях. Це відмінний приклад того, що думати треба не тільки при написанні, але і при використанні.

Спробуйте вирішити ці три завдання, а потім звіртеся з відповідями наприкінці статті.

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

Перша задача

Є кілька змінних:

x = 1
y = 2
l = [x, y]
x += 5

a = [1]
b = [2]
s = [a, b]
a.append(5)

Що буде виведено на екран при друку l та s?

Друга задача

Визначимо просту функцію:

def f(x, s=set()):
     s.add(x)
     print(s)

Що буде, якщо викликати:

>>f(7)
>>f(6, )
>>f(2)

Третя задача

Визначимо дві прості функції:

def f():
    l = [1]
    def inner(x):
        l.append(x)
        return l
    return inner

def g():
    y = 1
    def inner(x):
        y += x
        return y
    return inner

Що ми отримаємо після виконання цих команд?

>>f_inner = f()
>>print(f_inner(2))

>>g_inner = g()
>>print(g_inner(2))

Наскільки ви впевнені у своїх відповідях? Нумо перевірмо вашу правоту.

Розв'язання першої задачі

>>print(l)
[1, 2]

>>print(s)
[[1, 5], [2]]

Чому другий список реагує на зміну свого першого елемента a.append(5), а перший список повністю ігнорує таку ж зміну x+=5?

Розв'язання другої задачі

Подивимося, що станеться:

>>f(7)

>>f(6, )

>>f(2)

Стривайте, хіба останнім результатом не повинно бути ?

Рішення третього завдання

Результат буде таким:

>>f_inner = f()
>>print(f_inner(2))
[1, 2]

>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable 'y' referenced before assignment

Чому g_inner(2) не видала 3? Чому внутрішня функція f() пам'ятає про зовнішню області видимості, а внутрішня функція g() не пам'ятає? Вони ж практично ідентичні!

Пояснення

Що якщо я скажу вам, що всі ці приклади дивної поведінки пов'язані з розходженням між змінними та незмінними об'єктами в Python?

Змінювані об'єкти, такі як списки, множини або словники, можуть бути змінені на місці. Незмінні об'єкти, такі як числові та рядкові значення, кортежі, не можуть бути змінені; їх «зміна» призведе до створення нових об'єктів.

Пояснення першого завдання

x = 1
y = 2
l = [x, y]
x += 5

a = [1]
b = [2]
s = [a, b]
a.append(5)

>>print(l)
[1, 2]

>>print(s)
[[1, 5], [2]]

Оскільки x незмінна, операція x+=5 не змінює початковий об'єкт, а створює новий. Але перший елемент списку все ще посилається на вихідний об'єкт, тому його значення не змінюється.

Оскільки a змінюваний об'єкт, то команда a.append(5) змінює вихідний об'єкт (а не створює новий), і список s «бачить» зміни.

Пояснення другої задачі

def f(x, s=set()):
    s.add(x)
    print(s)
>>f(7)

>>f(6, )

>>f(2)

З першими двома результатами все зрозуміло: перше значення 7 додається до початкової порожньої множини і виходить ; потім значення 6 додається до множини і виходить .

А потім починаються дивацтва. Значення 2 не додається до порожньої множини, а до . Чому? Початкове значення опціонального параметра s обчислюється тільки один раз: при першому виклику s буде ініціалізована як порожня множина. А оскільки вона змінювана, після виклику f(7) вона буде змінена "на місці". Другий виклик f(6, ) не вплине на значення за замовчуванням: його замінює множина , тобто  є іншою змінною. Третій виклик f(2) використовує ту ж змінну s, що використовувалася при першому виклику, але вона не ініціалізується повторно як порожня множина, а замість цього береться її попереднє значення .

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

def f(x, s=None):
    if s is None:
        s = set()
    s.add(x)
    print(s)

Пояснення третьої задачі 

def f():
   l = [1]
   def inner(x):
       l.append(x)
       return l
   return inner

def g():
   y = 1
   def inner(x):
       y += x
       return y
   return inner

>>f_inner = f()
>>print(f_inner(2))
[1, 2]

>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable ‘y’ referenced before assignment

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

Чому так відбувається? Коли ми виконуємо l.append(x), змінюється змінюваний об'єкт, створений при визначенні функції. Але змінна l все ще посилається на стару адресу в пам'яті. Однак спроба змінити незмінну змінну у другій функції y += x призводить до того, що y починає посилатися на іншу адресу в пам'яті: вихідна y буде забута, що приведе до помилки UnboundLocalError.

Висновок

Різниця між змінними та незмінними об'єктами в Python дуже важлива. Уникайте дивної поведінки, описаної в цій статті. Особливо:

  • Не використовуйте за замовчуванням змінювані аргументи.
  • Не намагайтеся змінювати фіксовані змінні-змикання у внутрішніх функціях.

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.