Практика · Умови¶
Лекція: lecture.html · Тест: quiz.html · Домашнє: homework.md
Продовжуємо наскрізний приклад лекції: маленький застосунок, який читає з датчика температуру повітря й радить, що вдягнути.
Що зробимо:
- перевіримо дані з датчика, перш ніж їм повірити;
- напишемо ланцюжок
if / elif / elseі подивимось, скільки умов він реально обчислює; - зламаємо відступ навмисне й прочитаємо справжній
IndentationError; - переберемо всі хибні значення й переконаємось, що наше правило збігається з
bool(); - побачимо коротке замикання в дії — і що воно повертає не
True/False; - з'ясуємо, чим
is Noneвідрізняється відnot x; - перепишемо ланцюжок
elifнаmatch / caseі доведемо черезassert, що це те саме.
Циклів і функцій тут ще немає — вони будуть у темах 12 і 14. Усе, що нижче, — рівний код згори вниз, як у справжньому скрипті.
1 · Дані, з якими працюємо¶
Спершу опишемо одне вимірювання з датчика. Це чотири звичайні змінні — нічого нового,
але саме на них триматиметься вся решта зошита. Зверни увагу на стан_датчика: у житті
датчик не завжди відповідає «все добре», і програма мусить це передбачити.
температура = 5.0 # градусів за Цельсієм
вологість = 72 # відсотків
стан_датчика = "ok" # ok / cached / stale / off
місто = "" # користувач ще не ввів назву — порожній рядок
print("температура:", температура)
print("вологість:", вологість)
print("стан датчика:", стан_датчика)
print("місто:", repr(місто)) # repr показує лапки, тому порожній рядок видно
температура: 5.0 вологість: 72 стан датчика: ok місто: ''
2 · Перевірка даних: чи можна цьому вірити¶
Перше, що робить будь-яка серйозна програма з чужими даними, — перевіряє їх. Побутовий термометр не показує −90 °C і не показує +70 °C; якщо таке прийшло, це поломка датчика, а не погода.
Тут працює ланцюжок порівнянь із лекції: -90 <= температура <= 70 читається як у
підручнику й обчислює температура рівно один раз.
# межі беремо з фізики, а не зі стелі: рекорди Землі — приблизно -89 і +57 °C
дані_коректні = -90 <= температура <= 70
if дані_коректні:
print("✅ значення правдоподібне:", температура, "°C")
else:
print("❌ датчик збожеволів:", температура, "°C")
# ланцюжок і його розгортка через and — це буквально одне й те саме
розгортка = (-90 <= температура) and (температура <= 70)
assert дані_коректні == розгортка, "ланцюжок мусить збігатися з формою через and"
print("ланцюжок == розгортка через and:", дані_коректні == розгортка)
✅ значення правдоподібне: 5.0 °C ланцюжок == розгортка через and: True
3 · Найпростіша розвилка¶
Один if і один else. Двокрапка в кінці рядка обіцяє блок, а відступ у чотири пробіли
цей блок утворює. Поки що це вся конструкція.
if температура < 12:
порада = "візьми куртку"
else:
порада = "куртка не потрібна"
print(порада)
візьми куртку
4 · Ланцюжок elif: чотири гілки замість двох¶
Тепер повний ланцюжок з лекції. Головне, що варто побачити своїми очима: умови
перевіряються згори вниз, і перша істинна забирає все. Щоб це не було на віру,
порахуємо перевірки вручну — змінна перевірок збільшується в кожній гілці рівно на
стільки умов, скільки Python встиг обчислити, поки до неї дійшов.
if температура < 0:
одяг = "куртка й шапка"
перевірок = 1
elif температура < 12:
одяг = "светр"
перевірок = 2
elif температура < 22:
одяг = "сорочка"
перевірок = 3
else:
одяг = "футболка"
перевірок = 3 # else не є умовою, тому обчислено все ті самі три
print(f"при {температура} °C беремо: {одяг}")
print(f"Python обчислив умов: {перевірок} з 3")
assert одяг == "светр", "5 °C — це діапазон від 0 до 12, тобто светр"
assert перевірок == 2, "третю умову Python не мав навіть чіпати"
print("✅ гілка й кількість перевірок збігаються з очікуваними")
при 5.0 °C беремо: светр Python обчислив умов: 2 з 3 ✅ гілка й кількість перевірок збігаються з очікуваними
Той самий блок, але для спекотного дня. Код дослівно той самий — змінилося лише вхідне значення, а виконалася вже інша гілка. Це і є розгалуження: одна програма, різні шляхи.
температура_спека = 30.0
if температура_спека < 0:
одяг_спека = "куртка й шапка"
elif температура_спека < 12:
одяг_спека = "светр"
elif температура_спека < 22:
одяг_спека = "сорочка"
else:
одяг_спека = "футболка"
print(f"при {температура_спека} °C беремо: {одяг_спека}")
assert одяг_спека == "футболка", "30 °C не менше за 22, отже спрацював else"
print("✅ спрацював else — жодна з трьох умов не справдилась")
при 30.0 °C беремо: футболка ✅ спрацював else — жодна з трьох умов не справдилась
5 · Ламаємо відступ навмисне¶
Наступні дві клітинки не виконаються — і це задумано. Traceback теж навчальний матеріал, тому подивись на нього уважно: Python завжди називає номер рядка й тип помилки.
Перший випадок: після двокрапки блоку немає взагалі.
if температура < 12:
print("холодно")
Cell In[6], line 2 print("холодно") ^ IndentationError: expected an indented block after 'if' statement on line 1
Другий випадок: рядок зсунувся глибше, ніж мав, хоча жодного нового блоку не відкривали.
Зверни увагу на різницю в повідомленнях: expected an indented block — блоку бракує,
unexpected indent — блок зайвий.
if температура < 12:
print("холодно")
print("і вітряно")
Cell In[7], line 3 print("і вітряно") ^ IndentationError: unexpected indent
А тепер найпідступніше: правильний синтаксис і неправильний зміст. Помилки немає,
програма працює — просто робить не те. Останній рядок стоїть усередині if, тому при
30 градусах не друкується нічого.
надруковано = [] # сюди складатимемо все, що програма встигла сказати
if температура_спека < 12:
надруковано.append("холодно")
надруковано.append("кінець") # цей рядок теж усередині if — через відступ
print("вивід програми:", надруковано)
assert надруковано == [], "при 30 °C умова хибна, отже не виконався жоден рядок блоку"
print("✅ зсунутий на чотири пробіли рядок змінив поведінку, а не зламав програму")
вивід програми: [] ✅ зсунутий на чотири пробіли рядок змінив поведінку, а не зламав програму
6 · Що Python вважає правдою¶
Правило з лекції коротке: хибні — False, None, нулі й порожні колекції. Усе інше
істинне. Перевіримо його на найпідступніших значеннях і звіримо з вбудованим bool() —
це наша версія перевірки «наша реалізація = еталонна».
print("хибні значення")
print(" bool(0) =", bool(0))
print(" bool(0.0) =", bool(0.0))
print(" bool('') =", bool(""))
print(" bool([]) =", bool([]))
print(" bool({}) =", bool({}))
print(" bool(None) =", bool(None))
print()
print("а ось ці двоє підводять майже всіх")
print(" bool('0') =", bool("0"), "— рядок непорожній, у ньому один символ")
print(" bool([0]) =", bool([0]), "— список непорожній, у ньому один елемент")
хибні значення
bool(0) = False
bool(0.0) = False
bool('') = False
bool([]) = False
bool({}) = False
bool(None) = False
а ось ці двоє підводять майже всіх
bool('0') = True — рядок непорожній, у ньому один символ
bool([0]) = True — список непорожній, у ньому один елемент
# наше правило: хибне те, що порожнє або нульове. Звіряємо його з bool() по пунктах
assert bool(0) is False
assert bool(0.0) is False
assert bool("") is False
assert bool([]) is False
assert bool({}) is False
assert bool(None) is False
assert bool(False) is False
assert bool("0") is True, "непорожній рядок істинний, хай там що всередині"
assert bool(" ") is True, "пробіл — теж символ"
assert bool([0]) is True, "непорожній список істинний, навіть якщо всередині нуль"
assert bool(-1) is True, "істинність не про знак, а про «не нуль»"
print("✅ усі 11 перевірок пройшли — правило працює рівно так, як каже bool()")
✅ усі 11 перевірок пройшли — правило працює рівно так, як каже bool()
Чому if міста: краще за if len(міста) > 0:¶
Два записи дають однаковий результат, але перший читається як намір і не створює зайвого числа. Переконаймося, що вони справді збігаються — і на порожньому списку, і на непорожньому.
міста_порожньо = []
міста_є = ["Львів", "Одеса"]
коротко_порожньо = bool(міста_порожньо)
довго_порожньо = len(міста_порожньо) > 0
коротко_є = bool(міста_є)
довго_є = len(міста_є) > 0
print("порожній список: if міста ->", коротко_порожньо, " | len(міста) > 0 ->", довго_порожньо)
print("непорожній: if міста ->", коротко_є, " | len(міста) > 0 ->", довго_є)
assert коротко_порожньо == довго_порожньо
assert коротко_є == довго_є
print("✅ обидва записи еквівалентні — тож беремо коротший")
порожній список: if міста -> False | len(міста) > 0 -> False непорожній: if міста -> True | len(міста) > 0 -> True ✅ обидва записи еквівалентні — тож беремо коротший
7 · Коротке замикання¶
Порахуємо середню температуру за добу. Формула проста — сума поділена на кількість
вимірів, — але кількість може виявитись нулем. Оператор and рятує нас безкоштовно:
якщо лівий операнд хибний, правий не обчислюється взагалі.
сума_вимірів = 100.0
кількість_вимірів = 0 # датчик мовчав усю добу
# ділення тут просто не відбудеться: лівий операнд хибний, і Python зупиняється
день_був_теплий = кількість_вимірів != 0 and сума_вимірів / кількість_вимірів > 15
print("кількість вимірів:", кількість_вимірів)
print("день був теплий:", день_був_теплий)
assert день_був_теплий is False
print("✅ програма не впала, хоча ділення на нуль стояло прямо у виразі")
кількість вимірів: 0 день був теплий: False ✅ програма не впала, хоча ділення на нуль стояло прямо у виразі
А ось що буде, якщо охорону прибрати. Ця клітинка теж падає навмисне — саме такий
ZeroDivisionError і чекає на того, хто поміняє операнди місцями.
сума_вимірів / кількість_вимірів > 15
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[13], line 1 ----> 1 сума_вимірів / кількість_вимірів > 15 ZeroDivisionError: float division by zero
Результат — це операнд, а не True/False¶
Найважливіше про and і or: вони повертають один зі своїх операндів. У if різниці
не видно, але щойно результат кудись зберігають — вона стає головною.
підпис = місто or "Київ" # місто порожнє, отже беремо запасне значення
перший_символ = місто and місто[0] # місто порожнє — індексації не буде
print("підпис:", repr(підпис), "тип:", type(підпис).__name__)
print("перший символ:", repr(перший_символ), "тип:", type(перший_символ).__name__)
assert підпис == "Київ"
assert type(підпис) is str, "or повернув рядок, а не True"
assert перший_символ == "", "and повернув сам порожній рядок, а не False"
assert type(перший_символ) is str, "and теж повернув рядок"
print("✅ обидва оператори повернули str — жодного bool тут немає")
підпис: 'Київ' тип: str перший символ: '' тип: str ✅ обидва оператори повернули str — жодного bool тут немає
І одразу пастка ідіоми «значення за замовчуванням». Запис x or запасне спрацює не лише
коли значення немає, а й коли воно є, але хибне за істинністю. Чесний нуль перетвориться
на запасне число — і ніхто цього не помітить.
введена_кількість = 0 # користувач свідомо ввів нуль
через_or = введена_кількість or 10 # нуль хибний -> підставиться 10
чесно = 10 if введена_кількість is None else введена_кількість
print("через or:", через_or, " | через is None:", чесно)
assert через_or == 10, "or підмінив чесний нуль запасним значенням"
assert чесно == 0, "перевірка на None не чіпає нуль"
print("⚠️ саме тому «немає значення» питають через is None, а не через or")
через or: 10 | через is None: 0 ⚠️ саме тому «немає значення» питають через is None, а не через or
8 · Тернарний вираз¶
Коли вся розвилка потрібна лише щоб обрати одне з двох значень, чотири рядки — забагато.
Умовний вираз робить те саме одним рядком і, на відміну від if, має значення.
статус = "тепло" if температура > 15 else "прохолодно"
print(f"{температура} °C — це {статус}")
# перевіряємо, що тернар дає рівно те саме, що й повний if
if температура > 15:
статус_довго = "тепло"
else:
статус_довго = "прохолодно"
assert статус == статус_довго, "коротка форма мусить збігатися з повною"
print("✅ тернарний вираз == повний if")
5.0 °C — це прохолодно ✅ тернарний вираз == повний if
9 · None і чому саме is¶
Датчик може не дати значення взагалі. «Немає значення» позначають об'єктом None —
і перевіряють через is, бо None існує в програмі в єдиному екземплярі.
Найважливіше нижче: if not x і if x is None — різні перевірки. Вони розходяться
рівно тоді, коли за вікном чесний нуль градусів.
показ_датчика = None # датчик мовчить
нуль_градусів = 0.0 # датчик чесно виміряв нуль
print("показ_датчика is None:", показ_датчика is None)
print("нуль_градусів is None:", нуль_градусів is None)
print("not показ_датчика: ", not показ_датчика)
print("not нуль_градусів: ", not нуль_градусів, "<- ось де ховається баг")
assert (показ_датчика is None) is True
assert (нуль_градусів is None) is False, "нуль — це значення, а не його відсутність"
assert (not нуль_градусів) is True, "нуль хибний, тож not дає True — і плутає нас"
print("✅ дві перевірки розійшлися саме на нулі — як і обіцяла лекція")
показ_датчика is None: True нуль_градусів is None: False not показ_датчика: True not нуль_градусів: True <- ось де ховається баг ✅ дві перевірки розійшлися саме на нулі — як і обіцяла лекція
# практичний висновок: спершу питаємо про наявність, і лише потім про величину
if показ_датчика is None:
вердикт = "датчик мовчить"
elif показ_датчика < 0:
вердикт = "мороз"
else:
вердикт = "плюсова температура"
print(вердикт)
assert вердикт == "датчик мовчить"
print("✅ порядок гілок урятував нас від порівняння None з числом")
датчик мовчить ✅ порядок гілок урятував нас від порівняння None з числом
10 · match / case проти ланцюжка elif¶
Тепер найцікавіше: перепишемо ланцюжок на match і доведемо через assert, що це
дослівно та сама логіка. Спершу — звичний elif.
if стан_датчика == "ok":
повідомлення_elif = "дані свіжі"
elif стан_датчика == "cached" or стан_датчика == "stale":
повідомлення_elif = "дані застарілі"
elif стан_датчика == "off":
повідомлення_elif = "датчик вимкнено"
else:
повідомлення_elif = "невідомий стан"
print("ланцюжок elif сказав:", повідомлення_elif)
ланцюжок elif сказав: дані свіжі
Те саме через match. Назва змінної тепер написана один раз, у заголовку, а
case "cached" | "stale" замінює довге or. case _ — це «все решта», аналог else.
match стан_датчика:
case "ok":
повідомлення_match = "дані свіжі"
case "cached" | "stale":
повідомлення_match = "дані застарілі"
case "off":
повідомлення_match = "датчик вимкнено"
case _:
повідомлення_match = "невідомий стан"
print("match / case сказав:", повідомлення_match)
assert повідомлення_match == повідомлення_elif, "два записи однієї логіки мусять збігатися"
print("✅ elif і match дали однакову відповідь — це справді один і той самий алгоритм")
match / case сказав: дані свіжі ✅ elif і match дали однакову відповідь — це справді один і той самий алгоритм
Перевіримо збіг ще на двох станах — на тому, що ловиться зразком «або-або», і на тому, якого в переліку немає взагалі.
стан_2 = "stale"
if стан_2 == "ok":
результат_elif_2 = "дані свіжі"
elif стан_2 == "cached" or стан_2 == "stale":
результат_elif_2 = "дані застарілі"
elif стан_2 == "off":
результат_elif_2 = "датчик вимкнено"
else:
результат_elif_2 = "невідомий стан"
match стан_2:
case "ok":
результат_match_2 = "дані свіжі"
case "cached" | "stale":
результат_match_2 = "дані застарілі"
case "off":
результат_match_2 = "датчик вимкнено"
case _:
результат_match_2 = "невідомий стан"
print(f'стан "{стан_2}": elif -> {результат_elif_2!r}, match -> {результат_match_2!r}')
assert результат_elif_2 == результат_match_2 == "дані застарілі"
print("✅ зразок «cached | stale» спрацював так само, як довге or")
стан "stale": elif -> 'дані застарілі', match -> 'дані застарілі' ✅ зразок «cached | stale» спрацював так само, як довге or
стан_3 = "boom" # такого стану в переліку немає
match стан_3:
case "ok":
результат_3 = "дані свіжі"
case "cached" | "stale":
результат_3 = "дані застарілі"
case "off":
результат_3 = "датчик вимкнено"
case _:
результат_3 = "невідомий стан"
print(f'стан "{стан_3}" -> {результат_3!r}')
assert результат_3 == "невідомий стан", "без case _ match просто нічого не виконав би"
print("✅ case _ упіймав усе, чого не передбачили")
стан "boom" -> 'невідомий стан' ✅ case _ упіймав усе, чого не передбачили
Сторож і структурний зразок¶
match — не просто гарніший switch. Зразок описує форму даних, а if після нього
(це називають сторожем, guard) додає умову вже після того, як форма збіглася. Ось те саме
вимірювання у трьох різних формах.
вимір = [23.5, 60] # список із двох чисел: температура й вологість
match вимір:
case [t, h] if t > 40:
опис = f"підозріло гаряче: {t} °C"
case [t, h]:
опис = f"пара значень: {t} °C, {h} %"
case {"t": t}:
опис = f"словник із ключем t: {t} °C"
case _:
опис = "форма невідома"
print(опис)
assert опис == "пара значень: 23.5 °C, 60 %"
print("✅ зразок [t, h] і збігся з формою, і одразу дав елементам імена")
пара значень: 23.5 °C, 60 % ✅ зразок [t, h] і збігся з формою, і одразу дав елементам імена
вимір_словник = {"t": 41.0, "джерело": "дах"}
match вимір_словник:
case [t, h] if t > 40:
опис_2 = f"підозріло гаряче: {t} °C"
case [t, h]:
опис_2 = f"пара значень: {t} °C, {h} %"
case {"t": t} if t > 40:
опис_2 = f"словник каже: підозріло гаряче, {t} °C"
case {"t": t}:
опис_2 = f"словник із ключем t: {t} °C"
case _:
опис_2 = "форма невідома"
print(опис_2)
assert опис_2 == "словник каже: підозріло гаряче, 41.0 °C"
print("✅ сторож if t > 40 відсіяв перший словниковий зразок на користь другого")
словник каже: підозріло гаряче, 41.0 °C ✅ сторож if t > 40 відсіяв перший словниковий зразок на користь другого
11 · Пастки, які не викликають помилки¶
Найдорожчі баги — ті, що не падають. Три класичні приклади з лекції, кожен перевірений
assert-ом.
речі = ["шапка"]
коротко = bool(речі) # «чи є там хоч щось»
через_рівність = речі == True # «чи це той самий об'єкт True»
print("if речі: ->", коротко)
print("if речі == True: ->", через_рівність)
assert коротко is True
assert через_рівність is False, "непорожній список істинний, але не дорівнює True"
print("⚠️ саме тому пишуть просто if x:, а не if x == True")
if речі: -> True if речі == True: -> False ⚠️ саме тому пишуть просто if x:, а не if x == True
код_помилки = 5
неправильно = код_помилки == 1 or 2 # це (код == 1) or 2, а двійка істинна завжди
правильно = код_помилки in (1, 2)
print("код_помилки == 1 or 2 ->", неправильно)
print("код_помилки in (1, 2) ->", правильно)
assert bool(неправильно) is True, "вираз істинний за будь-якого коду — це і є пастка"
assert правильно is False, "п'ятірки немає серед (1, 2)"
print("⚠️ 'a == 1 or 2' завжди істинне: перевіряй через in")
код_помилки == 1 or 2 -> 2 код_помилки in (1, 2) -> False ⚠️ 'a == 1 or 2' завжди істинне: перевіряй через in
import math
сума = 0.1 + 0.2
print("0.1 + 0.2 =", сума)
print("сума == 0.3 ->", сума == 0.3)
print("math.isclose(...) ->", math.isclose(сума, 0.3))
assert (сума == 0.3) is False, "збережені дроби не рівні тому, що ми написали"
assert math.isclose(сума, 0.3) is True
print("⚠️ умови над float будуй на math.isclose, а не на ==")
0.1 + 0.2 = 0.30000000000000004 сума == 0.3 -> False math.isclose(...) -> True ⚠️ умови над float будуй на math.isclose, а не на ==
12 · Збираємо все разом¶
Фінальний фрагмент застосунку: валідація, перевірка на None, охорона через коротке
замикання, класифікація ланцюжком і match для стану датчика. Одна програма, усі
прийоми теми.
вхід_температура = 18.0
вхід_стан = "cached"
if вхід_температура is None:
звіт = "датчик мовчить"
elif not (-90 <= вхід_температура <= 70):
звіт = "значення поза межами фізики"
elif вхід_температура < 0:
звіт = "мороз: куртка й шапка"
elif вхід_температура < 12:
звіт = "прохолодно: светр"
elif вхід_температура < 22:
звіт = "комфортно: сорочка"
else:
звіт = "тепло: футболка"
match вхід_стан:
case "ok":
приписка = ""
case "cached" | "stale":
приписка = " (дані застарілі)"
case "off":
приписка = " (датчик вимкнено)"
case _:
приписка = " (стан невідомий)"
print(звіт + приписка)
assert звіт == "комфортно: сорочка"
assert приписка == " (дані застарілі)"
print("✅ застосунок зібрано")
комфортно: сорочка (дані застарілі) ✅ застосунок зібрано
Завдання¶
🟢 Рівень 1 — База¶
Додай до фінального фрагмента п'яту гілку для спеки: якщо температура вища за 30,
звіт має бути "спека: сиди вдома". Постав її на правильне місце в ланцюжку й поясни
коментарем, чому саме туди.
Зроблено, якщо: при вхід_температура = 35.0 друкується новий звіт, а при 18.0 —
попередній, і обидва підтверджені assert-ами.
🟡 Рівень 2 — Плюс¶
Напиши перевірку введеної назви міста, яка розрізняє три стани: назви немає взагалі
(None), назва порожня або складається з пробілів ("", " "), назва нормальна.
Використай is None, істинність рядка й метод .strip() з теми про рядки.
Зроблено, якщо: три різні входи дають три різні повідомлення, і assert показує, що
None та "" потрапляють у різні гілки.
🔴 Рівень 3 — Виклик¶
Візьми вимірювання у трьох формах — [23.5, 60], {"t": 23.5, "h": 60} і просто
23.5 — і напиши один match, який усі три зводить до пари (температура, вологість),
підставляючи None замість відсутньої вологості. Потім запиши те саме ланцюжком elif
із isinstance і len.
Зроблено, якщо: обидві версії дають однаковий результат на всіх трьох входах
(перевірено assert-ами), і ти можеш словами сказати, у якому місці версія на elif
довша й де саме в ній легше помилитись.
Підказки¶
- У ланцюжку
elifпорядок гілок — частина логіки: спершу питай про наявність значення, потім про його коректність, і лише потім про величину. - Голе ім'я в
case— це захоплення, а не порівняння. Щоб порівняти з константою, потрібен літерал або «крапкове» ім'я. - Щоб перевірити тип у гілці
match, є зразокcase float(x)— він і перевірить тип, і одразу дасть значенню ім'я.