Практика · Файли й pathlib¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Тема 20, перша в блоці «Робота з реальністю». Тут ми не читатимемо про файли — ми їх створимо, зіпсуємо, відновимо й приберемо за собою.
Що зробимо по кроках:
- Створимо тимчасову теку — усе, що нижче, живе тільки в ній.
- Запишемо прайс і прочитаємо його трьома способами, довівши, що результат однаковий.
- Побачимо на власні очі, що
"w"стирає файл у мить відкриття. - Прогонимо режими
r,a,xі зловимо їхні винятки. - Порівняємо наївний
close()ізwithна коді, що падає. - Відтворимо мотлох з інтерактиву 3 байт у байт і зловимо
UnicodeDecodeError. - Розберемо
Pathна частини й побудуємо дерево тек черезmkdir(parents=True). - Перевіримо всі шість шаблонів
globз інтерактиву 6 на справжніх файлах. - Подивимось, скільки байтів насправді на диску до
flush(). - Приберемо теку повністю — зошит не має лишати сміття.
Мережа не потрібна. Усі дані ми створюємо самі, нічого не завантажується.
1 · Тимчасова тека: пісочниця, яку не шкода¶
Перше правило експериментів із файлами — не експериментуй у робочій теці. Модуль
tempfile створює теку з унікальною назвою в системному місці для тимчасових файлів,
а shutil.rmtree наприкінці прибере її разом із усім вмістом.
import shutil
import tempfile
from pathlib import Path
# mkdtemp повертає РЯДОК зі шляхом; загортаємо його в Path, щоб далі користуватись
# оператором «/» і не клеїти шляхи вручну
робоча_тека = Path(tempfile.mkdtemp(prefix="praktyka-20-"))
print("працюємо в:", робоча_тека)
print("тека існує:", робоча_тека.exists(), "· це саме тека:", робоча_тека.is_dir())
print("усередині зараз:", list(робоча_тека.iterdir()))
працюємо в: /tmp/praktyka-20-s26s5cdz тека існує: True · це саме тека: True усередині зараз: []
2 · Перший запис: with, encoding і той самий \n¶
Три речі в одному рядку open: шлях (складений оператором /), режим "w" і
обовʼязкове encoding="utf-8". І одразу нагадування з лекції: write не додає
перехід на новий рядок, тому ми дописуємо \n самі.
прайс = робоча_тека / "prays.txt" # «/» ставить правильний роздільник на будь-якій системі
рядки_прайсу = ["хліб;28.5", "молоко;32.0", "яблука;19.0"]
with open(прайс, "w", encoding="utf-8") as f:
for рядок in рядки_прайсу:
f.write(рядок + "\n") # без цього «\n» усі три рядки склеїлись би в один
print("створено файл:", прайс.name)
print("байтів на диску:", прайс.stat().st_size)
# змінна f живе й після блоку, але сам файл уже закритий — це і є робота with
print("файл закрито після виходу з with:", f.closed)
створено файл: prays.txt байтів на диску: 50 файл закрито після виходу з with: True
Пʼятдесят байтів на дванадцять українських літер і кілька цифр — це не помилка. Кирилична літера в UTF-8 коштує два байти, латиниця й цифри — один. Перевіримо руками, щоб число перестало бути магічним.
# рахуємо очікуваний розмір самі: кожен рядок плюс один байт на «\n»
очікуваних_байтів = sum(len(рядок.encode("utf-8")) + 1 for рядок in рядки_прайсу)
print("порахували вручну:", очікуваних_байтів)
print("операційна система каже:", прайс.stat().st_size)
assert очікуваних_байтів == прайс.stat().st_size, "розрахунок розійшовся з диском!"
print("✅ збігається — жодної магії, просто сума довжин у байтах")
порахували вручну: 50 операційна система каже: 50 ✅ збігається — жодної магії, просто сума довжин у байтах
3 · Три способи прочитати той самий файл¶
read() віддає весь текст одним рядком, readlines() — список рядків разом із \n,
обхід циклом — по одному рядку за раз. Подивимось на кожен окремо.
with open(прайс, encoding="utf-8") as f:
увесь_текст = f.read()
print("тип:", type(увесь_текст).__name__)
print("символів:", len(увесь_текст))
print(repr(увесь_текст)) # repr показує «\n» явно — саме тому він тут, а не print
тип: str символів: 34 'хліб;28.5\nмолоко;32.0\nяблука;19.0\n'
with open(прайс, encoding="utf-8") as f:
список_рядків = f.readlines()
print("тип:", type(список_рядків).__name__, "· елементів:", len(список_рядків))
for номер, рядок in enumerate(список_рядків, 1):
print(номер, repr(рядок)) # хвостовий «\n» лишився в кожному елементі
тип: list · елементів: 3 1 'хліб;28.5\n' 2 'молоко;32.0\n' 3 'яблука;19.0\n'
зібрані_циклом = []
with open(прайс, encoding="utf-8") as f:
for рядок in f: # файловий обʼєкт сам є послідовністю рядків
зібрані_циклом.append(рядок)
print("зібрано рядків:", len(зібрані_циклом))
print("останній:", repr(зібрані_циклом[-1]))
зібрано рядків: 3 останній: 'яблука;19.0\n'
А тепер головне: три різні способи мають дати однаковий результат. Це наша перша
перевірка через assert — і вона доводить, що обхід циклом не «спрощений» варіант,
а рівноцінний.
assert увесь_текст == "".join(список_рядків), "read() і readlines() розійшлись!"
assert список_рядків == зібрані_циклом, "readlines() і цикл розійшлись!"
print("read() → один рядок на", len(увесь_текст), "символів")
print("readlines() → список із", len(список_рядків), "елементів")
print("цикл → зібрано", len(зібрані_циклом), "елементів, по одному за раз")
print("✅ усі три способи дали той самий вміст — різниця лише у витраті памʼяті")
read() → один рядок на 34 символів readlines() → список із 3 елементів цикл → зібрано 3 елементів, по одному за раз ✅ усі три способи дали той самий вміст — різниця лише у витраті памʼяті
Розібрати рядок на назву й ціну — те, заради чого ми файл і читали. Зверни увагу на
rstrip("\n"): без нього ціна останнього товару була б рядком "19.0\n".
товари = {}
with open(прайс, encoding="utf-8") as f:
for рядок in f:
назва, ціна = рядок.rstrip("\n").split(";") # зрізаємо перехід рядка ПЕРЕД розбором
товари[назва] = float(ціна)
print(товари)
assert товари == {"хліб": 28.5, "молоко": 32.0, "яблука": 19.0}
print("✅ прайс розібрано у словник")
{'хліб': 28.5, 'молоко': 32.0, 'яблука': 19.0}
✅ прайс розібрано у словник
4 · "w" стирає файл у мить відкриття¶
Найдорожча пастка теми. Відкриємо файл на запис без with — щоб зазирнути в
проміжок між open і write, коли ще нічого не записано, а вміст уже зник.
розмір_до = прайс.stat().st_size
# навмисно без with: нам треба подивитись на файл ДО того, як щось записали
f = open(прайс, "w", encoding="utf-8")
розмір_після_open = прайс.stat().st_size # жодного write ще не було
f.close()
print("байтів до open :", розмір_до)
print("байтів одразу після open:", розмір_після_open)
assert розмір_до == 50 and розмір_після_open == 0, "«w» мав обрізати файл ще до write"
print("✅ підтверджено: три рядки прайсу зникли від самого факту відкриття")
байтів до open : 50 байтів одразу після open: 0 ✅ підтверджено: три рядки прайсу зникли від самого факту відкриття
Прайс ми щойно знищили — відновимо його. Path.write_text робить те саме, що
with open(..., "w") плюс один write, тільки коротше.
відновлено_символів = прайс.write_text("\n".join(рядки_прайсу) + "\n", encoding="utf-8")
print("write_text повернув кількість записаних СИМВОЛІВ:", відновлено_символів)
print("а на диску БАЙТІВ:", прайс.stat().st_size)
assert прайс.stat().st_size == розмір_до, "відновлений файл має бути таким самим"
print("✅ прайс на місці; символів менше, ніж байтів, — бо кирилиця в UTF-8 двобайтова")
write_text повернув кількість записаних СИМВОЛІВ: 34 а на диску БАЙТІВ: 50 ✅ прайс на місці; символів менше, ніж байтів, — бо кирилиця в UTF-8 двобайтова
немає_такого = робоча_тека / "nemaye.txt"
try:
with open(немає_такого, "r", encoding="utf-8") as f:
f.read()
except FileNotFoundError as e:
print("режим «r» на відсутньому файлі →", type(e).__name__)
print("повідомлення:", e)
print("чи створив «r» файл?", немає_такого.exists())
assert not немає_такого.exists(), "«r» не має нічого створювати"
режим «r» на відсутньому файлі → FileNotFoundError повідомлення: [Errno 2] No such file or directory: '/tmp/praktyka-20-s26s5cdz/nemaye.txt' чи створив «r» файл? False
try:
with open(прайс, "x", encoding="utf-8") as f:
f.write("це ніколи не запишеться\n")
except FileExistsError as e:
print("режим «x» на наявному файлі →", type(e).__name__)
print("байтів у прайсі після невдалої спроби:", прайс.stat().st_size)
assert прайс.stat().st_size == 50, "«x» не має чіпати наявний файл"
print("✅ саме за це «x» і люблять: помилка замість тихого знищення")
режим «x» на наявному файлі → FileExistsError байтів у прайсі після невдалої спроби: 50 ✅ саме за це «x» і люблять: помилка замість тихого знищення
with open(прайс, "a", encoding="utf-8") as f:
f.write("сіль;12.0\n") # «a» завжди пише в кінець, хай там що
рядки_після_дозапису = прайс.read_text(encoding="utf-8").splitlines()
print("рядків стало:", len(рядки_після_дозапису))
for рядок in рядки_після_дозапису:
print(" ", рядок)
assert len(рядки_після_дозапису) == 4 and рядки_після_дозапису[0] == "хліб;28.5"
print("✅ старі три рядки цілі, четвертий дописано в хвіст")
рядків стало: 4 хліб;28.5 молоко;32.0 яблука;19.0 сіль;12.0 ✅ старі три рядки цілі, четвертий дописано в хвіст
6 · with проти наївного close() на коді, що падає¶
Той самий код із інтерактиву 2. Спершу наївний варіант: close() стоїть після
рядка, який кине ZeroDivisionError.
звіт_наївно = робоча_тека / "zvit-naivno.txt"
f = open(звіт_наївно, "w", encoding="utf-8")
try:
f.write("молоко;32.0\n")
ціна_за_літр = 32.0 / 0 # ось тут усе й обірветься
f.write(f"за літр: {ціна_за_літр}\n")
f.close() # цей рядок недосяжний
except ZeroDivisionError:
print("виняток спіймано зовні")
print("файл закрито?", f.closed)
print("байтів на диску:", звіт_наївно.stat().st_size)
assert not f.closed, "close() не викликано — файл мусить лишитись відкритим"
assert звіт_наївно.stat().st_size == 0, "буфер не скинуто — на диску має бути порожньо"
print("✅ файл відкритий, записаний рядок висить у буфері, на диску нуль байтів")
f.close() # прибираємо за собою руками, бо автоматика тут не спрацювала
виняток спіймано зовні файл закрито? False байтів на диску: 0 ✅ файл відкритий, записаний рядок висить у буфері, на диску нуль байтів
Тепер той самий код у with. Виняток нікуди не подівся — він так само летить далі,
але файл закрито, а буфер скинуто.
звіт_with = робоча_тека / "zvit-with.txt"
try:
with open(звіт_with, "w", encoding="utf-8") as g:
g.write("молоко;32.0\n")
ціна_за_літр = 32.0 / 0
g.write(f"за літр: {ціна_за_літр}\n")
except ZeroDivisionError:
print("виняток спіймано зовні — with його НЕ проковтнув")
print("файл закрито?", g.closed)
print("байтів на диску:", звіт_with.stat().st_size)
assert g.closed, "with мусить закрити файл навіть після винятку"
assert звіт_with.stat().st_size == len("молоко;32.0\n".encode("utf-8"))
print("✅ 18 байтів долетіли до диска, файл закрито — і все це без жодного close() у коді")
виняток спіймано зовні — with його НЕ проковтнув файл закрито? True байтів на диску: 18 ✅ 18 байтів долетіли до диска, файл закрито — і все це без жодного close() у коді
7 · Кодування: рахуємо байти й відтворюємо мотлох¶
Повторимо інтерактив 3 на справжніх файлах. Спершу подивимось, у скільки байтів перетворюється те саме слово різними кодуваннями.
слово = "хліб;28.5"
у_utf8 = слово.encode("utf-8")
у_cp1251 = слово.encode("cp1251")
print("символів у рядку:", len(слово))
print("utf-8 :", у_utf8.hex(" ").upper(), "→", len(у_utf8), "байтів")
print("cp1251:", у_cp1251.hex(" ").upper(), "→", len(у_cp1251), "байтів")
assert len(у_utf8) == 13 and len(у_cp1251) == 9
print("✅ ті самі 13 і 9 байтів, що в інтерактиві 3")
символів у рядку: 9 utf-8 : D1 85 D0 BB D1 96 D0 B1 3B 32 38 2E 35 → 13 байтів cp1251: F5 EB B3 E1 3B 32 38 2E 35 → 9 байтів ✅ ті самі 13 і 9 байтів, що в інтерактиві 3
Тепер тиха біда: записали UTF-8, прочитали cp1251. Винятку не буде — буде мотлох, і програма спокійно поїде далі.
файл_utf8 = робоча_тека / "utf8.txt"
файл_utf8.write_text(слово, encoding="utf-8")
мотлох = файл_utf8.read_text(encoding="cp1251")
print("оригінал:", слово, "→", len(слово), "символів")
print("прочитане:", мотлох, "→", len(мотлох), "символів")
assert мотлох == "хліб;28.5", "мотлох має бути саме таким, як у лекції"
print("✅ 13 байтів витлумачено як 13 однобайтових символів — і жодної скарги")
оригінал: хліб;28.5 → 9 символів прочитане: хліб;28.5 → 13 символів ✅ 13 байтів витлумачено як 13 однобайтових символів — і жодної скарги
А тепер гучна біда: записали cp1251, читаємо UTF-8. Байт 0xF5 не може починати
символ у UTF-8, тому Python зупиняється на найпершому байті.
файл_cp1251 = робоча_тека / "cp1251.txt"
файл_cp1251.write_text(слово, encoding="cp1251")
try:
файл_cp1251.read_text(encoding="utf-8")
except UnicodeDecodeError as e:
print("тип:", type(e).__name__)
print("повідомлення:", e)
# у винятку лежать самі байти й точна позиція — розбирати текст помилки не треба
print("позиція:", e.start, "· проблемний байт:", hex(e.object[e.start]))
assert e.start == 0 and e.object[e.start] == 0xF5
print("✅ гучна біда краща за тиху: ми дізнались про неї одразу")
тип: UnicodeDecodeError повідомлення: 'utf-8' codec can't decode byte 0xf5 in position 0: invalid start byte позиція: 0 · проблемний байт: 0xf5 ✅ гучна біда краща за тиху: ми дізнались про неї одразу
І чесний аварійний вихід: errors="replace". Він рятує від падіння, але це втрата
даних — переконаймось у цьому числами.
з_заміною = файл_cp1251.read_text(encoding="utf-8", errors="replace")
print("що вийшло:", repr(з_заміною))
print("символів:", len(з_заміною), "· в оригіналі:", len(слово))
assert з_заміною != слово, "після заміни це вже не той самий текст"
print("⚠️ падіння немає, але кириличні літери зникли безповоротно")
що вийшло: '���;28.5' символів: 8 · в оригіналі: 9 ⚠️ падіння немає, але кириличні літери зникли безповоротно
8 · Path розібраний на частини¶
Той самий шлях, що в інтерактиві 5. Головна перевірка — .stem і .suffixes на
назві з двома крапками.
шлях = Path("/home/olena") / "dani" / "prays.2026.csv"
print("повний шлях :", шлях)
print(".parent :", шлях.parent)
print(".name :", шлях.name)
print(".stem :", шлях.stem)
print(".suffix :", шлях.suffix)
print(".suffixes :", шлях.suffixes)
print(".parts :", шлях.parts)
assert шлях.stem == "prays.2026", "«2026» — частина назви, а не розширення"
assert шлях.suffix == ".csv"
assert шлях.suffixes == [".2026", ".csv"]
assert len(шлях.parts) == 5
print("✅ усе збіглося з інтерактивом 5")
повний шлях : /home/olena/dani/prays.2026.csv
.parent : /home/olena/dani
.name : prays.2026.csv
.stem : prays.2026
.suffix : .csv
.suffixes : ['.2026', '.csv']
.parts : ('/', 'home', 'olena', 'dani', 'prays.2026.csv')
✅ усе збіглося з інтерактивом 5
І доказ того, що Path не торкається диска, доки ти не попросиш: обʼєкт для
неіснуючого файлу створюється без жодної скарги.
вигаданий = Path("/nemaye/takoyi/teky/zvit.csv")
print("обʼєкт створено:", вигаданий)
print("розширення:", вигаданий.suffix, "· тека:", вигаданий.parent.name)
print("а файл існує?", вигаданий.exists())
assert not вигаданий.exists()
print("✅ Path — це текст із розумом, а не запит до файлової системи")
обʼєкт створено: /nemaye/takoyi/teky/zvit.csv розширення: .csv · тека: teky а файл існує? False ✅ Path — це текст із розумом, а не запит до файлової системи
9 · Дерево тек через mkdir(parents=True, exist_ok=True)¶
Відтворимо дерево з інтерактиву 6 — рівно ті самі девʼять файлів у трьох теках.
projekt = робоча_тека / "projekt"
# parents=True створює й «projekt», і «dani», і «stari» одним викликом
(projekt / "dani" / "stari").mkdir(parents=True, exist_ok=True)
(projekt / "utils").mkdir(parents=True, exist_ok=True)
файли_дерева = [
"main.py", "README.md", "prays.txt",
"dani/sichen.csv", "dani/lyutyi.csv",
"dani/stari/2024.csv", "dani/stari/nazvy.txt",
"utils/__init__.py", "utils/chytach.py",
]
for відносний in файли_дерева:
(projekt / відносний).write_text("# заглушка\n", encoding="utf-8")
усе_дерево = list(projekt.rglob("*"))
print("файлів створено:", len(файли_дерева))
print("усього записів у дереві (файли + теки):", len(усе_дерево))
assert len(усе_дерево) == 12, "у лекції в дереві рівно 12 записів"
print("✅ дерево таке саме, як в інтерактиві 6")
файлів створено: 9 усього записів у дереві (файли + теки): 12 ✅ дерево таке саме, як в інтерактиві 6
Навіщо потрібен exist_ok=True? Щоб повторний запуск програми не падав. Покажемо
обидві поведінки поруч.
(projekt / "dani" / "stari").mkdir(parents=True, exist_ok=True)
print("повторний mkdir з exist_ok=True: тиша, як і має бути")
try:
(projekt / "dani" / "stari").mkdir(parents=True) # без exist_ok
except FileExistsError as e:
print("той самий рядок без exist_ok →", type(e).__name__)
print("✅ саме тому exist_ok=True пишуть майже завжди: повторний запуск — не аварія")
повторний mkdir з exist_ok=True: тиша, як і має бути той самий рядок без exist_ok → FileExistsError ✅ саме тому exist_ok=True пишуть майже завжди: повторний запуск — не аварія
10 · Усі шість шаблонів glob на справжніх файлах¶
Прогонимо кожен шаблон з інтерактиву 6 і звіримо кількість знахідок із тим, що
показувала лекція. Якщо хоч одне число не збіжиться — assert це впіймає.
очікувана_кількість = {"*.py": 1, "*": 5, "*/*.csv": 2, "**/*.csv": 3, "**/*.py": 3, "**/*": 12}
for шаблон, очікуємо in очікувана_кількість.items():
# relative_to робить вивід читабельним: без довгого шляху тимчасової теки
знайдено = sorted(п.relative_to(projekt).as_posix() for п in projekt.glob(шаблон))
print(f"{шаблон:<9} → {len(знайдено):>2} {знайдено}")
assert len(знайдено) == очікуємо, f"{шаблон}: очікували {очікуємо}, знайшли {len(знайдено)}"
print("\n✅ усі шість шаблонів дали рівно ті числа, що й інтерактив 6")
*.py → 1 ['main.py'] * → 5 ['README.md', 'dani', 'main.py', 'prays.txt', 'utils'] */*.csv → 2 ['dani/lyutyi.csv', 'dani/sichen.csv'] **/*.csv → 3 ['dani/lyutyi.csv', 'dani/sichen.csv', 'dani/stari/2024.csv'] **/*.py → 3 ['main.py', 'utils/__init__.py', 'utils/chytach.py'] **/* → 12 ['README.md', 'dani', 'dani/lyutyi.csv', 'dani/sichen.csv', 'dani/stari', 'dani/stari/2024.csv', 'dani/stari/nazvy.txt', 'main.py', 'prays.txt', 'utils', 'utils/__init__.py', 'utils/chytach.py'] ✅ усі шість шаблонів дали рівно ті числа, що й інтерактив 6
Дві деталі, які легко забути: glob повертає генератор, а не список, і серед
знахідок можуть бути теки.
результат = projekt.glob("*")
print("тип того, що повернув glob:", type(результат).__name__)
print("надрукувати напряму марно:", результат)
усі_знахідки = list(результат)
теки = [п.name for п in усі_знахідки if п.is_dir()]
файли = [п.name for п in усі_знахідки if п.is_file()]
print("теки :", sorted(теки))
print("файли:", sorted(файли))
assert len(теки) == 2, "серед пʼяти знахідок дві мають бути теками"
print("✅ glob не фільтрує за типом — це робота .is_file() і .is_dir()")
тип того, що повернув glob:
generator надрукувати напряму марно: <generator object Path.glob at 0x7fb4d44cabd0> теки : ['dani', 'utils'] файли: ['README.md', 'main.py', 'prays.txt'] ✅ glob не фільтрує за типом — це робота .is_file() і .is_dir()
11 · Скільки байтів насправді на диску¶
Повторимо інтерактив 7 на справжньому файлі: запишемо 100 рядків і подивимось на
розмір файлу до flush() і після.
журнал = робоча_тека / "zhurnal.txt"
рядок_журналу = "хліб;28.5\n"
f = open(журнал, "w", encoding="utf-8")
for _ in range(100):
f.write(рядок_журналу)
розмір_до_flush = журнал.stat().st_size # файл ще відкритий, буфер не скинуто
f.flush()
розмір_після_flush = журнал.stat().st_size
f.close()
записано_байтів = 100 * len(рядок_журналу.encode("utf-8"))
print("ми записали байтів:", записано_байтів)
print("на диску ДО flush() :", розмір_до_flush)
print("на диску ПІСЛЯ flush():", розмір_після_flush)
assert розмір_до_flush == 0, "1400 байтів менші за буфер — на диску має бути нуль"
assert розмір_після_flush == записано_байтів
print("✅ доки буфер не скинуто, файл на диску порожній — саме про це інтерактив 7")
ми записали байтів: 1400 на диску ДО flush() : 0 на диску ПІСЛЯ flush(): 1400 ✅ доки буфер не скинуто, файл на диску порожній — саме про це інтерактив 7
12 · Прибирання: зошит не лишає сміття¶
shutil.rmtree видаляє теку разом із усім вмістом. Перед таким викликом варто
подивитись на змінну зі шляхом двічі — у нас це гарантовано тимчасова тека.
print("прибираємо:", робоча_тека)
print("усередині було записів:", len(list(робоча_тека.rglob("*"))))
shutil.rmtree(робоча_тека)
print("тека ще існує?", робоча_тека.exists())
assert not робоча_тека.exists(), "тимчасова тека мала зникнути"
print("✅ зошит не лишив по собі жодного файлу")
прибираємо: /tmp/praktyka-20-s26s5cdz усередині було записів: 19 тека ще існує? False ✅ зошит не лишив по собі жодного файлу
Завдання¶
Роби їх у новій тимчасовій теці — рецепт той самий, що в клітинці 1, і не забудь
shutil.rmtree наприкінці.
🟢 Рівень 1 — База¶
Напиши функцію зберегти_прайс(шлях, товари), яка приймає словник
{назва: ціна} і записує його у файл у форматі назва;ціна, по рядку на товар.
Друга функція прочитати_прайс(шлях) має повернути такий самий словник.
Зроблено, якщо: assert прочитати_прайс(p) == товари проходить для словника
з щонайменше пʼяти товарів з українськими назвами, а open у обох функціях має
явний encoding="utf-8".
🟡 Рівень 2 — Плюс¶
Зроби так, щоб зберегти_прайс відмовлялась затирати наявний файл: якщо файл
існує, вона має кинути FileExistsError із зрозумілим повідомленням, а не мовчки
перезаписати. Додай аргумент перезаписати=False, який цю поведінку вимикає.
Зроблено, якщо: у тебе три перевірки — виклик на порожньому місці створює файл,
повторний виклик кидає FileExistsError, виклик із перезаписати=True проходить.
Підказка: потрібний режим уже є в таблиці режимів із лекції.
🔴 Рівень 3 — Виклик¶
Напиши функцію звести_прайси(тека), яка знаходить усі файли *.csv у теці
й усіх вкладених теках, читає їх і повертає один спільний словник товарів. Файли,
записані в cp1251, теж мають прочитатись: якщо utf-8 дає UnicodeDecodeError,
спробуй cp1251, а якщо і він не спрацював — пропусти файл і додай його імʼя
в список проблемних.
Зроблено, якщо: ти будуєш дерево щонайменше з трьох рівнів, кладеш туди один
файл у cp1251, один у utf-8 і один навмисно зіпсований (запиши в нього випадкові
байти через write_bytes), а функція повертає правильний словник і рівно одне
проблемне імʼя. Порядок обходу зафіксуй через sorted, інакше тест буде
плаваючим.