З самого початку свого шляху як розробника програмного забезпечення я дуже любив порпатися в нутрощах мов програмування. Мені завжди було цікаво, як влаштована та чи інша конструкція, як працює та чи інша команда, що під капотом у синтаксичного цукру і т. п. Нещодавно мені на очі потрапила цікава стаття з прикладами того, як не завжди очевидно працюють 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 дуже важлива. Уникайте дивної поведінки, описаної в цій статті. Особливо:
- Не використовуйте за замовчуванням змінювані аргументи.
- Не намагайтеся змінювати фіксовані змінні-змикання у внутрішніх функціях.
Ще немає коментарів