Python з нуля · Блок 3 · Тема 11

Розвилка

Досі програма була списком справ: рядок за рядком, згори вниз, без варіантів. Одне слово if — і в неї зʼявляється вибір. А разом із вибором — цілий шар правил про те, що Python вважає правдою.

Десять тем поспіль наші програми були схожі на список покупок. Інтерпретатор читав рядок, виконував його, брав наступний — і так до кінця файлу. Порядок незмінний, результат передбачуваний, жодних варіантів. Сьогодні ця пряма дорога вперше розгалужується.

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

01 / РозвилкаПерше розгалуження

Умовна інструкція складається з трьох частин: слово if, умова й двокрапка, а на наступних рядках — блок коду зі зсувом управо. Цей блок виконається лише тоді, коли умова істинна:

температура = 5 if температура < 12: print("візьми куртку")

Якщо умов кілька, їх дописують через elif (скорочення від «else if»), а те, що має статися, коли не справдилась жодна, — через else. Ось повний вигляд нашої поради:

температура = 5 # градусів за Цельсієм if температура < 0: одяг = "куртка й шапка" elif температура < 12: одяг = "светр" elif температура < 22: одяг = "сорочка" else: одяг = "футболка" print(одяг) # светр

Три речі про цю конструкцію треба зрозуміти одразу, бо на них тримається все решта.

  1. Умови перевіряються згори вниз, і перша істинна забирає все. Щойно якась гілка спрацювала, решта навіть не обчислюються. Тому в прикладі вище при температурі 5 градусів Python зробив рівно дві перевірки, а не три.
  2. Гілок виконається рівно одна. Ніколи нуль (якщо є else) і ніколи дві. Це не набір незалежних if, а одна інструкція з кількома виходами.
  3. Кожна наступна умова читається в контексті попередніх. Рядок elif температура < 12 насправді означає «від 0 включно до 12»: адже сюди ми потрапляємо, лише коли перша умова була хибною. Саме тому порядок гілок міняти не можна — постав температура < 22 першою, і решта не спрацює ніколи.

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

Порухай повзунок і подивись, які саме умови перевіряються, а які Python пропускає.

Інтерактив 1 · Гілка виконання наживо

Повзунок задає температуру. Праворуч від кожної умови — що з нею сталося насправді.

гілка
перевірок
одяг
Що читати: бірюзова позначка — умова обчислилась і дала True, рожева — обчислилась і дала False, бліде «—» означає, що до цього рядка виконання просто не дійшло. Смуга внизу — та сама логіка у вигляді відрізків числової осі: чотири гілки ділять усі можливі температури без щілин і без перекриттів.
Блок не може бути порожнім. Якщо гілка потрібна за змістом, але робити в ній поки нічого, ставлять заглушку pass — інструкцію, яка нічого не робить: if температура < -30: і на наступному рядку pass. Без неї Python скаже IndentationError: він чекав блок, а блоку немає.

02 / ВідступВідступ — це синтаксис

У більшості мов межі блоку позначають фігурні дужки, а відступи — справа смаку. У Python навпаки: дужок немає, межу блоку задає сам відступ. Це називають правилом офсайду (off-side rule), і воно не косметичне — компілятор буквально бачить у зсуві управо початок блоку, а в поверненні вліво — його кінець.

Ідея тут глибша, ніж «щоб було гарно». У мові з дужками існують дві незалежні структури: та, яку бачить компілятор (дужки), і та, яку бачить людина (відступи). Вони можуть розійтися — і тоді код читається як одне, а працює як інше. Найвідоміший приклад — помилка «goto fail» у бібліотеці безпеки Apple 2014 року: продубльований рядок виглядав частиною умови, а насправді виконувався завжди, і перевірка сертифіката тихо переставала працювати. У Python таке розходження неможливе за побудовою: те, що бачить людина, і є те, що бачить інтерпретатор.

Ціна цієї гарантії — суворість. Правила короткі, але порушувати їх не можна взагалі:

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

Інтерактив 2 · Відступ ламає програму

Перемикай варіанти — і читай справжній traceback, який видасть Python.

що сталося
рядок-винуватець
запуститься?
Пунктирні лінії — межі рівнів відступу, по чотири пробіли кожен. Зверни увагу на останній варіант: помилки немає, синтаксис бездоганний — і саме тому він найнебезпечніший. Зсунувши рядок на один рівень, ти не зламав програму, ти змінив її зміст.
Чому Python вказує не на той рядок. У варіанті «без відступу» помилку зроблено на рядку 3, але за змістом винен рядок 2 — це після нього мав початися блок. Інтерпретатор називає позицію, де він зрозумів, що щось не так, а не місце, де ти помилився. Правило виживання: побачив IndentationError — дивись на названий рядок і на рядок вище.

03 / ПорівнянняОператори порівняння

Умова — це вираз, який Python обчислює перед тим, як вирішити. Найчастіше такий вираз будують із операторів порівняння. Їх шість, і всі повертають True або False — значення типу bool:

операторщо питаєприкладрезультат
==значення однакові?температура == 5True
!=значення різні?"дощ" != "сніг"True
<менше5 < 12True
<=менше або дорівнює5 <= 5True
>більше5 > 12False
>=більше або дорівнює5 >= 12False

До них додаються ще чотири, які ти вже бачив у темах про колекції та обʼєкти: in і not in перевіряють належність ("Львів" in міста), а is та is not питають не про значення, а про тотожність обʼєктів — чи це буквально один і той самий обʼєкт у памʼяті.

Два зауваження, які економлять години налагодження. Перше: == порівнює значення, і робить це розумно між сумісними типами — 1 == 1.0 дає True, хоча зліва ціле, а справа дріб. Друге: між несумісними типами == не падає з помилкою, а спокійно повертає False. Тому "5" == 5 — це False, і якщо температура випадково приїхала з датчика рядком, умова мовчки не спрацює жодного разу. А от "5" < 5 уже кине TypeError: упорядкувати рядок і число Python не береться.

04 / ЛанцюжкиЛанцюжки порівнянь

Тепер про можливість, якої немає майже ніде. У Python можна писати математично:

0 <= температура < 12

Читається точно як у підручнику: «температура від нуля включно до дванадцяти». Python розгортає цей запис у два порівняння, зʼєднані через and:

крок 1 · що бачить інтерпретатор
0 <= температура < 12  ⟹  (0 <= температура) and (температура < 12)
крок 2 · важлива дрібниця

Середній операнд обчислюється рівно один раз. Для змінної це непомітно, але щойно там опиниться щось складніше — різниця стає реальною: 0 < len(міста) < 20 викликає len один раз, а 0 < len(міста) and len(міста) < 20 — двічі.

крок 3 · а в інших мовах

У C, Java чи JavaScript такий запис теж скомпілюється — але означатиме зовсім інше. Там оператори просто застосовуються зліва направо: спершу 0 < x дає істину або хибу, тобто 1 або 0, а потім це число порівнюється з десяткою. Одиниця менша за десять, нуль теж — отже вираз істинний завжди, за будь-якого x. Мовчазна брехня, яку компілятор не помітить.

Інтерактив 3 · Ланцюжок проти двох умов

Три прочитання одного запису. Два з них завжди збігаються, третє — ні.

ланцюжок
через and
як читає C
звернень до x
Ланцюжки можна робити довшимиa < b <= c < d цілком законний, і кожна ланка перевіряється з коротким замиканням. Технічно дозволено навіть a < b > c, але такий запис нічого не означає для читача — у справжньому коді його не пишуть.

05 / Логікаand, or, not

Одного порівняння рідко вистачає. Щоб зліпити кілька в одну умову, є три логічні оператори, і в Python вони названі словами, а не значками:

Пріоритет у них такий: спершу not, потім and, останнім or. Порівняння звʼязують сильніше за всіх трьох, тому дужки в температура > 15 and не_дощ не потрібні — інтерпретатор сам розбере, що порівняння виконується першим. А от у виразі a or b and c дужки поставити варто: працює він як a or (b and c), і не кожен читач це побачить із першого разу.

Інтерактив 4 · Таблиця істинності

Ліворуч — таблиця, праворуч — той самий закон у вигляді кола з вимикачами.

результат
Чому саме коло: and — це два вимикачі підряд, струм іде, лише коли замкнені обидва. or — два вимикачі паралельно, досить одного. not — інвертор: лампа світить, поки вимикач розімкнено. Із чотирьох рядків таблиці and істинний в одному, or — у трьох.

06 / ІстинністьЩо Python вважає правдою

Тепер найважливіше в цій темі — і те, чого немає в більшості мов. if у Python не вимагає, щоб умова була типу bool. Йому можна дати що завгодно: число, рядок, список, словник. Він запитає в цього обʼєкта: «ти істина?» — і обʼєкт відповість сам за себе.

Правило, за яким вони відповідають, коротке. Хибними вважаються:

Усе інше істинне. Абсолютно все: рядок "0", список [0], рядок із одного пробілу, відʼємні числа, порожній список усередині списку. Механіка проста: Python шукає в обʼєкта метод __bool__; якщо його немає — бере __len__ і питає, чи довжина нульова; якщо немає й цього — обʼєкт істинний беззастережно.

Перш ніж читати далі, перевір себе. Натисни на кожній картці свій прогноз — і побач, скільки разів інтуїція підвела.

Інтерактив 5 · Що є істиною

Спершу вгадай, потім перевір. Дві картки в цьому наборі підводять майже всіх.

відповіли0 / 12
вгадано0
Дві головні пастки"0" і [0]. Рядок "0" істинний, бо він непорожній: у ньому один символ, і Python не заглядає всередину, щоб дізнатись, який саме. Список [0] істинний із тієї самої причини: у ньому один елемент, а що цей елемент сам хибний — нікого не обходить.

Практичний висновок звучить так: замість if len(міста) > 0: пиши if міста:. Не заради стислості, а тому що це три різні переваги одразу. По-перше, читається як намір: «якщо міста є». По-друге, працює для будь-якого обʼєкта, навіть такого, у якого len немає взагалі. По-третє, не створює зайвого проміжного числа й зайвого порівняння.

Де це правило ламається. Масив numpy і таблиця pandas навмисно відмовляються відповідати на питання про істинність: у масиві з десяти чисел незрозуміло, чи він «істина». Тому if масив: кидає ValueError: The truth value of an array with more than one element is ambiguous. Там потрібні масив.size > 0, масив.any() або масив.all() — залежно від того, що ти насправді питаєш.

07 / ЗамиканняКоротке замикання

Оператори and і or роблять дві речі, про які підручники часто мовчать. Обидві мають наслідки в реальному коді.

Перша: вони ліниві. Обчислюють лівий операнд і одразу дивляться, чи достатньо його, щоб дати відповідь. Для and хибний лівий операнд вирішує все — правий уже нічого не змінить, тож його не обчислюють узагалі. Для or дзеркально: істинний лівий закриває питання. Це і є коротке замикання (short-circuit evaluation).

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

if вимірів != 0 and сума / вимірів > 15: print("день був теплий")

Коли вимірів нуль, ліва частина хибна — і ділення просто не відбувається. Помінявши операнди місцями, ти отримаєш ZeroDivisionError. Порядок операндів тут не стиль, а працездатність.

Друга: вони повертають не True/False, а один із самих операндів. "" or "Київ" дає рядок 'Київ', а не True. Точне правило: a and b повертає a, якщо a хибне, інакше b. a or b повертає a, якщо a істинне, інакше b. В if різниці не видно — але щойно результат кудись зберігають, вона стає головною.

Інтерактив 6 · Коротке замикання наживо

Видно, чи дійшла черга до правого операнда — і що саме повернув вираз.

результат
тип результату
обчислено операндів
замкнуло?
Пунктирна рамка означає, що операнд не обчислювався. У варіанті «and · ділення» при нулі вимірів це рятує від ZeroDivisionError, у варіанті «and · перший символ» — від IndexError. Дивись і на клітинку «тип результату»: у двох варіантах він str, а зовсім не bool.
Пастка ідіоми «значення за замовчуванням». Запис назва = введена or "Київ" підставляє запасне значення, коли введена назва порожня. Але спрацює він і тоді, коли значення цілком законне, проте хибне за істинністю: кількість = введено or 10 замінить чесний нуль на десятку. Якщо тебе цікавить саме «значення не задали», питай прямо: if введено is None.

08 / ТернарТернарний вираз

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

значення_якщо_істина if умова else значення_якщо_хиба

У нашому застосунку це виглядає так:

статус = "тепло" if температура > 15 else "прохолодно" print(статус)

Порядок слів спершу дивує: значення стоїть перед умовою, а не після. Читай уголос як речення — «візьми „тепло“, якщо температура більша за 15, інакше „прохолодно“» — і він стане природним. Головна відмінність від звичайного if: це вираз, а не інструкція. Він має значення, тому його можна присвоїти, передати аргументом, покласти в список. Звичайний if значення не має й нікуди не вкладається.

Межа розумного використання проходить швидко. Один тернарний вираз у рядку — добре. Два вкладені (a if x else b if y else c) — уже ребус, і читач витратить на нього більше часу, ніж на чотири рядки чесного if. Довгі гілки теж краще розгортати: якщо вираз не влазить у рядок, він більше не коротка форма.

09 / NoneNone і чому саме is

Датчик може не дати значення взагалі. У Python «немає значення» позначають окремим обʼєктом None — і перевіряють його особливим чином:

if температура is None: print("датчик мовчить")

Не == None, а саме is None. Причина в тому, що Noneодинак (singleton): за весь час роботи програми існує рівно один такий обʼєкт, і всі імена, яким присвоїли None, вказують на нього. Отже питання «чи це той самий обʼєкт» тут не наближення, а точна перевірка.

А == — не перевірка, а виклик методу. Оператор == звертається до методу __eq__ лівого операнда, і клас має повне право перевизначити його як завгодно. Масив numpy, наприклад, на == повертає не True/False, а цілий масив порівнянь — і умова після цього ламається. is так підмінити не можна: він порівнює адреси обʼєктів на рівні інтерпретатора, і жоден клас у це не втручається. Саме тому PEP 8 прямо вимагає is None.

І одразу найтонша плутанина теми. if not температура: і if температура is None:різні перевірки. Перша спрацює й тоді, коли датчик чесно виміряв 0 градусів, бо нуль хибний. Друга — лише коли значення справді немає. Плутанина «немає значення» з «значення дорівнює нулю або порожньому рядку» — джерело цілого класу багів, які проявляються рівно раз на сезон, коли за вікном рівно нуль.

10 / matchmatch / case

Коли гілок багато й усі вони порівнюють одну змінну з різними значеннями, ланцюжок elif стає шумним: назва змінної повторюється в кожному рядку. З версії 3.10 для цього є окрема конструкція — структурне зіставлення зі зразком (structural pattern matching). Наш застосунок читає з датчика не лише температуру, а й стан:

match стан_датчика: case "ok": повідомлення = "дані свіжі" case "cached" | "stale": повідомлення = "дані застарілі" case "off": повідомлення = "датчик вимкнено" case _: повідомлення = "невідомий стан"

Читається зверху вниз, як і elif: перший зразок, що збігся, забирає все. Символ | означає «або», а case _ — універсальний зразок, який підходить під що завгодно; це аналог else. Провалювання у наступну гілку, як у switch мови C, тут неможливе — жодних break писати не треба.

Інтерактив 7 · match проти ланцюжка elif

Один вхід, два записи. Обери стан датчика й порівняй, що спрацювало.

повідомлення
гілка elif
гілка case
Результат завжди однаковий — це два записи однієї логіки. Різниця в іншому: у лівій колонці назва стан повторюється чотири рази, і кожне повторення — місце для друкарської помилки. У правій вона названа один раз, у заголовку.

Але match — не просто гарніший switch. Слово «структурне» в назві означає, що зразок описує форму даних, а не лише значення:

match вимір: case [температура, вологість]: print("пара значень:", температура, вологість) case {"t": температура}: print("словник із ключем t:", температура) case int(x) if x > 40: print("підозріло гаряче:", x)

Перший зразок збігається зі списком із двох елементів і одразу дає їм імена. Другий — зі словником, у якому є ключ "t". Третій показує сторожа (guard): додаткову умову if, яка перевіряється вже після того, як форма збіглася. Це те, чого ланцюжок elif не вміє без розпакування вручну.

Пастка, на яку наступають усі. Голе імʼя в зразку — це не порівняння, а захоплення. Якщо написати case ГРАНИЦЯ:, сподіваючись порівняти зі своєю константою, зразок збігнеться з будь-чим, а константа буде мовчки перезаписана. Порівнювати можна лише з літералом (case 40:) або з «крапковим» імʼям (case Пороги.ГАРЯЧЕ:). Це найчастіша причина того, що match «завжди йде в першу гілку».

Коли що брати? match виграє, коли гілки розрізняють форму даних або перелічують багато конкретних значень однієї змінної. Ланцюжок elif залишається кращим, коли умови — це діапазони (температура < 12) або взагалі різні питання про різні змінні. Наш вибір одягу за температурою на match перевести не вийде без потворних сторожів — і не треба.

11 / ПасткиПастки, на яких горять усі

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

1. if x == True

Виглядає педантично й правильно, а насправді звужує перевірку. if x: запитує «чи істинний x» — і на списку [1, 2] дасть істину. if x == True: запитує «чи дорівнює x обʼєкту True» — і на тому самому списку дасть хибу. Збігаються ці два записи лише коли x — це буквально True або 1. Пиши просто if x:, а якщо хочеш заперечення — if not x:.

2. if a == 1 or 2:

Найпоширеніша помилка новачка, і найпідступніша, бо читається як бажане. Python бачить тут (a == 1) or 2. Двійка — непорожнє ненульове число, тобто істина, отже вся умова істинна завжди, за будь-якого a. Правильно — if a == 1 or a == 2:, а ще краще if a in (1, 2):.

3. if x is 1

Тут ховається пастка з теми про обʼєкти. CPython заздалегідь створює й кеширує невеликі цілі — від −5 до 256. Тому int("256") is int("256") дасть True, а int("257") is int("257") — уже False: це два різні обʼєкти з однаковим значенням. Код «працює» на маленьких числах і ламається на великих. Python 3.8+ попереджає про це прямо: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?. Правило просте: is — лише для None, True, False й перевірки тотожності обʼєктів. Для значень — ==.

4. if сума == 0.3

Пряме продовження теми про числа. 0.1 + 0.2 == 0.3 дає False, бо збережені числа — не рівно десятина й не рівно три десятих. Умова, яка порівнює дроби через ==, рано чи пізно не спрацює на цілком правильних даних. Бери math.isclose(сума, 0.3), а для порівняння з нулем — math.isclose(x, 0.0, abs_tol=1e-9).

12 / ПідсумокЩо з цього забрати

Шість пунктів, яких вистачить, щоб писати умови без сюрпризів:

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

Що далі. Розгалуження навчило програму обирати — але кожна гілка досі виконується рівно один раз. У наступній темі зʼявляються цикли: for і while. І перше, що ти там побачиш, — умову з цієї лекції в новій ролі: у while вона перевіряється не один раз, а перед кожним повтором. А в practice.ipynb ти зараз побудуєш перевірку даних із датчика, класифікатор температури й переведеш ланцюжок elif на match — і зламаєш відступ навмисне, щоб побачити traceback власними очима.

Далі в темі

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