Python з нуля · Блок 1 · Тема 03

Ярлик, а не коробка

Майже кожен підручник каже: «змінна — це коробка, куди кладуть значення». У Python це неправда. І саме з цієї неправди згодом виростають найдивніші баги.

У попередній темі ти вже запустив свою першу програму й побачив, що інтерпретатор виконує рядки один за одним. Тепер розберемо те, з чого складається будь-яка програма взагалі: дані та імена для них. Тема виглядає елементарною — рівно доти, доки твоя програма не почне змінювати список, якого ти не чіпав. Тоді виявиться, що модель «коробки зі значенням» у голові була хибною, і чинити доведеться не код, а саме уявлення.

Тож почнімо одразу з правильної моделі, а не з тієї, яку потім треба переучувати.

01 / ПрисвоєнняЩо насправді робить знак «=»

Напишімо найкоротшу можливу програму:

повідомлення = "привіт"

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

  1. Спершу праворуч від = обчислюється вираз. У памʼяті зʼявляється обʼєкт — рядок «привіт». Він існує сам по собі й нічого не знає про імена.
  2. Потім імʼя повідомлення прикріплюється до цього обʼєкта, як наліпка на валізу.

Різниця не філософська. Коробка вміщує одне значення, і два імені — це дві коробки. Ярлик же можна почепити на вже наявний обʼєкт, і тоді два імені вкажуть на один обʼєкт. Усе, що ти зробиш через одне імʼя, миттєво «побачить» друге.

Саме тому b = a — це не копіювання. Це другий ярлик. Пройди інтерактив крок за кроком і подивись, скільки обʼєктів насправді створено.

Інтерактив 1 · Імена як ярлики

Натискай «наступний крок». Ліворуч — імена, праворуч — обʼєкти в памʼяті. Стрілка = ярлик.

a
b
a is b
a is c
Головне з цієї картинки: після b = a обʼєкт лишився один, а стрілок на нього стало дві. Тому b.append(3) змінює те, що бачить і a. А c — уже інший обʼєкт з таким самим вмістом: a == c дає True, a is c — False.
Термінологія. Слово «змінна» в Python лишилося за традицією, але точніше казати імʼя (name) і звʼязування імені (binding). У документації Python так і написано: оператор присвоєння звʼязує імʼя з обʼєктом. Не «записує в памʼять за адресою імені» — це модель мов на кшталт C, і вона тут не працює.

02 / ОбʼєктТип, значення, ідентичність

Якщо імена — лише ярлики, то вся суть у тому, на що вони чіпляються. У Python усе є обʼєктом: число, рядок, список, навіть функція й сам тип. І кожен обʼєкт має рівно три речі:

властивістьщо цеяк побачити
типдо якого сорту належить обʼєкт і що з ним можна робитиtype(x)
значеннявласне вміст: число 7, текст, елементи спискуprint(x)
ідентичність«який саме» це обʼєкт — його особистістьid(x)

Перші дві зрозумілі. Третя — найважливіша для цієї теми. Ідентичність (identity) — це відповідь на питання «це той самий обʼєкт чи інший, просто схожий?». Два різні аркуші з однаковим текстом мають однакове значення, але це два аркуші.

Функція id(x) повертає ціле число — унікальний номер обʼєкта, поки той живий. Він не змінюється за все життя обʼєкта. Тому перевірка «чи це той самий обʼєкт» зводиться до порівняння id(a) == id(b).

Для тих, хто вже пише код. У CPython — основній реалізації мови — id() повертає буквально адресу обʼєкта в памʼяті. Це деталь реалізації, а не обіцянка мови: в інших реалізаціях (PyPy, Jython) число може означати щось інше. Звідси й наслідок: після знищення обʼєкта його номер може дістатися новому обʼєкту. Порівнювати збережені «старі» id() з новими — надійний спосіб отримати загадковий баг.

03 / ТипізаціяТип живе в обʼєкті, а не в імені

У багатьох мовах тип оголошують разом зі змінною: «ось ціле число n, і нічого, крім цілого, туди не покласти». Python влаштований інакше. Імʼя не має типу взагалі — воно просто стрілка. Тип має обʼєкт на іншому кінці стрілки.

Тому одне й те саме імʼя може сьогодні вказувати на число, а через рядок — на список, і мова не заперечить. Це і називають динамічною типізацією. Покрути повзунок: імʼя лишається те саме, а тип змінюється разом з обʼєктом.

Інтерактив 2 · Одне імʼя, різні обʼєкти

Повзунок міняє те, що стоїть праворуч від =. Дивись, у кого змінюється тип.

type()
змінюваний?
id() — номер обʼєкта
Що видно: ліворуч нічого не змінюється — імʼя значення однакове для всіх шести випадків, у нього немає й не може бути типу. Праворуч щоразу інший обʼєкт: інший тип, інший номер, інші правила гри.

Динамічна типізація дає свободу, але не скасовує типів. Спроба додати число до рядка не «якось приведеться», а зупинить програму:

>>> "вік: " + 30 TypeError: can only concatenate str (not "int") to str

Це нормально й навіть добре: помилка спіймана одразу, з точною вказівкою, що з чим не склеїлось. Правильно так: "вік: " + str(30).

04 / НезмінністьНезмінні та змінювані обʼєкти

Тепер найважливіший поділ у всій темі. Обʼєкти Python бувають двох сортів:

Звідси головний наслідок. Коли ти пишеш y = y + 1, число не змінюється — не може змінитися. Python рахує y + 1, отримує новий обʼєкт і перечіпляє на нього ярлик y. Старий обʼєкт стоїть неторканий, і якщо на нього дивиться ще чиєсь імʼя, воно нічого не помітить.

А коли ти пишеш список.append(6), нічого нового не створюється: змінюється сам обʼєкт. Ярлики нікуди не рухаються — але всі, хто на нього вказує, бачать нове значення. Ось ті самі три кроки для числа й для списку поруч:

Інтерактив 3 · Незмінний проти змінюваного

Ліворуч число, праворуч список. Дії однакові — результат протилежний.

x
y
s
t
Ключ до всієї теми: ліворуч на третьому кроці рухається стрілка — обʼєкт 5 лишається собою. Праворуч стрілки стоять на місці, а змінюється обʼєкт. Перше безпечне завжди; друге видно всім, хто тримає ярлик.
Пастка з кортежем. tuple незмінний — але лише на один рівень. Кортеж пара = ([1, 2], "текст") не дозволить замінити свої елементи, проте список усередині нього змінити можна: пара[0].append(3) спрацює. Незмінність кортежа означає «набір посилань зафіксовано», а не «все всередині заморожено».

05 / Порівнянняis проти ==

Два оператори, які новачки плутають щотижня, а модель ярликів робить очевидними:

Два списки з однаковими числами дають == True і is False. Правило просте: у 99% випадків тобі потрібен ==. Оператор is застосовують майже виключно для перевірки на None: if результат is None. Так пишуть, бо None існує в програмі в єдиному примірнику, і порівняння за ідентичністю тут і точніше, і швидше.

Інтерактив 4 · Однакові чи ті самі

Угорі — обрана пара імен. Унизу — кеш малих цілих чисел: пересунь повзунок через 256.

a == b
a is b
Унизу: для a = n і b = n результат a is b залежить від самого числа. CPython наперед створює всі цілі від −5 до 256 і роздає їх повторно — тому дрібні числа виявляються «тими самими». Це оптимізація, а не правило мови.
Ніколи не покладайся на це. a is b для двох однакових чисел — випадковість реалізації. Межа 256 може змінитися, а той самий код у файлі й у REPL дасть різні відповіді: усередині одного блоку коду компілятор ще й склеює однакові сталі. Порівнюєш числа чи рядки — пиши ==. Завжди.

06 / ОбмінПомінятися значеннями

Класична задача: у a лежить одне, у b — інше, треба поміняти місцями. Наївна спроба «в лоб» ламається, і модель ярликів пояснює чому: після a = b перший обʼєкт лишається без жодного імені й губиться назавжди.

Рятує третє імʼя — воно притримує обʼєкт, поки той не знадобиться. Але в Python є коротший спосіб: a, b = b, a. Спершу обчислюється весь правий бік — Python збирає з нього тимчасовий кортеж, — і лише потім роздає імена. Тому нічого загубитись не встигає. Перемикай режими й дивись, куди дінеться одиниця:

Інтерактив 5 · Три способи обміняти значення

Стартуємо з a = 1, b = 2. Мета — щоб стало навпаки.

a
b
Порівняй фінал: у перших двох способах виходить a = 2, b = 1. У третьому обидва імені осідають на двійці, а одиниця зникає — її вже нічим не дістати.

07 / ІменаЯк називати й чому це не дрібниця

Правила мови короткі: імʼя складається з літер, цифр і підкреслення, не починається з цифри, регістр важливий (ціна й Ціна — різні імена). Українські літери дозволені, і ціна = 100 — цілком робочий код.

Далі йдуть домовленості спільноти, і вони важать більше за правила:

Використати імʼя до присвоєння не вийде — Python не має «порожніх» імен за замовчуванням:

>>> print(підсумок) NameError: name 'підсумок' is not defined

Читається буквально: такого ярлика ще ні на чому не висить. Найчастіша причина — одруківка в імені, друга за частотою — спроба скористатися результатом до того, як його порахували.

08 / ВидаленняЩо робить del

Команда del a звучить як «видалити обʼєкт», але робить рівно протилежне до очікуваного: вона відчіпляє ярлик. Обʼєкт при цьому може лишитися жити — якщо на нього вказує ще хтось.

Хто ж тоді прибирає сміття? CPython рахує, скільки посилань веде на кожен обʼєкт. Це підрахунок посилань (reference counting). Прикріпили нове імʼя — лічильник +1; відчепили — −1. Щойно лічильник падає до нуля, памʼять звільняється негайно, без жодної паузи й без окремого «збирача». Пройди чотири кроки й подивись на лічильник:

Інтерактив 6 · Лічильник посилань і del

Число в кутку обʼєкта — скільки імен на нього вказує.

посилань
обʼєкт
Висновок: del a на третьому кроці не знищив список — на нього ще дивилось b. Обʼєкт помер лише тоді, коли зникло останнє імʼя. Те саме відбувається без del: щойно ти перечіпляєш ярлик на інший обʼєкт, старий втрачає посилання.
Для профі: чому лічильника недостатньо. Два списки, що посилаються один на одного, утворюють цикл: жоден лічильник ніколи не дійде до нуля, хоч ззовні на них не дивиться ніхто. Саме заради таких випадків у CPython є ще й окремий циклічний збирач (модуль gc), який періодично обходить памʼять і знаходить недосяжні групи обʼєктів. Функція sys.getrefcount(x) покаже поточний лічильник — тільки памʼятай, що вона сама на мить створює зайве посилання, тому число завжди на одиницю більше за очікуване.

09 / ПасткаСпільний список, який памʼятає все

Наостанок — найвідоміша пастка Python, і вона повністю пояснюється ярликами. Забігаючи наперед: функція — це спосіб дати імʼя шматку коду, щоб виконувати його багато разів (докладно про них буде далі в курсі). Аргументу можна задати значення за замовчуванням. І ось тут:

def додати(що, кошик=[]): кошик.append(що) return кошик

Виглядає невинно: «якщо кошика не передали — візьми порожній». Насправді список [] створюється один раз, коли Python читає рядок def, і назавжди залишається прикріпленим до самої функції. Кожен виклик без аргументу отримує не новий порожній список, а той самий — уже з усім, що накидали попередні виклики. Натисни «викликати» тричі:

Інтерактив 7 · Змінюване значення за замовчуванням

р1, р2, р3 — те, що повернули перший, другий і третій виклики.

викликів0
обʼєктів1
Ліворуч у режимі «пастка» усі три результати — стрілки на один список, і в ньому три елементи. У правильному варіанті кожен виклик створює свій обʼєкт, тому в кожному результаті рівно один елемент.

Ліки стандартні: значенням за замовчуванням ставлять None — незмінний одинак, який неможливо зіпсувати, — а порожній список створюють усередині функції, на кожен виклик свій. Загальне правило звучить так: змінюваний обʼєкт ніколи не має бути значенням за замовчуванням.

10 / ПідсумокЩо забрати з теми

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

Далі буде цікавіше: у темі 04 візьмемо найпростіший на вигляд тип — числа — і зʼясуємо, чому 0.1 + 0.2 не дорівнює 0.3 навіть у бездоганно написаній програмі. Ти вже бачив цей результат в інтерактиві 4; тепер розберемо, звідки він береться.

Далі в практиці. У practice.ipynb ти передбачиш результат восьми присвоєнь до запуску, звіриш свої відповіді з id() до і після зміни, зловиш пастку спільного списку власними руками й навчишся робити чесну копію.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.