Практика · Що таке програмування і навіщо Python¶

Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md

У лекції ми розібрали теорію: комп'ютер виконує інструкції буквально й по черзі, Python спершу компілює твій файл у байт-код, а вже байт-код виконує віртуальна машина. Тут ми все це побачимо руками.

Що зробимо:

  1. Напишемо першу програму — і змінимо її.
  2. Переконаємось, що рядки виконуються зверху вниз, а не «як зрозуміліше».
  3. Свідомо зламаємо код трьома різними способами й навчимося читати повідомлення про помилку.
  4. Подивимось на справжній байт-код і перевіримо твердження лекції про згортання констант.
  5. Порахуємо середнє вручну й звіримо з бібліотечним — щоб побачити, що всередині бібліотеки немає магії.
  6. Заміряємо, наскільки чистий Python повільніший за NumPy, і зіставимо з інтерактивом 6 з лекції.

Як користуватись зошитом. Клітинка з кодом запускається кнопкою ▶ або сполученням Shift + Enter. Результат з'являється просто під нею. Клітинки треба виконувати згори вниз: пізніші спираються на змінні, створені в ранніших.

1 · Перша програма¶

print — це вбудована дія: «надрукуй те, що в дужках». Текст беруть у лапки, щоб Python зрозумів, що це саме текст, а не ім'я чогось.

In [1]:
print("Привіт, світе!")
Привіт, світе!

2 · Змінимо програму¶

Тепер зробимо привітання особистим. ім_я — це змінна: ім'я, під яким лежить значення. Рядок з f перед лапками дозволяє підставити значення змінної просто в текст.

Зверни увагу: комп'ютер нічого не «здогадується». Він бере те, що зараз лежить у ім_я, і підставляє символ у символ.

In [2]:
ім_я = "Оксана"
print(f"Привіт, {ім_я}!")

# міняємо значення змінної — і той самий рядок друкує інше
ім_я = "Богдан"
print(f"Привіт, {ім_я}!")
Привіт, Оксана!
Привіт, Богдан!

3 · Порядок має значення¶

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

In [3]:
рахунок = 10
рахунок = рахунок + 5     # спершу додали
рахунок = рахунок * 2     # потім помножили
print("додали, потім помножили:", рахунок)

рахунок = 10
рахунок = рахунок * 2     # тепер навпаки: спершу помножили
рахунок = рахунок + 5
print("помножили, потім додали:", рахунок)
додали, потім помножили: 30
помножили, потім додали: 25

4 · Ламаємо код навмисно: помилка №1 — NameError¶

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

Щоб зошит не зупинився, ми ловимо помилку конструкцією try / except (її детально розберемо в темі 18) і друкуємо повідомлення так, як його побачив би ти.

In [4]:
# Ця клітинка ПАДАЄ навмисно — так і має бути. Дивись на повідомлення під нею.
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 — тією самою функцією, яка перетворює текст на байт-код. Ми даємо їй свідомо зіпсований рядок: не закрита дужка.

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

In [6]:
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», розібрати).

In [7]:
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 (додавання) залишається в байт-коді.

In [8]:
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).

In [9]:
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.

In [10]:
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 — змінна). Скільки інструкцій у кожному? Поясни різницю словами.

Місце для твоїх спроб — нижче.

In [11]:
# ── твій код ──────────────────────────────────────────────
# Приклад-заготовка для рівня 1, зміни під себе:
моє_ім_я = "Тарас"
улюблена_страва = "вареники"
print(f"{моє_ім_я} найбільше любить {улюблена_страва}.")
Тарас найбільше любить вареники.

Далі: тест самоперевірки · домашнє завдання · наступна тема: Встановлення й перший запуск