Практика 02 · Встановлення й перший запуск¶

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

У лекції ми говорили про інтерпретатор, термінал і traceback. Тут ми зробимо все це руками — але не в терміналі, а прямо із зошита, щоб кожен крок було видно.

Що зробимо:

  1. З'ясуємо, який саме Python виконує цей зошит і де він лежить.
  2. Створимо перший файл hello.py і запустимо його — тим самим інтерпретатором.
  3. Зламаємо програму тричі й прочитаємо кожен traceback по частинах.
  4. Напишемо власну функцію, яка збирає останній рядок traceback, і звіримо її з бібліотечною.
  5. Побачимо на власні очі, чим режим REPL відрізняється від режиму файлу.

Нічого встановлювати не треба: усе, що потрібно, є в стандартній бібліотеці.

1 · Хто саме виконує цей зошит¶

Перше питання, яке варто ставити щоразу, коли «щось не працює»: а який Python зараз працює? Відповідь дає модуль sys. sys.executable — це шлях до самого інтерпретатора, той самий, що знайшовся б у PATH за командою python3.

In [1]:
import sys
import platform

print("версія Python :", platform.python_version())
print("повний рядок  :", sys.version.replace("\n", " "))
print("сам файл      :", sys.executable)
print("система       :", platform.system(), platform.release())

# перевіряємо те саме, що й у лекції: нам потрібна 3.12 або новіша
if sys.version_info >= (3, 12):
    print("\n✅ версія підходить для курсу")
else:
    print("\n⚠️ версія стара — частина прикладів курсу може не працювати")
версія Python : 3.12.2
повний рядок  : 3.12.2 (main, Feb 21 2024, 00:00:00) [GCC 13.2.1 20231205 (Red Hat 13.2.1-6)]
сам файл      : /usr/bin/python3
система       : Linux 6.7.11-200.fc39.x86_64

✅ версія підходить для курсу

2 · Де ми зараз стоїмо¶

У лекції ми називали це «поточним каталогом». У зошита він теж є, і саме відносно нього шукатимуться всі файли, які ми напишемо без повного шляху. Команді pwd із термінала відповідає Path.cwd().

In [2]:
from pathlib import Path

potochna_teka = Path.cwd()
print("поточна тека:", potochna_teka)

# аналог команди ls: перші кілька імен із поточної теки
imena = sorted(p.name for p in potochna_teka.iterdir())
print("тут лежить  :", ", ".join(imena[:8]) if imena else "(порожньо)")
поточна тека: /mnt/var/www/projects_teacher/python-ml-lessons/courses/01-python/02-setup-and-first-run
тут лежить  : homework.html, homework.md, lecture.html, practice.html, practice.ipynb, quiz.html

3 · Перший файл¶

Тепер створимо hello.py. Щоб не смітити в теці курсу, покладемо його в тимчасову теку — у житті це була б твоя власна тека projects. Шлях друкуємо повністю: саме такий шлях з'явиться в traceback, коли ми зламаємо програму.

In [3]:
import tempfile

# окрема тека під наші досліди — щоб нічого не заважало й нічого не зіпсувалося
robocha_teka = Path(tempfile.mkdtemp(prefix="pershi_kroky_"))

shlyah_hello = robocha_teka / "hello.py"
shlyah_hello.write_text('print("Привіт, світе!")\n', encoding="utf-8")

print("робоча тека :", robocha_teka)
print("файл        :", shlyah_hello)
print("вміст файлу :", shlyah_hello.read_text(encoding="utf-8").strip())
робоча тека : /tmp/pershi_kroky_k1t86z8e
файл        : /tmp/pershi_kroky_k1t86z8e/hello.py
вміст файлу : print("Привіт, світе!")

4 · Запуск¶

У терміналі ми написали б python3 hello.py. Тут зробимо буквально те саме, лише програмно: subprocess.run запускає окремий процес, а sys.executable гарантує, що запуститься той самий інтерпретатор, який виконує зошит. Це та сама ідея, що й python3 -m pip замість голого pip.

Результат запуску складається з трьох частин, і всі три варто побачити:

  • stdout — те, що програма надрукувала;
  • stderr — те, на що вона поскаржилась;
  • returncode — код повернення: 0 означає «все добре», будь-що інше — «впало».
In [4]:
import subprocess


def zapustyty(shlyah_do_faylu):
    """Запускає файл окремим процесом і повертає результат.

    Виносимо в функцію, бо далі запускатимемо ще тричі — і щоразу нас
    цікавитимуть ті самі три речі: вивід, помилка й код повернення.
    """
    return subprocess.run(
        [sys.executable, str(shlyah_do_faylu)],
        capture_output=True,      # перехоплюємо вивід замість друку в консоль
        text=True,                # хочемо рядки, а не байти
        encoding="utf-8",
    )


rezultat = zapustyty(shlyah_hello)

print("код повернення:", rezultat.returncode)
print("stdout        :", repr(rezultat.stdout))
print("stderr        :", repr(rezultat.stderr))
print()
print("а ось так це виглядає в терміналі:")
print(rezultat.stdout, end="")
код повернення: 0
stdout        : 'Привіт, світе!\n'
stderr        : ''

а ось так це виглядає в терміналі:
Привіт, світе!

Зверни увагу на repr() у другому рядку: він показує рядок як його бачить Python, разом із невидимим символом кінця рядка \n. Той самий \n, який print перетворює на перехід на новий рядок. Ця різниця між «як виглядає» і «як записано» ще не раз згодиться.

5 · Ламаємо навмисно · помилка перша: NameError¶

Найчастіша помилка новачка — одрук в імені. Створимо змінну pryvit, а звернемося до pryvet.

In [5]:
kod_z_name_error = (
    'pryvit = "Привіт, світе!"\n'
    "\n"
    "print(pryvet)\n"
)

shlyah_name = robocha_teka / "name_error.py"
shlyah_name.write_text(kod_z_name_error, encoding="utf-8")

rezultat_name = zapustyty(shlyah_name)

print("код повернення:", rezultat_name.returncode, "→ програма впала")
print()
print(rezultat_name.stderr, end="")
код повернення: 1 → програма впала

Traceback (most recent call last):
  File "/tmp/pershi_kroky_k1t86z8e/name_error.py", line 3, in <module>
    print(pryvet)
          ^^^^^^
NameError: name 'pryvet' is not defined. Did you mean: 'pryvit'?

6 · Розбираємо traceback по частинах¶

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

In [6]:
def rozibraty_traceback(tekst_pomylky):
    """Друкує traceback по рядках із підписами, що є що.

    Мета — не автоматизація, а звичка: після кількох разів очі самі
    починають чіплятися за потрібні рядки.
    """
    rjadky = tekst_pomylky.rstrip("\n").split("\n")
    for nomer, rjadok in enumerate(rjadky, start=1):
        if rjadok.startswith("Traceback"):
            pidpys = "шапка стосу"
        elif rjadok.lstrip().startswith("File "):
            pidpys = "файл, рядок, місце"
        elif set(rjadok.strip()) <= set("^~") and rjadok.strip():
            pidpys = "стрілки під винним фрагментом"
        elif nomer == len(rjadky):
            pidpys = "ДІАГНОЗ — читай першим"
        else:
            pidpys = "сам рядок коду"
        print(f"{nomer}. [{pidpys}]")
        print(f"   {rjadok}")


rozibraty_traceback(rezultat_name.stderr)
1. [шапка стосу]
   Traceback (most recent call last):
2. [файл, рядок, місце]
     File "/tmp/pershi_kroky_k1t86z8e/name_error.py", line 3, in <module>
3. [сам рядок коду]
       print(pryvet)
4. [стрілки під винним фрагментом]
             ^^^^^^
5. [ДІАГНОЗ — читай першим]
   NameError: name 'pryvet' is not defined. Did you mean: 'pryvit'?

Останній рядок — це і є діагноз: NameError: name 'pryvet' is not defined. І Python навіть пропонує, що ти, ймовірно, мав на увазі. Ця підказка з'явилася в Python 3.10 — ще одна причина не сидіти на старій версії.

7 · Помилка друга: SyntaxError¶

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

In [7]:
kod_z_syntax_error = (
    'print("Привіт, світе!"\n'
    'print("цей рядок навіть не спробує виконатися")\n'
)

shlyah_syntax = robocha_teka / "syntax_error.py"
shlyah_syntax.write_text(kod_z_syntax_error, encoding="utf-8")

rezultat_syntax = zapustyty(shlyah_syntax)

print(rezultat_syntax.stderr, end="")
print()
print("є шапка «Traceback»?", "Traceback" in rezultat_syntax.stderr)
print("щось надрукувалось у stdout?", repr(rezultat_syntax.stdout))
  File "/tmp/pershi_kroky_k1t86z8e/syntax_error.py", line 1
    print("Привіт, світе!"
         ^
SyntaxError: '(' was never closed

є шапка «Traceback»? False
щось надрукувалось у stdout? ''

Два спостереження, і обидва важливі:

  1. Шапки «Traceback» немає. Вона описує шлях виконання — а виконання й не починалося: файл не пройшов перевірку граматики.
  2. stdout порожній. Хоча зламано другий рядок, перший print теж не спрацював. Синтаксис перевіряється для всього файлу до запуску.

Порівняй це з NameError: там програма встигла попрацювати й упала вже на ходу.

8 · Помилка третя: TypeError¶

Класика: значення виглядає як число, але насправді це текст.

In [8]:
kod_z_type_error = (
    "vik = \"17\"\n"
    "print(vik + 1)\n"
)

shlyah_type = robocha_teka / "type_error.py"
shlyah_type.write_text(kod_z_type_error, encoding="utf-8")

rezultat_type = zapustyty(shlyah_type)

print(rezultat_type.stderr, end="")
Traceback (most recent call last):
  File "/tmp/pershi_kroky_k1t86z8e/type_error.py", line 2, in <module>
    print(vik + 1)
          ~~~~^~~
TypeError: can only concatenate str (not "int") to str

Подивись на рядок зі стрілками: ~~~~^~~. Тильди позначають вираз навколо, а «дашок» — саму дію, що впала. Тобто Python каже не просто «щось не так у рядку 2», а «не так саме в оцьому додаванні». Це поведінка Python 3.11 і новіше.

9 · Ловимо помилку, не зупиняючи програму¶

Досі помилка вбивала процес. Але помилку можна впіймати — конструкцією try / except. Детально ми розберемо її в темі 18, а поки просто подивимось, що це можливо: програма переживає збій і працює далі.

In [9]:
try:
    vik = "17"
    suma = vik + 1          # тут буде TypeError
    print("сюди виконання не дійде:", suma)
except TypeError as pomylka:
    print("впіймали помилку типу:", type(pomylka).__name__)
    print("текст помилки        :", pomylka)

print("а програма живе далі — цей рядок надрукувався")
впіймали помилку типу: TypeError
текст помилки        : can only concatenate str (not "int") to str
а програма живе далі — цей рядок надрукувався

10 · Перевірка: наша версія проти бібліотечної¶

Обов'язковий ритуал курсу — переконатися, що всередині бібліотеки немає магії.

Останній рядок traceback має простий вигляд: Тип: пояснення. Напишемо власну функцію, яка складає такий рядок, і звіримо результат із тим, що дає стандартний модуль traceback. Якщо рядки збігаються — ми зрозуміли механізм правильно.

In [10]:
import traceback


def nash_ostannij_rjadok(pomylka):
    """Збирає останній рядок traceback так, як це робить сам Python: «Тип: текст»."""
    return f"{type(pomylka).__name__}: {pomylka}"


try:
    vik = "17"
    print(vik + 1)
except TypeError as pomylka:
    nash = nash_ostannij_rjadok(pomylka)
    # бібліотечна версія повертає список рядків; останній — той самий діагноз
    bibliotechnyj = traceback.format_exception_only(type(pomylka), pomylka)[-1].strip()

print("наш         :", nash)
print("бібліотечний:", bibliotechnyj)

assert nash == bibliotechnyj, "розрахунок розійшовся!"
print("✅ збігається")
наш         : TypeError: can only concatenate str (not "int") to str
бібліотечний: TypeError: can only concatenate str (not "int") to str
✅ збігається

11 · REPL проти файлу — на справжньому механізмі¶

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

Функція compile приймає третім аргументом режим:

  • "single" — режим REPL: після обчислення виразу результат друкується;
  • "exec" — режим файлу: результат просто викидається.
In [11]:
kod = "2 + 2"

print("режим REPL (single) — результат друкується сам:")
exec(compile(kod, "<repl>", "single"))

print()
print("режим файлу (exec) — той самий вираз:")
exec(compile(kod, "<file>", "exec"))
print("↑ порожньо: значення обчислилось і зникло")

print()
print("а з print працює в обох режимах:")
exec(compile("print(2 + 2)", "<file>", "exec"))
режим REPL (single) — результат друкується сам:
Out[11]:
4
режим файлу (exec) — той самий вираз:
↑ порожньо: значення обчислилось і зникло

а з print працює в обох режимах:
4

Ось де ховається та сама різниця в лапках, про яку ми говорили. REPL показує значення через repr — «як це виглядало б у коді», разом із лапками. print показує через str — «як це читає людина».

In [12]:
imya = "Оля"

print("те, що показав би REPL:", repr(imya))
print("те, що друкує print   :", str(imya))
print()
print("а це вже не косметика — довжина рядків різна:")
print("довжина repr :", len(repr(imya)))
print("довжина str  :", len(str(imya)))
те, що показав би REPL: 'Оля'
те, що друкує print   : Оля

а це вже не косметика — довжина рядків різна:
довжина repr : 5
довжина str  : 3

12 · Підсумкова таблиця помилок¶

Зберемо в одному місці чотири типи з лекції: викличемо кожен навмисно, впіймаємо й надрукуємо діагноз. Синтаксичні помилки не можна просто написати в клітинці (зошит би не запустився), тому їх ми отримуємо через compile — це якраз той крок, на якому Python перевіряє граматику.

In [13]:
# кожен рядок — це маленька програма, яку ми навмисно зламали
vypadky = [
    ("звернення до неіснуючого імені", "print(nevidome_imya)"),
    ("незакрита дужка", 'print("привіт"'),
    ("зайвий відступ", 'print("а")\n  print("б")'),
    ("текст плюс число", '"17" + 1'),
]

for opys, fragment in vypadky:
    try:
        # compile ловить помилки граматики, exec — помилки виконання
        exec(compile(fragment, "demo.py", "exec"))
        diahnoz = "помилки не сталося"
    except Exception as pomylka:
        diahnoz = f"{type(pomylka).__name__}: {pomylka}"
    print(f"{opys:32} → {diahnoz}")
звернення до неіснуючого імені   → NameError: name 'nevidome_imya' is not defined
незакрита дужка                  → SyntaxError: '(' was never closed (demo.py, line 1)
зайвий відступ                   → IndentationError: unexpected indent (demo.py, line 2)
текст плюс число                 → TypeError: can only concatenate str (not "int") to str

Чотири рядки — чотири різні причини падіння, і кожну видно з першого слова діагнозу. Саме тому в лекції ми й наполягали: читай останній рядок першим.

13 · Прибираємо за собою¶

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

In [14]:
import shutil

print("файли, які ми створили:")
for fayl in sorted(robocha_teka.iterdir()):
    print(f"  {fayl.name:18} {fayl.stat().st_size:>3} байт")

shutil.rmtree(robocha_teka)
print()
print("тимчасову теку прибрано:", not robocha_teka.exists())
файли, які ми створили:
  hello.py            35 байт
  name_error.py       52 байт
  syntax_error.py    115 байт
  type_error.py       26 байт

тимчасову теку прибрано: True

Завдання¶

🟢 Рівень 1 — База¶

Повтори кроки 3-4 у своєму терміналі, без зошита: створи теку, файл hello.py, перейди в теку командою cd і запусти файл.

Зроблено, якщо: термінал надрукував твій текст, а python3 --version показав 3.12 або новішу версію.

🟡 Рівень 2 — Плюс¶

Додай до zapustyty другий аргумент — список рядків, які треба передати програмі як аргументи командного рядка (sys.argv). Напиши файл, який друкує ці аргументи, і запусти його з трьома різними наборами.

Зроблено, якщо: твій файл надрукував рівно ті аргументи, які ти передав, і ти можеш пояснити, чому sys.argv[0] — це не перший аргумент, а ім'я самого файлу.

🔴 Рівень 3 — Виклик¶

Напиши функцію pidkazka(tekst_pomylky), яка приймає текст traceback і повертає одне речення українською з порадою: для NameError — «перевір, чи не одрукувався в імені», для SyntaxError — «шукай незакриту дужку або лапку», і так далі для чотирьох типів із таблиці.

Зроблено, якщо: функція дає правильну пораду для всіх чотирьох tracebacks, які ми отримали в цьому зошиті, і розумну відповідь за замовчуванням для невідомого типу.