Практика · Що таке програмування і навіщо Python¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
У лекції ми розібрали теорію: комп'ютер виконує інструкції буквально й по черзі, Python спершу компілює твій файл у байт-код, а вже байт-код виконує віртуальна машина. Тут ми все це побачимо руками.
Що зробимо:
- Напишемо першу програму — і змінимо її.
- Переконаємось, що рядки виконуються зверху вниз, а не «як зрозуміліше».
- Свідомо зламаємо код трьома різними способами й навчимося читати повідомлення про помилку.
- Подивимось на справжній байт-код і перевіримо твердження лекції про згортання констант.
- Порахуємо середнє вручну й звіримо з бібліотечним — щоб побачити, що всередині бібліотеки немає магії.
- Заміряємо, наскільки чистий Python повільніший за NumPy, і зіставимо з інтерактивом 6 з лекції.
Як користуватись зошитом. Клітинка з кодом запускається кнопкою ▶ або сполученням
Shift + Enter. Результат з'являється просто під нею. Клітинки треба виконувати згори вниз:
пізніші спираються на змінні, створені в ранніших.
1 · Перша програма¶
print — це вбудована дія: «надрукуй те, що в дужках».
Текст беруть у лапки, щоб Python зрозумів, що це саме текст, а не ім'я чогось.
print("Привіт, світе!")
Привіт, світе!
2 · Змінимо програму¶
Тепер зробимо привітання особистим. ім_я — це змінна: ім'я, під яким лежить
значення. Рядок з f перед лапками дозволяє підставити значення змінної просто в текст.
Зверни увагу: комп'ютер нічого не «здогадується». Він бере те, що зараз лежить у ім_я,
і підставляє символ у символ.
ім_я = "Оксана"
print(f"Привіт, {ім_я}!")
# міняємо значення змінної — і той самий рядок друкує інше
ім_я = "Богдан"
print(f"Привіт, {ім_я}!")
Привіт, Оксана! Привіт, Богдан!
3 · Порядок має значення¶
Три однакові дії, записані в різному порядку, дають різний результат. Це та сама буквальність, з якою ми гралися в інтерактиві з роботом: машина виконує рядки один за одним, згори вниз.
рахунок = 10
рахунок = рахунок + 5 # спершу додали
рахунок = рахунок * 2 # потім помножили
print("додали, потім помножили:", рахунок)
рахунок = 10
рахунок = рахунок * 2 # тепер навпаки: спершу помножили
рахунок = рахунок + 5
print("помножили, потім додали:", рахунок)
додали, потім помножили: 30 помножили, потім додали: 25
4 · Ламаємо код навмисно: помилка №1 — NameError¶
Найчастіша помилка новачка: звернутись до змінної, якої ще немає. Причини зазвичай дві — або друкарська помилка в імені, або клітинку зі створенням змінної просто не запустили.
Щоб зошит не зупинився, ми ловимо помилку конструкцією try / except
(її детально розберемо в темі 18) і друкуємо повідомлення так, як його побачив би ти.
# Ця клітинка ПАДАЄ навмисно — так і має бути. Дивись на повідомлення під нею.
print(прізвище) # такої змінної ми ніколи не створювали
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[4], line 2 1 # Ця клітинка ПАДАЄ навмисно — так і має бути. Дивись на повідомлення під нею. ----> 2 print(прізвище) # такої змінної ми ніколи не створювали NameError: name 'прізвище' is not defined
Як читати це повідомлення¶
Читай знизу вгору — найважливіше в останньому рядку:
| Частина | Що означає |
|---|---|
NameError |
тип помилки: «невідоме ім'я» |
name 'прізвище' is not defined |
пояснення: ім'я прізвище не визначено |
Cell In[4], line 2 |
де саме: яка клітинка і який рядок у ній |
----> 2 print(прізвище) |
сам винний рядок, зі стрілкою |
^^^^^^^^ |
підкреслення під тим місцем, яке зламалось |
Ці чотири речі є в будь-якому traceback. Навчишся їх бачити — і половина страху перед помилками зникне.
5 · Помилка №2 — SyntaxError¶
Попередня помилка сталася під час виконання. Ця — інша: Python навіть не почне виконувати код, бо не зміг його розібрати на етапі 3 нашого конвеєра (парсер).
Щоб показати це безпечно, скористаємось compile — тією самою функцією, яка перетворює
текст на байт-код. Ми даємо їй свідомо зіпсований рядок: не закрита дужка.
import traceback # стандартний модуль: уміє показати текст помилки рядком
зіпсований_код = 'print("Привіт"' # дужку не закрито
try:
compile(зіпсований_код, "мій_файл.py", "exec")
except SyntaxError:
print(traceback.format_exc())
Traceback (most recent call last):
File "/tmp/ipykernel_2789793/865077221.py", line 6, in <module>
compile(зіпсований_код, "мій_файл.py", "exec")
File "мій_файл.py", line 1
print("Привіт"
^
SyntaxError: '(' was never closed
6 · Помилка №3 — TypeError¶
А ось помилка, якої Java чи C не допустили б узагалі: там компілятор зупинив би тебе ще до запуску. Python дозволяє написати такий код — і падає лише тоді, коли до цього рядка дійде виконання. Це та сама плата за гнучкість, про яку йшлося в лекції.
try:
результат = "2" + 3 # текст "2" і число 3 — різні типи, додавати їх нічим
print(результат)
except TypeError:
print(traceback.format_exc())
# правильно — спершу привести до одного типу:
print("а так працює:", int("2") + 3)
Traceback (most recent call last):
File "/tmp/ipykernel_2789793/1517036516.py", line 2, in <module>
результат = "2" + 3 # текст "2" і число 3 — різні типи, додавати їх нічим
~~~~^~~
TypeError: can only concatenate str (not "int") to str
а так працює: 5
7 · Дивимось на байт-код¶
У лекції (інтерактив 4) стверджувалось: Python компілює print(2 + 3) у байт-код,
і в цьому байт-коді немає ані двійки, ані трійки — компілятор порахував суму
заздалегідь. Перевіримо це самі модулем dis («disassemble», розібрати).
import dis
dis.dis("print(2 + 3)")
0 0 RESUME 0
1 2 PUSH_NULL
4 LOAD_NAME 0 (print)
6 LOAD_CONST 0 (5)
8 CALL 1
16 RETURN_VALUE
Шукай у виводі рядок LOAD_CONST — поряд із ним стоїть 5, а не 2 і 3.
Це і є згортання констант (constant folding): вирази з відомих наперед чисел
обчислюються один раз під час компіляції, а не щоразу під час роботи.
Для порівняння — той самий вираз, але зі змінною. Тут згорнути нічого не можна:
значення a стане відомим лише під час виконання, тож інструкція BINARY_OP (додавання)
залишається в байт-коді.
dis.dis("print(a + 3)")
0 0 RESUME 0
1 2 PUSH_NULL
4 LOAD_NAME 0 (print)
6 LOAD_NAME 1 (a)
8 LOAD_CONST 0 (3)
10 BINARY_OP 0 (+)
14 CALL 1
22 RETURN_VALUE
8 · Наша реалізація проти бібліотечної¶
Найкорисніша звичка: не вірити бібліотеці на слово, а перевірити її своїм розрахунком. Порахуємо середнє арифметичне трьох чисел — рівно ту задачу, яку в лекції писали трьома мовами (інтерактив 3).
import statistics
import numpy as np
числа = [4, 8, 15]
# наш розрахунок: сума поділена на кількість — за означенням середнього
наше_середнє = (числа[0] + числа[1] + числа[2]) / 3
# те саме двома бібліотеками
середнє_statistics = statistics.mean(числа)
середнє_numpy = np.mean(числа)
print("наше: ", наше_середнє)
print("statistics: ", середнє_statistics)
print("numpy: ", середнє_numpy)
# порівнюємо з допуском: числа з рухомою крапкою рідко збігаються символ у символ
assert np.allclose(наше_середнє, середнє_statistics), "розійшлося зі statistics!"
assert np.allclose(наше_середнє, середнє_numpy), "розійшлося з numpy!"
print("\n✅ збігається: всередині бібліотеки та сама формула, що й у нас")
наше: 9.0 statistics: 9 numpy: 9.0 ✅ збігається: всередині бібліотеки та сама формула, що й у нас
9 · Скільки насправді коштує повільність¶
Інтерактив 6 з лекції показував модель. Тепер заміряємо на твоїй машині.
Обидва рядки роблять одне: додають усі числа від 0 до 2 000 000. Різниця лише в тому, де крутиться цикл — у Python чи всередині скомпільованої бібліотеки NumPy.
import timeit
скільки_чисел = 2_000_000
масив_numpy = np.arange(скільки_чисел)
# number=3 — повторюємо тричі й беремо середній час, щоб випадковий стрибок не збив вимір
час_python = timeit.timeit(lambda: sum(range(скільки_чисел)), number=3) / 3
час_numpy = timeit.timeit(lambda: масив_numpy.sum(), number=3) / 3
print(f"чистий Python: {час_python * 1000:8.2f} мс")
print(f"NumPy: {час_numpy * 1000:8.2f} мс")
print(f"різниця: у {час_python / час_numpy:.0f} разів")
# результат мусить бути однаковий — прискорення не має міняти відповідь
assert sum(range(скільки_чисел)) == масив_numpy.sum(), "суми розійшлися!"
print("\n✅ суми однакові — швидше, але не інакше")
чистий Python: 92.44 мс NumPy: 0.90 мс різниця: у 103 разів ✅ суми однакові — швидше, але не інакше
Точні числа в тебе будуть свої: вони залежать від процесора й від того, чим ще зайнятий комп'ютер. Але співвідношення буде тим самим порядком, що й у лекції — десятки разів.
І головний висновок теж той самий: це все ще Python. Ми не переписували нічого на C — ми просто попросили Python звернутись до бібліотеки, яка на C уже написана. Саме так «повільна» мова робить важкі обчислення.
10 · Твоя черга¶
Три рівні. Розгорнуті умови й критерії «зроблено» — у homework.md, тут коротко, щоб можна було спробувати одразу під цією клітинкою.
🟢 Рівень 1¶
Створи змінні моє_ім_я й улюблена_страва і надрукуй одним print речення,
яке використовує обидві.
🟡 Рівень 2¶
Зламай власний код навмисно трьома різними способами (не тими, що вище),
злови кожну помилку через try / except і надрукуй traceback. Поряд напиши
коментарем, який це тип помилки й що саме її спричинило.
🔴 Рівень 3¶
Візьми dis.dis і порівняй байт-код двох виразів, які дають однаковий результат:
2 * 3 * 4 і x * 3 * 4 (де x — змінна). Скільки інструкцій у кожному?
Поясни різницю словами.
Місце для твоїх спроб — нижче.
# ── твій код ──────────────────────────────────────────────
# Приклад-заготовка для рівня 1, зміни під себе:
моє_ім_я = "Тарас"
улюблена_страва = "вареники"
print(f"{моє_ім_я} найбільше любить {улюблена_страва}.")
Тарас найбільше любить вареники.