Практика · Умови¶

Лекція: lecture.html · Тест: quiz.html · Домашнє: homework.md

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

Що зробимо:

  1. перевіримо дані з датчика, перш ніж їм повірити;
  2. напишемо ланцюжок if / elif / else і подивимось, скільки умов він реально обчислює;
  3. зламаємо відступ навмисне й прочитаємо справжній IndentationError;
  4. переберемо всі хибні значення й переконаємось, що наше правило збігається з bool();
  5. побачимо коротке замикання в дії — і що воно повертає не True/False;
  6. з'ясуємо, чим is None відрізняється від not x;
  7. перепишемо ланцюжок elif на match / case і доведемо через assert, що це те саме.

Циклів і функцій тут ще немає — вони будуть у темах 12 і 14. Усе, що нижче, — рівний код згори вниз, як у справжньому скрипті.

1 · Дані, з якими працюємо¶

Спершу опишемо одне вимірювання з датчика. Це чотири звичайні змінні — нічого нового, але саме на них триматиметься вся решта зошита. Зверни увагу на стан_датчика: у житті датчик не завжди відповідає «все добре», і програма мусить це передбачити.

In [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 читається як у підручнику й обчислює температура рівно один раз.

In [2]:
# межі беремо з фізики, а не зі стелі: рекорди Землі — приблизно -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. Двокрапка в кінці рядка обіцяє блок, а відступ у чотири пробіли цей блок утворює. Поки що це вся конструкція.

In [3]:
if температура < 12:
    порада = "візьми куртку"
else:
    порада = "куртка не потрібна"

print(порада)
візьми куртку

4 · Ланцюжок elif: чотири гілки замість двох¶

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

In [4]:
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
✅ гілка й кількість перевірок збігаються з очікуваними

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

In [5]:
температура_спека = 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 завжди називає номер рядка й тип помилки.

Перший випадок: після двокрапки блоку немає взагалі.

In [6]:
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 — блок зайвий.

In [7]:
if температура < 12:
    print("холодно")
        print("і вітряно")
  Cell In[7], line 3
    print("і вітряно")
    ^
IndentationError: unexpected indent

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

In [8]:
надруковано = []           # сюди складатимемо все, що програма встигла сказати

if температура_спека < 12:
    надруковано.append("холодно")
    надруковано.append("кінець")   # цей рядок теж усередині if — через відступ

print("вивід програми:", надруковано)
assert надруковано == [], "при 30 °C умова хибна, отже не виконався жоден рядок блоку"
print("✅ зсунутий на чотири пробіли рядок змінив поведінку, а не зламав програму")
вивід програми: []
✅ зсунутий на чотири пробіли рядок змінив поведінку, а не зламав програму

6 · Що Python вважає правдою¶

Правило з лекції коротке: хибні — False, None, нулі й порожні колекції. Усе інше істинне. Перевіримо його на найпідступніших значеннях і звіримо з вбудованим bool() — це наша версія перевірки «наша реалізація = еталонна».

In [9]:
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 — список непорожній, у ньому один елемент
In [10]:
# наше правило: хибне те, що порожнє або нульове. Звіряємо його з 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:¶

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

In [11]:
міста_порожньо = []
міста_є = ["Львів", "Одеса"]

коротко_порожньо = 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 рятує нас безкоштовно: якщо лівий операнд хибний, правий не обчислюється взагалі.

In [12]:
сума_вимірів = 100.0
кількість_вимірів = 0      # датчик мовчав усю добу

# ділення тут просто не відбудеться: лівий операнд хибний, і Python зупиняється
день_був_теплий = кількість_вимірів != 0 and сума_вимірів / кількість_вимірів > 15

print("кількість вимірів:", кількість_вимірів)
print("день був теплий:", день_був_теплий)
assert день_був_теплий is False
print("✅ програма не впала, хоча ділення на нуль стояло прямо у виразі")
кількість вимірів: 0
день був теплий: False
✅ програма не впала, хоча ділення на нуль стояло прямо у виразі

А ось що буде, якщо охорону прибрати. Ця клітинка теж падає навмисне — саме такий ZeroDivisionError і чекає на того, хто поміняє операнди місцями.

In [13]:
сума_вимірів / кількість_вимірів > 15
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[13], line 1
----> 1 сума_вимірів / кількість_вимірів > 15

ZeroDivisionError: float division by zero

Результат — це операнд, а не True/False¶

Найважливіше про and і or: вони повертають один зі своїх операндів. У if різниці не видно, але щойно результат кудись зберігають — вона стає головною.

In [14]:
підпис = місто 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 запасне спрацює не лише коли значення немає, а й коли воно є, але хибне за істинністю. Чесний нуль перетвориться на запасне число — і ніхто цього не помітить.

In [15]:
введена_кількість = 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, має значення.

In [16]:
статус = "тепло" 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 — різні перевірки. Вони розходяться рівно тоді, коли за вікном чесний нуль градусів.

In [17]:
показ_датчика = 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 <- ось де ховається баг
✅ дві перевірки розійшлися саме на нулі — як і обіцяла лекція
In [18]:
# практичний висновок: спершу питаємо про наявність, і лише потім про величину
if показ_датчика is None:
    вердикт = "датчик мовчить"
elif показ_датчика < 0:
    вердикт = "мороз"
else:
    вердикт = "плюсова температура"

print(вердикт)
assert вердикт == "датчик мовчить"
print("✅ порядок гілок урятував нас від порівняння None з числом")
датчик мовчить
✅ порядок гілок урятував нас від порівняння None з числом

10 · match / case проти ланцюжка elif¶

Тепер найцікавіше: перепишемо ланцюжок на match і доведемо через assert, що це дослівно та сама логіка. Спершу — звичний elif.

In [19]:
if стан_датчика == "ok":
    повідомлення_elif = "дані свіжі"
elif стан_датчика == "cached" or стан_датчика == "stale":
    повідомлення_elif = "дані застарілі"
elif стан_датчика == "off":
    повідомлення_elif = "датчик вимкнено"
else:
    повідомлення_elif = "невідомий стан"

print("ланцюжок elif сказав:", повідомлення_elif)
ланцюжок elif сказав: дані свіжі

Те саме через match. Назва змінної тепер написана один раз, у заголовку, а case "cached" | "stale" замінює довге or. case _ — це «все решта», аналог else.

In [20]:
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 дали однакову відповідь — це справді один і той самий алгоритм

Перевіримо збіг ще на двох станах — на тому, що ловиться зразком «або-або», і на тому, якого в переліку немає взагалі.

In [21]:
стан_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
In [22]:
стан_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) додає умову вже після того, як форма збіглася. Ось те саме вимірювання у трьох різних формах.

In [23]:
вимір = [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] і збігся з формою, і одразу дав елементам імена
In [24]:
вимір_словник = {"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-ом.

In [25]:
речі = ["шапка"]

коротко = 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
In [26]:
код_помилки = 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
In [27]:
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 для стану датчика. Одна програма, усі прийоми теми.

In [28]:
вхід_температура = 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) — він і перевірить тип, і одразу дасть значенню ім'я.