Урок 15. Файли, менеджери контексту та JSON¶
В уроці 14 програма навчилася розбирати рядки каси: чотири правильні чеки приймає, п'ять зіпсованих пропускає й пояснює чому. Але рядки були вписані прямо в код, а результат жив лише доти, доки працювала програма. Закрив ноутбук — звіту немає.
Наприкінці липня каса вивантажила місячний файл kasa_2024_07.txt. Власниця хоче, щоб програма сама його прочитала, склала звіт і зберегла його у файл, який відкриє сайт кафе чи бухгалтер. Касирові — окремий файл з переліком зіпсованих рядків, щоб їх виправити. А щоб знати, коли звіт запускали, — журнал запусків.
Що потрібно з попередніх уроків: рядки й f-strings (урок 3), словники (урок 6), функції (урок 8), ітератори й for (урок 11), datetime і Counter (урок 13), try / except, parse_line і load_orders (урок 14).
Після уроку ти зможеш:
- читати текстовий файл цілком і рядок за рядком через
with open(...); - пояснювати, чому
withзакриває файл навіть тоді, коли всередині стався виняток; - обирати режим відкриття:
r— читати,w— перезаписати,a— дописати в кінець; - розбиратися з відносними шляхами й
FileNotFoundError, користуватисяpathlib.Path; - зберігати словники у JSON і читати їх назад, знати, які типи JSON не підтримує;
- тримати налаштування програми в JSON-файлі, а не в коді; читати CSV модулем
csv.
Задача розділу. Прочитати kasa_2024_07.txt, зберегти звіт у report_2024_07.json, зіпсовані рядки — в errors_2024_07.txt, а запуск — у журнал runs.log. Повний код — у розділі «Практика».
Ноутбук заняття: Відкрити вправи в Colab Переглянути розв’язки
Пригадай¶
Дай відповідь подумки, нічого не запускаючи:
- Що повертає
load_orders(lines)з уроку 14? - У програмі є словник
report. Програма завершилась. Де теперreport? - Що надрукує цикл
for x in iterator:, якщо ітератор уже вичерпали раніше?
Відповіді
- Пару: список прийнятих чеків
RawOrderі список пояснень до пропущених рядків, наприклад"рядок 3: could not convert string to float: '540,00'". - Ніде. Змінні живуть в оперативній пам'яті процесу, а після завершення програми цю пам'ять віддають системі.
- Нічого: вичерпаний ітератор більше нічого не дає (урок 11). Відкритий файл поводиться так само.
Пам'ять і диск¶
flowchart TD
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
K["kasa_2024_07.txt<br>файл каси"] --> R["читання<br>with open(..., 'r')"]
R --> P["load_orders<br>урок 14"]
P --> B["звіт — словник<br>у пам'яті"]
B --> J["report_2024_07.json<br>для сайту й бухгалтера"]
P --> E["errors_2024_07.txt<br>для касира"]
B --> L["runs.log<br>журнал запусків"]
class K,J,E,L success
class R,P,B step
Зелені блоки — файли на диску: вони переживають завершення програми. Сірі — робота в пам'яті: змінні, списки, словники. Програма читає файл у пам'ять, працює з даними і записує результат назад на диск.
Python не працює з диском сам. open() просить операційну систему відкрити файл і отримує файловий об'єкт — «ручку», через яку читають і пишуть. Коли роботу закінчено, ручку треба повернути: закрити файл.
Читаємо файл каси¶
Ось що вивантажила каса. Це ті самі рядки, що в уроці 14, тепер у файлі:
2024-07-19 18:30;540.00;50;2
2024-07-19 12:10;320.00;30;1
2024-07-19 19:05;540,00;40;3
2024-07-20 20:15;980.00;120;4
2024-07-20 13:40;760.00
2024-02-30 19:00;450.00;0;5
2024-07-21 18:00;-120.00;0;2
2024-07-21 14:20;610.00;60;0
2024-07-21 21:30;1200.00;150;6
Файл лежить у тій самій теці, що й програма. Прочитаємо його цілком:
with open("kasa_2024_07.txt", "r", encoding="utf-8") as file:
text = file.read()
print(type(text), len(text))
print(text.splitlines()[0])
open(шлях, режим, encoding=...)відкриває файл. Режим"r"(read) — читання, він стоїть за замовчуванням;encoding="utf-8"— як перетворювати байти на букви. Без нього Python візьме кодування системи, і на Windows кирилиця може перетворитися наРЇчи впасти зUnicodeDecodeError. Для текстових файлів курсу завжди пишемоutf-8;file.read()повертає весь вміст одним рядкомstr. Для файлу каси на 258 символів це нормально, для файлу на гігабайт — ні.
Рядок за рядком¶
Відкритий файл — ітератор рядків, як генератор з уроку 11. Цикл for читає по одному рядку і не тримає в пам'яті весь файл:
with open("kasa_2024_07.txt", encoding="utf-8") as file:
for number, line in enumerate(file, start=1):
if number <= 2:
print(repr(line))
Кожен рядок закінчується символом переходу на новий рядок \n. Якщо його не прибрати, останнє поле буде "2\n". int("2\n") таке пробачить, а порівняння рядків чи ключ словника — ні. Тому рядок з файлу спершу чистять: line.rstrip("\n") або просто line.strip().
Загорнемо читання у функцію. Порожні рядки (наприклад, зайвий Enter у кінці файлу) пропускаємо: це не зіпсовані чеки, їх просто немає.
def read_kasa(path):
"""Непорожні рядки файлу каси без символу нового рядка."""
lines = []
with open(path, encoding="utf-8") as file:
for line in file:
line = line.strip()
if line:
lines.append(line)
return lines
lines = read_kasa("kasa_2024_07.txt")
print(len(lines), lines[-1])
Навіщо with¶
with open(...) as file: відкриває файл, а коли блок з відступом закінчується, сам його закриває:
Без with файл закривають вручну, file.close(). Але якщо між open і close станеться виняток, до close черга не дійде, і файл лишиться відкритим. with закриває файл за будь-якого виходу з блоку, так само як finally з уроку 14:
try:
with open("kasa_2024_07.txt", encoding="utf-8") as file:
first = file.readline()
average = 860.0 / 0
except ZeroDivisionError:
print("виняток усередині with")
print(file.closed)
Незакриті файли — це витік ресурсів: операційна система дає програмі обмежену кількість відкритих файлів. Найнеприємніше з записом: дані, які не встигли записатися до закриття, можуть не потрапити на диск. with називають менеджером контексту (context manager): він сам виконує дію на вході в блок і дію на виході з нього.
Розбираємо і записуємо¶
parse_line і load_orders беремо з уроку 14 без змін:
Код з уроку 14: RawOrder, parse_line, load_orders
from datetime import datetime
from typing import NamedTuple
class RawOrder(NamedTuple):
total_bill: float
tip: float
size: int
timestamp: datetime
def parse_line(line):
"""Рядок каси -> RawOrder. Зіпсований рядок -> ValueError з поясненням."""
fields = line.split(";")
if len(fields) != 4:
raise ValueError(f"очікували 4 поля, а маємо {len(fields)}")
time_text, bill_text, tip_text, size_text = fields
timestamp = datetime.strptime(time_text, "%Y-%m-%d %H:%M")
bill, tip, size = float(bill_text), float(tip_text), int(size_text)
if bill <= 0:
raise ValueError(f"сума чека має бути більшою за 0, а маємо {bill}")
if tip < 0:
raise ValueError(f"чайові не можуть бути від'ємними: {tip}")
if size < 1:
raise ValueError(f"гостей має бути хоча б один, а маємо {size}")
return RawOrder(bill, tip, size, timestamp)
def load_orders(lines):
"""Правильні чеки і список пояснень до пропущених рядків."""
orders = []
errors = []
for number, line in enumerate(lines, start=1):
try:
orders.append(parse_line(line))
except ValueError as error:
errors.append(f"рядок {number}: {error}")
return orders, errors
Режим w: файл для касира¶
Режим "w" (write) створює файл, а якщо він уже є — стирає його вміст і пише з нуля. Для переліку помилок за місяць це саме те, що треба: кожен запуск дає свіжий перелік.
with open("errors_2024_07.txt", "w", encoding="utf-8") as file:
for message in errors:
file.write(message + "\n")
with open("errors_2024_07.txt", encoding="utf-8") as file:
print(file.read(), end="")
рядок 3: could not convert string to float: '540,00'
рядок 5: очікували 4 поля, а маємо 2
рядок 6: day is out of range for month
рядок 7: сума чека має бути більшою за 0, а маємо -120.0
рядок 8: гостей має бути хоча б один, а маємо 0
Дві особливості write:
- він не додає
\nсам, на відміну відprint. Забудеш — усі повідомлення зліпляться в один рядок; - він приймає лише рядки:
file.write(5)дастьTypeError. Число спершу перетворюють:file.write(str(5))або f-string.
Замість write можна писати звичним print з параметром file: print(message, file=file). Він сам додасть \n і перетворить числа на текст.
Режим a: журнал запусків¶
Журнал має накопичуватися: кожен запуск додає рядок у кінець і не стирає попередні. Для цього режим "a" (append):
from pathlib import Path
Path("runs.log").unlink(missing_ok=True) # починаємо з чистого журналу
def log_run(message, path="runs.log"):
with open(path, "a", encoding="utf-8") as file:
print(message, file=file)
log_run("2024-07: прийнято 4, пропущено 5")
log_run("2024-07: прийнято 4, пропущено 5")
with open("runs.log", encoding="utf-8") as file:
print(file.read(), end="")
Два запуски — два рядки. З режимом "w" лишився б один: кожен запуск стирав би журнал.
| Режим | Файл є | Файлу немає | Для чого в кафе |
|---|---|---|---|
"r" |
читає з початку | FileNotFoundError |
файл каси, налаштування |
"w" |
стирає і пише з нуля | створює | звіт і помилки за місяць |
"a" |
дописує в кінець | створює | журнал запусків |
w не питає
open("kasa_2024_07.txt", "w") миттєво стирає файл каси, навіть якщо ти нічого не записав. Перш ніж відкривати файл на запис, перевір ім'я: вхідні дані й результати краще називати по-різному.
Крім r, w, a, є ще режим x («лише новий файл»), модифікатори + (читати й писати) і b (байти). Вони не потрібні для звіту кафе — повна таблиця в розділі Поглиблення: усі режими open().
Шляхи і FileNotFoundError¶
"kasa_2024_07.txt" — відносний шлях: Python шукає файл у поточній теці (current working directory). Це тека, з якої запустили програму, а не обов'язково та, де лежить .py-файл:
python report.pyз теки проєкту — поточна тека проєкту;python cafe/report.pyз теки вище — поточна тека вище, іopen("kasa_2024_07.txt")шукатиме файл там;- у Jupyter — тека ноутбука, у Colab —
/content, куди потрапляють файли з панелі 📁.
Коли файлу немає, open у режимі "r" піднімає FileNotFoundError. Як і в уроці 14, ловимо його там, де знаємо, що відповісти людині:
path = Path("kasa_2024_08.txt")
print(path.exists())
try:
read_kasa(path)
except FileNotFoundError as error:
print("Каса ще не вивантажила файл:", error.filename)
FileNotFoundError — різновид OSError, як і PermissionError («немає прав на файл»). Path.cwd() покаже поточну теку, якщо не зрозуміло, де Python шукає файли.
pathlib.Path — шлях як об'єкт, а не рядок. Частини шляху з'єднують оператором /, і Python сам поставить правильний роздільник для системи:
folder = Path("kasa")
path = folder / "kasa_2024_07.txt"
print(path.name, path.stem, path.suffix)
print(path.parent.name)
open приймає і рядок, і Path. Path має й зручні методи: path.exists(), path.read_text(encoding="utf-8"), path.write_text(text, encoding="utf-8").
JSON: звіт, який прочитає інша програма¶
Звіт — це словник. Чому б не записати його через str()?
report = {"cafe": "Смачно", "orders": 4, "revenue": 3040.0, "open": True, "note": None}
with open("report.txt", "w", encoding="utf-8") as file:
file.write(str(report))
with open("report.txt", encoding="utf-8") as file:
back = file.read()
print(type(back))
print(back[:12])
Назад повертається рядок, а не словник: back["orders"] не спрацює. А сайт кафе, написаний не на Python, взагалі не знає, що таке True і None. Потрібен формат, який розуміють усі мови й з якого словник збирається назад.
JSON (JavaScript Object Notation) — текстовий формат, схожий на словники й списки Python. Його розуміє практично кожна мова програмування, і саме ним обмінюються сайти й сервери. Модуль json зі стандартної бібліотеки перетворює Python ↔ JSON:
import json
text = json.dumps(report, ensure_ascii=False)
print(text)
print(json.loads(text) == report)
Лапки в JSON лише подвійні, True став true, None — null. json.loads зібрав з тексту той самий словник.
| Функція | Що робить |
|---|---|
json.dumps(obj) |
об'єкт → рядок JSON (s — string) |
json.loads(text) |
рядок JSON → об'єкт |
json.dump(obj, file) |
об'єкт → у відкритий файл |
json.load(file) |
з відкритого файлу → об'єкт |
Два параметри для людей: ensure_ascii=False залишає кирилицю кирилицею, а indent=2 розбиває JSON на рядки з відступами. Без ensure_ascii=False українські літери стануть кодами:
Це теж правильний JSON, і json.loads поверне «Смачно», але людина такий файл не прочитає.
Що JSON вміє, а що ні¶
| Python | JSON |
|---|---|
dict |
об'єкт {...}, ключі — лише рядки |
list, tuple |
масив [...] |
str |
рядок у подвійних лапках |
int, float |
число |
True, False, None |
true, false, null |
Прогноз: що повернеться після запису й читання?
from collections import Counter
data = {"best_day": ("сб", 980.0), 7: "липень", "by_time": Counter({"вечеря": 3, "обід": 1})}
back = json.loads(json.dumps(data, ensure_ascii=False))
print(back)
Три тихі зміни: кортеж повернувся списком, ключ 7 — рядком '7', а Counter — звичайним словником. Помилки немає, але back[7] дасть KeyError. Для звіту кафе це не страшно, якщо про це пам'ятати.
А datetime JSON не знає зовсім:
Дату записують рядком у стандартному форматі ISO 8601 і відновлюють з нього:
Зіпсований JSON¶
Якщо в JSON-файлі помилка (одинарні лапки, зайва кома, файл записали через str()), json.loads / json.load піднімає json.JSONDecodeError. Це різновид ValueError, тож обробляється так само, як в уроці 14:
try:
json.loads("{'orders': 4}")
except json.JSONDecodeError as error:
print(isinstance(error, ValueError))
print(error)
Повідомлення вказує рядок і позицію: у колонці 2 чекали ", а стоїть '.
Налаштування у файлі¶
У коді звіту досі вшиті назва кафе й імена файлів. Щоб перейменувати файл каси, доведеться лізти в код. Налаштування краще тримати окремо, у JSON:
{
"cafe": "Кафе «Смачно»",
"currency": "грн",
"kasa_file": "kasa_{year}_{month:02d}.txt",
"report_file": "report_{year}_{month:02d}.json",
"errors_file": "errors_{year}_{month:02d}.txt",
"log_file": "runs.log"
}
with open("cafe_config.json", encoding="utf-8") as file:
config = json.load(file)
print(config["cafe"])
print(config["kasa_file"].format(year=2024, month=7))
"kasa_{year}_{month:02d}.txt" — шаблон: str.format підставляє значення в {} за тими самими правилами, що f-string (:02d — два знаки з нулем попереду). f-string тут не підійде: його значення обчислюються одразу в коді, а шаблон приходить з файлу як звичайний рядок.
Тепер касир може змінити назву файлу, а бухгалтер — валюту, не відкриваючи Python.
CSV: таблиця в тексті¶
Сайт замовлень кафе вивантажує дані в інший формат — CSV (comma-separated values): перший рядок — назви колонок, далі по рядку на запис, значення через кому:
order_id,customer_name,dish,price,order_date,city
1,Anna,Pizza,320,2026-03-10 12:30,Kyiv
2,Oleh,Burger,210,2026-03-10 13:10,Lviv
Розбирати такий файл через split(",") ризиковано: кома може трапитися всередині значення, у лапках. Модуль csv робить це правильно. csv.DictReader віддає кожен рядок словником з ключами-назвами колонок:
import csv
with open("orders.csv", encoding="utf-8", newline="") as file:
rows = list(csv.DictReader(file))
print(len(rows))
print(rows[0]["dish"], repr(rows[0]["price"]))
print(sum(float(row["price"]) for row in rows))
Усі значення з CSV — рядки, навіть '320': перетворювати їх на числа й дати — наша робота, з тими самими ValueError, що в уроці 14. newline="" радить документація модуля csv: так він сам правильно обробляє переходи на новий рядок.
Для великих таблиць зазвичай беруть бібліотеку pandas: pd.read_csv("orders.csv"). Приклад — у ноутбуці заняття викладача file_json_example.ipynb .
Практика¶
Розібраний приклад: місячний звіт кафе¶
Зберемо все разом. Правила з уроку 13 (день тижня і прийом їжі) — у згорнутому блоці:
Правила з уроку 13: DAYS, meal_type_from_hour
Що вийшло у файлі звіту — його побачить сайт кафе:
{
"cafe": "Кафе «Смачно»",
"currency": "грн",
"period": "2024-07",
"orders": 4,
"skipped": 5,
"revenue": 3040.0,
"average": 760.0,
"by_time": {
"вечеря": 3,
"обід": 1
},
"first_order": "2024-07-19T12:10:00"
}
Що відбувається в ключових рядках:
- рядок 1 —
build_reportлише рахує і повертає словник, файлів не торкається. Її легко перевірити на кількох чеках; - рядки 6 і 14 — усе, що JSON не знає, перетворюється заздалегідь:
datetime→ рядок ISO,Counter→dict.if orders elseзахищає від порожнього місяця:min()порожнього списку підняв биValueError; - рядки 20–22 — імена файлів з шаблонів конфігу, жодного вшитого імені;
- рядки 25–26 —
json.dumpпише просто у відкритий файл,ensure_ascii=Falseіindent=2— для людей; - рядки 29–30 — перелік помилок перезаписується (
"w"), журнал дописується ("a", всерединіlog_run); - рядки 32–33 —
mainловитьFileNotFoundErrorі відповідає людською мовою, якmainв уроці 14.month_reportпро людей нічого не знає.
А тепер програма, якою бухгалтер відкриє звіт, — теж Python, хоча могла б бути будь-якою мовою:
with open("report_2024_07.json", encoding="utf-8") as file:
saved = json.load(file)
print(f"{saved['cafe']}, {saved['period']}: {saved['revenue']:.2f} {saved['currency']}")
print("Вечері:", saved["by_time"]["вечеря"])
Зміни приклад: найкращий день і чайові¶
Додай до build_report два ключі:
"best_day"— день тижня ("пн"…"нд") з найбільшим виторгом;"tips_percent"— чайові як відсоток від виторгу, округлені до одного знака.
І перший рядок у файлі помилок: Пропущено рядків: 5.
Очікувані значення для липня:
Критерії перевірки:
report_2024_07.jsonпісля запуску містить обидва нові ключі;- для місяця без жодного правильного чека
build_reportне падає:"best_day"—null,"tips_percent"—0.0; errors_2024_07.txtпісля двох запусків поспіль містить заголовок один раз.
Підказка
Виторг за днями — словник, як revenue_by_day в уроці 8, тільки ключ — DAYS[order.timestamp.weekday()]. Найкращий день — max(revenue_by_day, key=revenue_by_day.get), але лише якщо словник непорожній. Заголовок пиши першим print у тому самому with open(..., "w").
Спробуй самостійно: картка постійного гостя¶
Кафе запускає картки лояльності: за кожен візит гість отримує бал. Бали мають зберігатися між запусками програми в loyalty.json, наприклад:
Напиши три функції:
load_loyalty(path)— словник з файлу; якщо файлу ще немає (перший запуск) — порожній словник{};add_visit(cards, name)— додає гостеві бал, новому гостю створює картку з балом 1;save_loyalty(cards, path)— записує словник у файл, кирилицею і з відступами.
Перевірка — «запуск програми двічі»:
перший запуск: {} → після двох візитів Оксани і одного Тараса → {'Оксана': 2, 'Тарас': 1}
другий запуск: {'Оксана': 2, 'Тарас': 1} → ще візит Оксани → {'Оксана': 3, 'Тарас': 1}
Правила:
- відсутній файл — через
try/except FileNotFoundError, а неPath.exists()(EAFP з уроку 14); - файл читається й пишеться лише через
with; add_visitне торкається файлів.
Знайди помилку¶
У кожному фрагменті одна помилка. Що піде не так?
# 1 — журнал
with open("runs.log", "w", encoding="utf-8") as file:
file.write("2024-07: прийнято 4, пропущено 5")
# 2 — перелік помилок
with open("errors_2024_07.txt", "w", encoding="utf-8") as file:
for message in errors:
file.write(message)
# 3 — звіт
with open("report_2024_07.json", encoding="utf-8") as file:
saved = json.loads(file)
Відповіді
- Режим
"w"стирає журнал при кожному запуску: лишиться лише останній рядок. Для журналу потрібен"a", а ще\nв кінці рядка. writeне додає\n: п'ять повідомлень зліпляться в один рядок. Требаfile.write(message + "\n")абоprint(message, file=file).json.loadsчекає рядок, а отримує файловий об'єкт:TypeError. Для файлу —json.load(file)безs.
Поглиблення: усі режими open()¶
Для звіту кафе вистачає трьох режимів: r, w, a. Цей розділ можна пропустити й повернутися до нього, коли знадобиться захистити файл від перезапису, виправити файл «на місці» або прочитати не текст, а байти.
Режим x: не затерти готовий звіт¶
Звіт за липень уже відправили бухгалтеру. Якщо хтось запустить програму вдруге з "w", файл тихо перезапишеться. Замінимо одну літеру: "w" → "x" (exclusive creation):
import json
from pathlib import Path
Path("report_2024_07.json").unlink(missing_ok=True) # починаємо з чистого стану
report = {"period": "2024-07", "orders": 4, "revenue": 3040.0}
for attempt in (1, 2):
try:
with open("report_2024_07.json", "x", encoding="utf-8") as file:
json.dump(report, file, ensure_ascii=False)
print(f"Спроба {attempt}: звіт створено")
except FileExistsError as error:
print(f"Спроба {attempt}: звіт уже є —", error.filename)
"x" створює лише новий файл. Якщо файл уже є, open не чіпає його і піднімає FileExistsError. Це дзеркало "r": той падає, коли файлу немає, а "x" — коли файл є.
Перевір себе: чому тут не варто спершу перевіряти Path(...).exists(), а потім відкривати з "w"?
Відповідь
Між перевіркою і відкриттям файл може з'явитися (інша програма, другий запуск). "x" перевіряє і створює одним кроком — це той самий EAFP з уроку 14.
Знак +: читати й писати одним відкриттям¶
+ додає до базового режиму другу дію: r+, w+, a+, x+ дозволяють і читати, і писати. Але що станеться з файлом і де стоїть курсор, вирішує базова літера. Перевіримо всі вісім текстових режимів на файлі зі словом "дані":
from pathlib import Path
for mode in ["r", "w", "a", "x", "r+", "w+", "a+", "x+"]:
if "x" in mode:
Path("probe.txt").unlink(missing_ok=True) # x потребує, щоб файлу не було
else:
Path("probe.txt").write_text("дані", encoding="utf-8")
with open("probe.txt", mode, encoding="utf-8") as file:
cursor = "початок" if file.tell() == 0 else "кінець"
content = Path("probe.txt").read_text(encoding="utf-8")
print(f"{mode:<3} читати: {file.readable()!s:<5} писати: {file.writable()!s:<5} "
f"курсор: {cursor:<7} у файлі: {content!r}")
r читати: True писати: False курсор: початок у файлі: 'дані'
w читати: False писати: True курсор: початок у файлі: ''
a читати: False писати: True курсор: кінець у файлі: 'дані'
x читати: False писати: True курсор: початок у файлі: ''
r+ читати: True писати: True курсор: початок у файлі: 'дані'
w+ читати: True писати: True курсор: початок у файлі: ''
a+ читати: True писати: True курсор: кінець у файлі: 'дані'
x+ читати: True писати: True курсор: початок у файлі: ''
Три висновки з таблиці:
+змінює лише колонку «читати / писати»;wіw+стерли файл одразу під час відкриття, ще до будь-якого запису;aіa+ставлять курсор у кінець, томуread()одразу після відкриття поверне"".
r+ пише поверх, а не вставляє. У файлі зміни написано зміна: відкрита. Виправимо на закрита:
Path("shift.txt").write_text("зміна: відкрита", encoding="utf-8")
with open("shift.txt", "r+", encoding="utf-8") as file:
print("прочитано:", file.read())
file.seek(0) # курсор назад на початок
file.write("зміна: закрита")
print("у файлі: ", Path("shift.txt").read_text(encoding="utf-8"))
Звідки друге а? r+ не очищає файл: новий текст ліг поверх старого, а старий був на літеру довший. Хвіст обрізає file.truncate() після запису. На практиці простіше прочитати файл з "r", змінити текст у пам'яті й записати заново з "w".
w+ і a+: курсор після запису.
with open("shift.txt", "w+", encoding="utf-8") as file:
file.write("зміна: закрита")
print("після write: ", repr(file.read()))
file.seek(0)
print("після seek(0):", repr(file.read()))
Після write курсор стоїть за останнім записаним символом, тож read() там нічого не знаходить. Щоб прочитати щойно записане, повертаємо курсор: file.seek(0).
a+ теж можна перемотати на початок, щоб прочитати. Але запис у режимі a/a+ завжди йде в кінець, навіть після seek(0): журнал не зіпсуєш випадковим записом посередині.
Літера b: байти замість тексту¶
Досі ми читали текст (str): Python сам перетворював байти з диска на літери за кодуванням encoding="utf-8". Літера b вимикає це перетворення:
Path("menu.txt").write_text("Кава 60", encoding="utf-8")
with open("menu.txt", encoding="utf-8") as file:
text = file.read()
with open("menu.txt", "rb") as file:
data = file.read()
print(type(text).__name__, len(text), repr(text))
print(type(data).__name__, len(data), data)
print(data.decode("utf-8"))
7 літер, але 11 байтів: кожна кирилична літера в UTF-8 займає 2 байти. bytes.decode("utf-8") перетворює байти назад на текст.
Двійковий режим потрібен для файлів, які не є текстом: картинки, PDF, архіви, копіювання файлу «як є». Дві помилки, які в ньому трапляються:
open("menu.txt", "rb", encoding="utf-8") # ValueError: binary mode doesn't take an encoding argument
with open("copy.bin", "wb") as file:
file.write("Кава") # TypeError: a bytes-like object is required, not 'str'
У двійковому режимі кодування немає, бо немає перетворення на текст. Писати теж треба байти: "Кава".encode("utf-8").
Зведення: режими, модифікатори, комбінації¶
4 базові режими — що станеться з файлом:
| Режим | Назва | Файл є | Файлу немає | Курсор | Для чого в кафе |
|---|---|---|---|---|---|
r |
read | читає з початку | FileNotFoundError |
на початку | файл каси, налаштування |
w |
write | стирає одразу | створює | на початку | звіт і помилки за місяць |
a |
append | дописує в кінець | створює | у кінці | журнал запусків |
x |
exclusive creation | FileExistsError |
створює | на початку | звіт, який не можна перезаписати |
3 модифікатори — додаються до базового режиму:
| Модифікатор | Що змінює | Приклад |
|---|---|---|
t |
текстовий режим: str, потрібне encoding (стоїть за замовчуванням) |
"r" = "rt" |
b |
двійковий режим: bytes, без encoding |
"rb", "wb" |
+ |
додає другу дію: і читати, і писати | "r+", "a+" |
16 різних комбінацій — 8 текстових і 8 двійкових:
| Текстові | Двійкові | Читати | Писати | Стирає файл |
|---|---|---|---|---|
r або rt |
rb |
✅ | — | — |
w або wt |
wb |
— | ✅ | ✅ |
a або at |
ab |
— | ✅ в кінець | — |
x або xt |
xb |
— | ✅ | файлу не було |
r+ або r+t |
r+b |
✅ | ✅ поверх | — |
w+ або w+t |
w+b |
✅ | ✅ | ✅ |
a+ або a+t |
a+b |
✅ | ✅ в кінець | — |
x+ або x+t |
x+b |
✅ | ✅ | файлу не було |
Порядок літер не важливий: "rb+", "r+b" і "br+" — той самий режим (Python покаже його як file.mode == "rb+"). Але базова літера має бути рівно одна: "rw" дасть ValueError.
Як обрати режим:
| Задача | Режим |
|---|---|
| Прочитати текстовий файл | "r" (можна не писати) |
| Записати результат заново, старе не потрібне | "w" |
| Додати рядок у журнал, не стираючи попередні | "a" |
| Створити файл, але ніколи не перезаписати наявний | "x" |
| Прочитати й змінити той самий файл | "r" → змінити в пам'яті → "w"; рідше "r+" |
| Картинка, PDF, архів, копія файлу «як є» | "rb" / "wb" |
Головне про небезпечні режими
w і w+ очищають наявний файл одразу після відкриття. r+ не очищає, але пише поверх старого вмісту.
Підсумок¶
| Поняття | Що запам'ятати |
|---|---|
with open(path, mode, encoding="utf-8") |
файл закриється сам, навіть після винятку |
read() і for line in file |
весь текст одним рядком або рядок за рядком; strip() прибирає \n |
"r", "w", "a" |
читати; стерти й записати; дописати в кінець |
"x", +, b |
лише новий файл; читати й писати; байти замість тексту — таблиця режимів |
| Відносний шлях | шукається від поточної теки; немає файлу — FileNotFoundError |
| JSON | dump/load — файл, dumps/loads — рядок; кортеж → список, ключі → рядки |
datetime у JSON |
isoformat() і datetime.fromisoformat() |
| CSV | csv.DictReader; усі значення — рядки |
Самоперевірка¶
- Чим
with open(...) as file:кращий заfile = open(...)іfile.close()? - Файл відкрили в режимі
"w", але нічого не записали. Що сталося з його вмістом? - Чому
file.read()вдруге поспіль повертає порожній рядок? - Програма
cafe/report.pyвідкриває"kasa_2024_07.txt", файл лежить поруч з нею, а виходитьFileNotFoundError. Чому? - Чому звіт зберігають у JSON, а не через
str(report)? - У звіті був ключ
7. Як дістати значення післяjson.load? - Чим
json.loadвідрізняється відjson.loads?
Відповіді
withзакриває файл за будь-якого виходу з блоку, зокрема після винятку. Ручнийclose()після винятку не виконається.- Вміст стерто:
"w"очищає файл одразу під час відкриття. - Файл — ітератор з курсором. Перший
read()дочитав до кінця, і курсор лишився там. Потрібно відкрити файл знову (абоfile.seek(0)). - Відносний шлях шукається від поточної теки, з якої запустили програму, а не від теки файлу
.py. Запусти програму з текиcafeабо побудуй шлях відPath(__file__).parent. str()дає текст, з якого словник назад не збирається, і його не прочитає програма іншою мовою. JSON відновлюється черезjson.loadу той самий словник і зрозумілий будь-якій мові.saved["7"]: ключі в JSON — лише рядки.json.load(file)читає з відкритого файлу,json.loads(text)— з рядка.
Що далі¶
- Ноутбук заняття: Відкрити вправи в Colab Переглянути розв’язки — той самий місяць каси: ноутбук сам створює файл каси й конфіг, далі прогнози, вправи й перевірки.
- Довідник:
notes_file_io_json.ipynb— курсор файлу (
tell,seek), типи JSON докладно, форматування таблиць, телефонна книга. Стислий повтор — File I/O та JSON. - Наступне заняття: Урок 16. Git + GitHub. Файли проєкту кафе вже є — час зберігати їхню історію і показувати код іншим.
Документація і джерела¶
- Туторіал Python: Reading and Writing Files, Saving structured data with json
open()— режими й параметри;pathlibjson,csv,datetime.isoformat- Глосарій: context manager; оператор
with - Специфікація JSON: json.org (українською)
- Для охочих:
- Harvard CS50P, лекція 6 «File I/O» —
open,with, CSV,csv.DictReader; - Princeton, Introduction to Programming in Python, розділ 1.5 «Input and Output» — стандартний ввід/вивід, перенаправлення у файли.
- Harvard CS50P, лекція 6 «File I/O» —