Увесь цей блок ми вчилися працювати з реальністю: читати й писати файли, розбирати CSV і JSON, рахувати дати, шукати в тексті за зразком. Виходили корисні шматки коду — але жили вони в зошиті або у файлі, де шлях до даних був вписаний прямо в текст програми. Сьогодні ми перетворимо такий шматок на інструмент: програму, якій кажеш, що робити, прямо в момент запуску.
Це остання тема блоку, і вона склеює кілька попередніх у щось цілісне. Термінал ми відкривали
ще у темі 02, функції писали
в темі 14, загадковий рядок
if __name__ == "__main__" розібрали в темі 16
й пообіцяли показати, навіщо він потрібен насправді. Обіцянку виконуємо тут.
01 / МотивПрограма, яку не треба правити
Уяви звичайну задачу. Є файл prodazhi.csv з колонками data,
tovar, kilkist, suma. Треба порахувати підсумок по колонці
suma. Перше, що напише кожен:
with open("prodazhi.csv", encoding="utf-8") as f:
ryadky = list(csv.DictReader(f))
print(sum(float(r["suma"]) for r in ryadky))
Працює. Рівно один раз. Бо завтра прийде prodazhi-lypen.csv, післязавтра
знадобиться сума не по suma, а по kilkist, а через тиждень колега
попросить те саме для свого файлу. І щоразу доведеться відкривати код і правити рядок.
Здається, дрібниця. Насправді ця дрібниця коштує дорого, і ось чому:
- Кожна правка — шанс зламати. Ти міняєш назву файлу, випадково зачіпаєш сусідній символ — і програма падає або, значно гірше, тихо рахує не те.
- Немає історії. Через місяць неможливо згадати, з якими саме даними ти запускав програму, коли отримав той звіт. Файл коду вже інший.
- Це не можна автоматизувати. Розклад завдань, сервер збірки, скрипт колеги — усі вони вміють запускати програму, але жоден із них не вміє її редагувати.
Правильна картинка інша. Програма лишається незмінною, а те, що змінюється від запуску до запуску, передається їй ззовні — у момент виклику:
python3 zvit.py prodazhi-lypen.csv --kolonka kilkist
python3 zvit.py dani/kvartal.csv --dia serednye --tochnist 1
Слова після імені файлу називаються аргументами командного рядка (command-line arguments). Це рівно та сама ідея, що й параметри функції: тіло не змінюється, а поведінка залежить від того, що передали. Різниця лише в тому, хто передає. Функцію викликає інша функція, а скрипт викликає оболонка — і аргументи приходять не з коду, а з клавіатури.
zvit.py. Він читає CSV із продажами й друкує підсумок по числовій колонці. Ми
доростимо його від трьох рядків до повноцінної утиліти з довідкою, перевіркою аргументів
і чесними кодами виходу.02 / Найпростішеsys.argv: список слів
Аргументи потрапляють у програму найпростішим можливим способом: Python кладе їх у список
рядків sys.argv. Список, звичайні рядки, нічого більше.
print(sys.argv)
Запусти цей файл як python3 pokaz.py prodazhi.csv --tochnist 1 — і побачиш:
Три речі варто зауважити одразу, бо на кожній хтось спотикається.
- Нульовий елемент — це імʼя скрипта, а не інтерпретатора. Слова
python3у списку немає взагалі: воно адресоване оболонці, а не програмі. Тому «справжні» аргументи починаються зsys.argv[1]. - Усе всередині — рядки.
'1', а не1. Навіть якщо ти передав очевидне число, отримаєш текст, іsys.argv[3] + 1дастьTypeError. Перетворення типу — твоя робота. - Рядок на слова ріже не Python. Це робить оболонка ще до запуску програми. Python отримує вже готовий список.
Третій пункт найважливіший і найменш очевидний. Оболонка ділить командний рядок за пробілами, але спершу застосовує власні правила: лапки склеюють кілька слів в одне, зірочка розкривається у список наявних файлів, зворотна скісна екранує наступний символ. Подивись, як той самий, на перший погляд, рядок перетворюється на різні списки:
Інтерактив 1 · З рядка в список
Що саме бачить програма. Сірим — те, що дістається оболонці й до Python не доходить.
sys.argv[0] не завжди імʼя файлу.
При запуску python3 -c "код" там опиниться рядок '-c', при
python3 -m модуль — повний шлях до модуля, а в інтерактивному REPL — порожній рядок.
Тому будувати логіку на argv[0] — погана ідея; для повідомлень краще брати
sys.argv[0] лише як підказку, а справжнє імʼя програми задавати явно.03 / МежаДе sys.argv перестає тішити
Поки аргумент один, sys.argv — ідеальний інструмент. Проблеми починаються, щойно
зʼявляється другий. Порахуймо, що доведеться написати руками:
- перевірити кількість — інакше
sys.argv[1]кинеIndexErrorна порожньому запуску; - перетворити тип —
int()уtry, бо користувач напише--tochnist багатой отримаєValueErrorзамість зрозумілого повідомлення; - підставити типове значення, коли параметр не вказали;
- розібрати порядок — необовʼязкові параметри можуть стояти де завгодно, отже потрібен цикл із лічильником, а не просте індексування;
- написати довідку і не забувати оновлювати її при кожній зміні;
- повідомити про помилку — і не абикуди, а в потрібний потік, і вийти з правильним кодом.
Кожен пункт окремо — дрібниця. Разом вони дають кілька десятків рядків, які не мають жодного стосунку до задачі: у скрипті про підсумки CSV майже весь код виявляється про розбір аргументів. Перемикай вимоги й дивись, як росте ліва колонка:
Інтерактив 2 · Той самий скрипт двома способами
Ліворуч — руками через sys.argv, праворуч — через argparse. Вимоги додаються зліва направо.
руками · sys.argv
через argparse
І це ще оптимістична картина. Ліва колонка не вміє того, що права дає безкоштовно: не розуміє
злиті прапорці, не приймає форму --tochnist=1, не підказує схожу назву при одруку,
не вміє зупинити розбір на --. Дописати можна все — питання лише в тому, навіщо,
якщо це вже написано й лежить у стандартній бібліотеці.
04 / Інструментargparse за пʼять рядків
argparse — модуль стандартної бібліотеки, який бере на себе весь
розбір командного рядка. Нічого встановлювати не треба: він приїхав разом з інтерпретатором.
Робота з ним завжди складається з трьох дій.
Обʼєкт, який зберігатиме опис усіх аргументів. Текст із description
зʼявиться в довідці — це єдине, для чого він потрібен.
По одному виклику на кожен аргумент. Перший рядок — імʼя, решта — уточнення: тип, типове значення, перелік дозволених варіантів, текст довідки.
Тут відбувається все: argparse сам бере sys.argv, звіряє його
з описом, перетворює типи, підставляє типові значення — а якщо щось не так, друкує зрозуміле
повідомлення й завершує програму. До наступного рядка виконання просто не дійде.
Результат parse_args() — обʼєкт Namespace, у якого значення лежать
атрибутами: args.fajl, args.tochnist. Це не словник, звертатися через
квадратні дужки не вийде. Надрукувати його цілком корисно під час налагодження:
І найголовніше: разом із цими трьома кроками ти безкоштовно отримав довідку. Ніхто її
не писав — argparse зібрав її з опису аргументів. Спробуй запустити будь-який скрипт
із -h, і побачиш готовий текст, який завжди відповідає дійсності, бо генерується
з того самого джерела, що й розбір.
05 / ВидиПозиційні, необовʼязкові, прапорці
Аргументи бувають трьох сортів, і argparse розрізняє їх за однією ознакою —
чи починається імʼя з дефіса.
| сорт | як описати | як передати | поведінка |
|---|---|---|---|
| позиційний | add_argument("fajl") | zvit.py dani.csv | обовʼязковий, розпізнається за місцем |
| необовʼязковий | add_argument("-k", "--kolonka") | --kolonka suma | можна пропустити, порядок вільний |
| прапорець | add_argument("--tyho", action="store_true") | --tyho | значення не має: є або немає |
Позиційний аргумент — той, чиє призначення визначається місцем у рядку. Він
обовʼязковий за замовчуванням: якщо не передати, argparse лайне й вийде. Імʼя, яке
ти дав, стає і назвою атрибута, і підписом у довідці.
Необовʼязковий — той, що починається з дефіса. Прийнято описувати дві форми
одразу: коротку -k для щоденного набору й довгу --kolonka для
читабельності в скриптах. Імʼя атрибута argparse бере з довгої форми,
відкидає дефіси й міняє решту дефісів на підкреслення: --vyvid-fajl стане
args.vyvid_fajl.
Прапорець — окремий випадок необовʼязкового. action="store_true"
означає «якщо аргумент є в рядку, поклади True, інакше False».
Значення після нього не пишуть. Саме тому прапорці називають так: вони або підняті, або ні.
action="store_true" легко
переплутати з type=bool. Другий варіант виглядає логічно, але працює не так, як
здається: bool("False") — це True, бо непорожній рядок істинний.
Тобто --tyho False увімкне режим замість вимкнути. Для «так або ні» правильна
відповідь завжди action="store_true".Найкраще це відчувається руками. Нижче — конструктор: додавай аргументи прапорцями праворуч і дивись, як одночасно росте код і сама собою зʼявляється довідка.
Інтерактив 3 · Конструктор argparse
Обери, які аргументи має приймати zvit.py. Довідку ніхто не пише — вона збирається з опису.
zvit.py — код розбирача
python3 zvit.py -h — цей текст згенеровано
06 / Уточненняtype, default, choices, nargs
Один виклик add_argument приймає кілька уточнень, і кожне знімає з тебе окрему
ручну перевірку.
| уточнення | що робить | що знімає з тебе |
|---|---|---|
| type=int | перетворює рядок обраною функцією | try/except ValueError |
| default=2 | значення, коли аргумент не передали | перевірку «а чи вказали?» |
| choices=[…] | дозволяє лише перелічені значення | перевірку на список допустимих |
| required=True | робить необовʼязковий обовʼязковим | перевірку на None |
| nargs="+" | приймає кілька значень у список | ручний збір решти argv |
| metavar="N" | як значення зветься в довідці | — |
type= — найкорисніше. Важлива тонкість: туди передають
функцію, а не назву типу. int і float — це функції, тому вони
й працюють. Але туди можна віддати будь-яку функцію, яка приймає рядок і повертає значення:
type=Path одразу дасть обʼєкт з
pathlib, а власна функція може перевіряти, що число додатне, і кидати
argparse.ArgumentTypeError із людським поясненням.
default= без явного значення дорівнює None. Дрібна
зручність: у тексті help можна написати %(default)s, і
argparse підставить туди фактичне типове значення. Тоді довідка не бреше навіть
після того, як ти передумав.
nargs= задає, скільки значень поглинає аргумент:
"+" — одне або більше (список ніколи не порожній), "*" — нуль або
більше, "?" — нуль або одне, ціле число — рівно стільки. Перші два дають
список, і це варто памʼятати: nargs="+" перетворює
args.fajl на args.fajly, по якому треба ходити циклом.
required=True. Технічно можна зробити
--kolonka обовʼязковим. Але подумай, що це означає: користувач мусить писати
--kolonka завжди, хоча «необовʼязковий» у назві натякає на протилежне. Спільнота
домовилась просто: обовʼязкове — позиційне, необовʼязкове — з дефісами. Порушувати цю угоду
варто лише тоді, коли обовʼязкових речей кілька й запамʼятати їхній порядок неможливо.Тепер найцікавіше: подивитись, як конкретний набраний рядок розкладається по цих параметрах. І що буває, коли не розкладається:
Інтерактив 4 · Куди йде кожне слово
Ліворуч — слова з командного рядка, праворуч — поля Namespace. Сірим — значення за замовчуванням.
07 / ВідповідьКоди виходу
Кожна програма, завершуючись, повертає операційній системі одне ціле число. Воно зветься кодом виходу (exit code, або status code) і відповідає рівно на одне питання: все пройшло добре чи ні.
Домовленість стара, проста й спільна для всіх мов:
| код | що означає | звідки береться в Python |
|---|---|---|
| 0 | успіх | програма дійшла до кінця без винятку |
| 1 | помилка виконання | sys.exit(1) або неперехоплений виняток |
| 2 | помилка в аргументах | argparse при поганому командному рядку |
| 3…125 | свої значення | що завгодно, що ти задокументував |
Побачити код останньої команди можна одразу в терміналі: echo $? на macOS і Linux,
echo $LASTEXITCODE у PowerShell. Нуль — усе гаразд; будь-яке інше число — щось
пішло не так.
Чому це має значення для скрипта, який пишеш ти? Бо твій скрипт майже напевно запускатиме не людина. Розклад завдань, сервер збірки, інший скрипт — усі вони не читають твої повідомлення. Вони дивляться на число. І якщо програма впала, але повернула нуль, ланцюг спокійно поїде далі з порожнім або зіпсованим файлом.
Найпростіший спосіб побудувати ланцюг — оператор &&: «виконай наступну
команду, тільки якщо попередня повернула нуль». Покрути перемикач і подивись, де ланцюг
зупиняється:
Інтерактив 5 · Ланцюг команд через &&
Три команди підряд. Ненульовий код зупиняє ланцюг, нульовий пропускає далі.
sys.exit. Функція приймає не лише число.
sys.exit("файл не знайдено") надрукує цей рядок у stderr і поверне код
1. Зручно, але легко переплутати з print: рядок піде не туди, куди
ти думав. І окремо: sys.exit(0) усередині try/except буде перехоплений
блоком except Exception? Ні — SystemExit успадковується не від
Exception, а від BaseException саме для цього. А от голий
except: його проковтне, і програма не завершиться (детальніше про ієрархію —
у темі 18).08 / Потокиstdout проти stderr
У кожної програми, яка працює в терміналі, є три канали звʼязку зі світом. Їх називають стандартними потоками:
stdin— звідки програма читає вхідні дані;stdout— куди пише результат роботи;stderr— куди пише повідомлення про перебіг і помилки.
Звичайний print пише в stdout. Щоб написати в stderr,
достатньо додати один іменований аргумент:
print("оброблено 1240 рядків", file=sys.stderr) # звіт про перебіг → stderr
Навіщо два потоки, якщо обидва зрештою потрапляють на той самий екран? Бо на екран вони потрапляють лише тоді, коли програму запустила людина. Оболонка вміє перенаправляти їх окремо, і саме тут різниця стає відчутною:
| запис | що робить |
|---|---|
| > zvit.txt | stdout — у файл; stderr лишається на екрані |
| 2> pomylky.log | stderr — у файл; stdout лишається на екрані |
| | sort | stdout — на вхід наступної програми; stderr на екран |
| > zvit.txt 2>&1 | обидва в один файл (двійка «стає» одиницею) |
| 2> /dev/null | stderr викинути зовсім |
Звідси правило, яке відрізняє акуратний скрипт від неохайного: у stdout іде тільки результат. Усе інше — у stderr. Заголовки, індикатори прогресу, «читаю файл…», попередження — усе це в другий потік. Перемкни, куди пише програма, і подивись, що станеться:
Інтерактив 6 · Куди що потрапляє
Той самий скрипт друкує два рядки: результат і службове повідомлення.
stdout веде не на термінал,
а у файл чи в трубу, Python буферизує його блоками по кілька кілобайтів, а stderr
з версії 3.9 лишається порядковим. Побічний ефект помітний одразу: у файлі
> usе.log 2>&1 рядки з двох потоків можуть перемішатися не в тому порядку,
у якому їх друкували. Ліки — print(..., flush=True) у важливих місцях або запуск
з python3 -u.09 / ТрубаКонвеєр і stdin
Вертикальна риска | зветься конвеєром (pipe) і робить рівно одну
річ: зʼєднує stdout лівої програми зі stdin правої. Це той механізм,
завдяки якому дрібні утиліти складаються в довгі ланцюги:
Щоб твій скрипт умів стояти в такому ланцюгу, він має читати не тільки з файлу, а й зі
stdin. У Python це не окремий механізм: sys.stdin — звичайний
текстовий файловий обʼєкт, по якому можна ходити циклом рядок за рядком.
for ryadok in sys.stdin:
print(len(ryadok.rstrip("\n")))
Домовленість, яку розуміють майже всі утиліти: одинокий дефіс замість імені файлу
означає «читай зі stdin». У argparse це робиться однією конструкцією —
type=argparse.FileType("r") сам відкриє файл, а на дефісі підставить
sys.stdin.
FileType. Зручно, але з підступом: файл
відкривається під час розбору аргументів і ніколи не закривається автоматично. Для
короткого скрипта це не проблема, для довгого — витік дескрипторів. Надійніший підхід — приймати
рядок зі шляхом, а відкривати самому через with: тоді і момент відкриття, і момент
закриття під твоїм контролем.І ще одна деталь, на якій спотикаються всі, хто вперше пише утиліту для конвеєра. Команда
python3 zvit.py velykyj.csv | head -3 завершиться помилкою
BrokenPipeError: head прочитала три рядки, закрилася, а твоя програма
все ще пише в трубу, якої вже немає. Це не поломка — це нормальний кінець життя в конвеєрі, і
акуратний скрипт ловить цей виняток та тихо виходить.
10 / ФормаСтруктура зрілого скрипта
Тепер зберімо все докупи. Ось zvit.py цілком — у тому вигляді, у якому такі речі
пишуть у справжніх проєктах:
"""Підсумок по числовій колонці CSV-файлу з продажами."""
import argparse
import csv
import sys
def zrobyty_rozbyrach():
# окрема функція, щоб розбирач можна було перевірити в тесті
parser = argparse.ArgumentParser(
description="Рахує підсумок по колонці CSV-файлу з продажами.")
parser.add_argument("fajl", help="шлях до CSV-файлу")
parser.add_argument("-k", "--kolonka", default="suma",
metavar="НАЗВА",
help="назва колонки (типово: %(default)s)")
parser.add_argument("--tyho", action="store_true",
help="друкувати лише число, без заголовка")
return parser
def main(argv=None):
args = zrobyty_rozbyrach().parse_args(argv)
try:
with open(args.fajl, encoding="utf-8", newline="") as f:
ryadky = list(csv.DictReader(f))
except OSError as pomylka:
# повідомлення — у stderr, щоб не змішатися з результатом
print(f"zvit.py: {pomylka}", file=sys.stderr)
return 1
if not ryadky or args.kolonka not in ryadky[0]:
print(f"zvit.py: немає колонки {args.kolonka!r}",
file=sys.stderr)
return 1
pidsumok = sum(float(r[args.kolonka]) for r in ryadky)
# результат — і тільки він — іде в stdout
if args.tyho:
print(f"{pidsumok:.2f}")
else:
print(f"сума по колонці {args.kolonka}: {pidsumok:.2f}")
return 0
if __name__ == "__main__":
sys.exit(main())
У цій формі три рішення, і кожне має причину.
Не на верхньому рівні файлу. Тоді модуль можна імпортувати, і нічого не станеться — рівно те, про що говорила тема 16: у файлі, який хтось може імпортувати, на верхньому рівні тільки означення.
Це різниця між «функцію можна протестувати» і «функція вбиває тест». Виклик
main(["prodazhi.csv", "--tyho"]) з тесту
поверне число, яке легко порівняти. Якби всередині стояв sys.exit, тест довелося
б обгортати в pytest.raises(SystemExit) і діставати код із винятку.
Ось де код виходу нарешті потрапляє в операційну систему. main() повертає
число, sys.exit віддає його назовні. Один рядок — і скрипт чесно відповідає
ланцюгу з &&.
Параметр argv=None у main — теж не випадковість.
parse_args(None) означає «бери sys.argv[1:]», тобто при звичайному
запуску все працює як зазвичай. А в тесті можна передати свій список і перевірити скрипт, не
запускаючи окремий процес.
#!. Він зветься shebang і потрібен
на macOS та Linux: після chmod +x zvit.py файл можна запускати як
./zvit.py prodazhi.csv, без слова python3. Форма
#!/usr/bin/env python3 краща за прямий шлях, бо шукає інтерпретатор у PATH — а отже
підхопить той, що з віртуального середовища. На Windows рядок ігнорується як звичайний
коментар.11 / ПрактикаТренажер: запусти сам
Теорія закінчилась. Нижче — модель того самого zvit.py: не справжній Python, але
розбір аргументів, повідомлення про помилки й коди виходу відтворені точно. У теці лежить
prodazhi.csv — 1240 рядків із колонками data, tovar,
kilkist, suma.
Інтерактив 7 · Тренажер запуску
Пиши команду й тисни Enter. Червоним — те, що йде в stderr.
12 / ГлибшеДля тих, хто вже писав скрипти
Далі — речі, які новачку поки не потрібні, але економлять час тому, хто вже щось писав.
argparse проти click і typer
argparse — у стандартній бібліотеці, нічого ставити не треба, але описувати
аргументи доводиться багатослівно. click робить те саме
декораторами над функцією: менше тексту, зручні підкоманди, кольори й інтерактивні запити з
коробки. typer іде далі й будує інтерфейс просто з
сигнатури функції та її анотацій типів — імена
параметрів стають іменами аргументів, а типи — перетворювачами.
Обидва — сторонні пакети, тобто плюс залежність. Просте правило: якщо скрипт має жити поруч
із даними й запускатися будь-де без установки — argparse. Якщо це утиліта проєкту
з десятком підкоманд — typer зекономить помітно більше, ніж коштує залежність.
Підкоманди
Коли утиліта вміє кілька різних дій, аргументами це виражати незручно. Правильний спосіб —
підкоманди, як у git commit чи pip install. У
argparse їх дає parser.add_subparsers(): кожна підкоманда отримує
власний розбирач із власними аргументами й власною довідкою. Правило вибору просте: різні дії з
різними наборами параметрів — підкоманди; одна дія з варіаціями — choices.
Чому не можна друкувати в stdout зайве
Ми вже бачили це в інтерактиві, але варто сформулювати як закон. Усе, що потрапило в
stdout, є частиною результату — не тому, що так гарніше, а тому, що наступна програма
в конвеєрі не має способу відрізнити твій дружній заголовок від даних. Рядок «Обробляю файл…»,
надрукований у stdout, у ланцюгу | sort | head -1 перетвориться на першу «найменшу»
стрічку звіту.
Практичний наслідок: у скрипті, який щось віддає, print без
file=sys.stderr має бути рівно там, де ти віддаєш результат. Усе решта — прогрес,
попередження, «знайдено 12 файлів» — у stderr. Тоді і людина все побачить, і
конвеєр не зламається.
Дрібниці, які тішать
- Скорочення довгих імен.
argparseза замовчуванням приймає будь-який однозначний префікс:--kolспрацює як--kolonka. Зручно вручну, небезпечно в скриптах: додаси колись--koloryt— і чужий скрипт зламається зambiguous option. Вимикається параметромallow_abbrev=False. - Подвійний дефіс як межа. Усе після голого
--вважається значеннями, а не аргументами. Єдиний спосіб передати файл із назвою--dyvnyj.csv. BooleanOptionalAction(з версії 3.9) створює одразу пару--tyhoі--no-tyho— корисно, коли типове значенняTrueі його треба вміти вимкнути.parse_known_args()повертає пару «розібране, решта» замість того, щоб падати на незнайомому аргументі. Так роблять обгортки, що передають хвіст командного рядка іншій програмі.- Змінні середовища. Те, що не хочеться писати щоразу (ключі, адреси
серверів), беруть не з аргументів, а з
os.environ. Звична схема —default=os.environ.get("ZVIT_KOLONKA", "suma"): аргумент перекриває змінну, змінна перекриває типове значення.
13 / ПідсумокЩо ми тепер уміємо
Ми навчилися перетворювати код на інструмент, яким може скористатися хтось інший — або ти сам через півроку:
sys.argv— список рядків, де нульовий елемент є імʼям скрипта; ріже рядок оболонка, а не Python;argparse— три кроки: створити розбирач, описати аргументи, розібрати; даліNamespaceз готовими значеннями;- три сорти аргументів — позиційні за місцем, необовʼязкові з дефісами,
прапорці через
action="store_true"; - уточнення —
type,default,choices,nargs: кожне знімає одну ручну перевірку; - довідка
-h— генерується з опису й тому ніколи не застаріває; - коди виходу — 0 успіх, ненульовий помилка, 2 у
argparse; саме на них дивиться той, хто запускає скрипт автоматично; - два потоки — результат у
stdout, усе інше вstderr, бо перенаправляються вони окремо; main()+sys.exit(main())— форма, у якій скрипт водночас і програма, і модуль, який можна протестувати.
На цьому блок 5 закінчено. Озирнись: ми навчилися читати й писати файли, розуміти табличні й вкладені формати, працювати з датами й текстом — а сьогодні ще й запускати все це ззовні. Цього достатньо, щоб написати справжню робочу утиліту.
І саме тут стає видно наступну межу. Подивись на наш zvit.py: усередині ходять
голі рядки й числа. Рядок з args.kolonka, список словників із CSV, число-підсумок.
Поки програма робить одну дію, це нормально. Але щойно зʼявиться поняття «продаж» зі своєю датою,
товаром, кількістю й сумою — і власними правилами, як його перевіряти, порівнювати й друкувати, —
таскати це в словниках стане боляче. Потрібен спосіб описати власний тип: сказати, які
дані має продаж і що з ним можна робити. Це класи й
обʼєкти, з яких починається блок 6.
practice.ipynb ти напишеш справжній
скрипт із argparse у тимчасовій теці, запустиш його через subprocess
з різними аргументами й перевіриш assert-ами найважливіше: що код виходу нульовий
при успіху й другий при поганих аргументах, що повідомлення про помилку пішло в
stderr, а не в stdout, і що довідка -h справді
згенерувалася сама.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.