Практика 02 · Встановлення й перший запуск¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
У лекції ми говорили про інтерпретатор, термінал і traceback. Тут ми зробимо все це руками — але не в терміналі, а прямо із зошита, щоб кожен крок було видно.
Що зробимо:
- З'ясуємо, який саме Python виконує цей зошит і де він лежить.
- Створимо перший файл
hello.pyі запустимо його — тим самим інтерпретатором. - Зламаємо програму тричі й прочитаємо кожен traceback по частинах.
- Напишемо власну функцію, яка збирає останній рядок traceback, і звіримо її з бібліотечною.
- Побачимо на власні очі, чим режим REPL відрізняється від режиму файлу.
Нічого встановлювати не треба: усе, що потрібно, є в стандартній бібліотеці.
1 · Хто саме виконує цей зошит¶
Перше питання, яке варто ставити щоразу, коли «щось не працює»: а який Python зараз працює?
Відповідь дає модуль sys. sys.executable — це шлях до самого інтерпретатора, той самий,
що знайшовся б у PATH за командою python3.
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().
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, коли ми зламаємо програму.
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 означає «все добре», будь-що інше — «впало».
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.
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 по частинах¶
Це той самий текст, який ти щойно бачив, — але тепер розкладений на рядки з підписами. Саме в такому порядку його читає досвідчена людина: спершу останній рядок, потім номер рядка, потім усе решта.
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¶
Тепер зламаємо граматику: не закриємо дужку. І подивимось на дві речі, яких не було в попередньому випадку.
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? ''
Два спостереження, і обидва важливі:
- Шапки «Traceback» немає. Вона описує шлях виконання — а виконання й не починалося: файл не пройшов перевірку граматики.
stdoutпорожній. Хоча зламано другий рядок, першийprintтеж не спрацював. Синтаксис перевіряється для всього файлу до запуску.
Порівняй це з NameError: там програма встигла попрацювати й упала вже на ходу.
8 · Помилка третя: TypeError¶
Класика: значення виглядає як число, але насправді це текст.
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, а поки просто подивимось, що це можливо: програма
переживає збій і працює далі.
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.
Якщо рядки збігаються — ми зрозуміли механізм правильно.
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"— режим файлу: результат просто викидається.
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) — результат друкується сам:
4
режим файлу (exec) — той самий вираз: ↑ порожньо: значення обчислилось і зникло а з print працює в обох режимах: 4
Ось де ховається та сама різниця в лапках, про яку ми говорили. REPL показує значення через
repr — «як це виглядало б у коді», разом із лапками. print показує через str —
«як це читає людина».
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
перевіряє граматику.
# кожен рядок — це маленька програма, яку ми навмисно зламали
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 · Прибираємо за собою¶
Тимчасова тека нам більше не потрібна. Але спершу подивимось, що ми в ній наробили — чотири файли, кожен зі своєю історією.
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, які ми отримали в цьому зошиті, і розумну відповідь за замовчуванням для невідомого типу.