Усі дані, з якими ми досі працювали, ми набирали руками. Список покупок у темі 06, словник цін у темі 08, зіпсовані рядки прайсу в темі 18 — усе це літерали, записані просто в коді. Порахували, надрукували, закрили вікно. Наступного разу все починається з тих самих літералів, бо більше нічого й не лишилось.
Це нормально, поки ти вчишся. Але жодна справжня програма так не працює: дані приходять ззовні й мають пережити завершення програми. Прайс вивантажили з бухгалтерії, результат треба віддати колезі, журнал подій має існувати завтра вранці. Усе це — файли, і сьогоднішня тема відкриває блок «Робота з реальністю»: там, де дані вже не ми вигадуємо.
Наскрізний приклад лишається той самий, що в темі 18, — прайс із трьох рядків.
Тільки тепер він не змінна в коді, а файл prays.txt на диску:
01 / МотивЧому програма все забуває
Коли ти запускаєш скрипт, операційна система виділяє йому шматок оперативної памʼяті. Там живуть усі обʼєкти: числа, рядки, списки, словники. Коли скрипт завершується, система забирає цей шматок назад і віддає наступній програмі. Нічого не «зберігається саме собою»: памʼять для того й потрібна, щоб її переприділяти.
Побутова аналогія проста. Оперативна памʼять — це стіл: широкий, швидкий, але ввечері прибиральниця змітає з нього все. Диск — це шафа: повільніша, зате вранці все на місці.
Файл — це названа послідовність байтів, яку зберігає операційна система. Байт — число від 0 до 255, і сама система не знає, що ті байти означають: текст прайсу, фотографію чи музику. Тлумачення — цілком наша справа, і саме звідси візьметься половина сьогоднішніх пасток.
У файлів є друга роль, не менш важлива: це спосіб обміну між програмами. Бухгалтерка вивантажила таблицю, ти прочитав її Python-ом, записав результат, вона відкрила його в Excel. Жодна з трьох програм нічого не знає про дві інші — домовились лише про формат файлу.
Робота з файлом складається з трьох дій: відкрити, прочитати або записати, закрити. Найважча — остання, бо про неї забувають, а наслідки видно не одразу.
02 / Основаopen(): файл як обʼєкт
Уся робота починається з однієї вбудованої функції:
Зверни увагу на те, чого тут немає: вмісту файлу. open
не читає нічого — вона повертає обʼєкт, через який читати можна.
Це принципово: якби open одразу читала файл, відкрити фільм на чотири
гігабайти було б неможливо.
Точніша аналогія — не «прочитати книжку», а «покласти закладку». Файловий обʼєкт памʼятає дві речі: який файл відкрито і на якій позиції ми зараз стоїмо. Кожне читання зсуває позицію вперед.
Аргументів у open три: шлях (рядок або обʼєкт
Path із розділу 08), режим — що саме ми збираємось
робити, за замовчуванням "r", тобто «читати», — і
encoding, тобто за якою таблицею перекладати байти
в символи. Останній формально не обовʼязковий, а фактично обовʼязковий завжди:
чому — у розділі 05.
Текстовий режим і двійковий
Літера b в режимі ("rb", "wb") перемикає
файл у двійковий режим, і різниця більша, ніж здається:
текстовий ("r") | двійковий ("rb") | |
|---|---|---|
| що віддає | str — символи | bytes — числа 0…255 |
| кодування | декодує за encoding | не чіпає взагалі |
| кінці рядків | будь-які перетворює на \n | лишає як є |
| для чого | текст, CSV, JSON, код | картинки, архіви, бази, мережа |
Правило вибору без винятків: текст для людини — текстовий режим
з encoding, усе інше — двійковий. Спроба відкрити картинку
в текстовому режимі майже напевно дасть UnicodeDecodeError, і це ще
щасливий кінець.
03 / НаміриРежими r, w, a, x
Другий аргумент open — це оголошення намірів. Ти
кажеш системі, що збираєшся робити, а вона на основі цього вирішує, чи створювати
файл і що зробити з наявним вмістом. Чотири базові літери:
| режим | файлу немає | файл уже є | дозволяє |
|---|---|---|---|
| r | FileNotFoundError | відкрити з початку | лише читати |
| w | створити порожній | стерти вміст | лише писати |
| a | створити порожній | стати в кінець | лише дописувати |
| x | створити порожній | FileExistsError | лише писати |
Найважливіший рядок таблиці — другий. "w" стирає файл у мить
відкриття, ще до першого write. Не після, не «якщо запис вдався» —
одразу. Рядок open("prays.txt", "w"), виконаний випадково, знищує прайс
навіть тоді, коли наступний рядок програми падає з винятком. Відновити немає звідки.
Інтерактив 1 · Що робить кожен режим із твоїм файлом
Обери режим і те, чи файл уже існує. Дивись на середню колонку: вона показує стан одразу після open(), ще до запису.
w на наявному файлі й подивись на середню колонку.
Було три рядки — стало нуль, і жодного попередження. Порівняй із x:
той самий намір, але замість тихого знищення — FileExistsError
і жоден байт не втрачено.Ще дві деталі. "a" пише тільки в кінець:
навіть якщо перемотати позицію на початок, наступний write усе одно
опиниться у хвості — так працює прапорець O_APPEND в операційній системі.
І плюс відкриває друге вміння: "r+" — читати й писати
в наявному файлі, "w+" — писати й читати, попередньо стерши. Новачкові
вони не потрібні майже ніколи, і це добре: змішувати читання із записом в одному
обʼєкті — надійний спосіб заплутатись у позиції.
04 / Гарантіяwith: обіцянка з теми 18
У темі 18 ми домовились, що
with — це try/finally, спакований у два слова, і що
докладно буде тут. Час виконувати обіцянку.
Чому взагалі треба закривати файл? Три причини, і всі практичні:
- Дані не на диску, доки файл не закритий. Запис іде через буфер
у памʼяті — так швидше.
close()скидає буфер на диск. - Файловий дескриптор — обмежений ресурс. Система дає процесу
скінченну кількість відкритих файлів (типово тисячу-дві), і цикл, що відкриває
й не закриває, впаде з
OSError: Too many open files. - Файл заблокований. У Windows відкритий файл часто не можна ані перейменувати, ані видалити — і незрозуміло, хто його тримає.
Найпростіший спосіб — дописати f.close() у кінці. Він працює рівно
доти, доки між open і close нічого не падає. А падає,
як ми знаємо з теми 18, регулярно: виняток пропускає решту блоку, і рядок
close() просто не виконується.
Інтерактив 2 · Той самий код трьома способами
Перемкни сценарій на «виняток усередині» й порівняй три колонки: чи закрито файл і скільки рядків справді долетіло до диска.
Обʼєкт, який уміє стояти після with, називають менеджером
контексту (context manager). Він знає дві дії: що зробити на вході в блок
і що зробити на виході. Файловий обʼєкт на вході віддає сам себе (тому
as f — це той самий обʼєкт, що повернула open), а на
виході викликає close(). Вихід відбувається завжди:
через кінець блоку, return, break чи виняток.
with закриває, але не рятує. Виняток,
що стався всередині блоку, летить далі вгору рівно так само, як без
with: менеджер контексту гарантує прибирання, а не тишу. Хочеш ще
й обробити виняток — потрібен try/except навколо блоку.Після виходу з блоку обʼєкт лишається, але вже закритий, і спроба скористатись ним дає впізнаваний виняток:
Кілька файлів в одному with пишуть через кому — обидва закриються,
навіть якщо впаде посередині:
05 / ПасткаКодування: чому utf-8 обовʼязково
Тепер головна практична пастка теми. На диску немає символів — там байти. Щоб з байтів вийшов текст, потрібна таблиця відповідності, і називають її кодуванням (encoding).
Таблиць історично багато. ASCII описує 128 символів: латиниця, цифри, розділові знаки — і жодної кириличної літери. cp1251 (вона ж Windows-1251) розширює ASCII до 256 і додає туди кирилицю — але лише її, і в кожної мовної групи була така своя таблиця. UTF-8 вміщає всі писемності світу: ASCII в ній лишився однобайтовим, а решта займає два-чотири байти.
Порахуймо руками на слові хліб. У cp1251 кожна літера — один байт:
F5 EB B3 E1, разом чотири. У UTF-8 кирилична літера коштує два байти:
D1 85 D0 BB D1 96 D0 B1, разом вісім. Той самий текст, різна довжина —
і жодного натяку всередині файлу на те, за якою таблицею його писали. Кодування
у файлі не зберігається. Його треба знати наперед.
Інтерактив 3 · Записали одним кодуванням, читаємо іншим
Той самий український рядок. Обери, чим писали й чим читаємо, — і подивись на нижній ряд.
хліб;28.5.
Помилки немає, програма спокійно працює далі, і мотлох їде в базу даних.
«Пишемо cp1251, читаємо utf-8» — це UnicodeDecodeError на першому
ж байті. Гучна біда краща за тиху: про неї ти дізнаєшся в ту саму секунду.Тепер найголовніше. Якщо не передати encoding, Python не вгадує
кодування файлу — він бере налаштування системи. На сучасному Linux
і macOS це UTF-8, а на Windows донедавна була кирилична cp1251. Ось звідки береться
класична історія: у тебе все читається, у колеги той самий скрипт на тому самому
файлі падає. Код однаковий, система різна.
open
у текстовому режимі отримує encoding="utf-8". Завжди. Це чотирнадцять
символів, які роблять поведінку програми однаковою на всіх машинах світу. Пропущений
encoding — не «дрібна недбалість», а зламаність, яка проявиться
не в тебе.Що робити, коли файл таки прийшов у cp1251? Не вгадувати, а спитати
того, хто його дав, і передати кодування явно:
open("staryi.txt", encoding="cp1251"). Автовизначення — це евристика,
і короткий файл вона вгадує особливо погано.
Якщо джерело брудне й кілька байтів псують усе читання, є аварійний вихід —
аргумент errors:
Обидва варіанти — втрата даних, і ставити їх «щоб не падало» не можна. Вони доречні там, де прочитати 99% тексту цінніше, ніж зупинитись: пошук по логах, груба статистика. У розборі платіжок такому місця немає.
utf-8-sig. Excel і низка
редакторів Windows ставлять на початок файлу три службові байти
EF BB BF — «мітку порядку байтів» (BOM). Прочитаний як звичайний
utf-8, такий файл дасть невидимий символ \ufeff на початку,
і перший заголовок стовпця раптом не збігається ні з чим. Кодування
utf-8-sig зʼїдає цю мітку при читанні. Дивно поводиться перше поле
CSV — підозра саме на BOM.06 / ЧитанняТри способи прочитати
Файл відкрито. Далі є кілька способів дістати з нього текст, і вибір між ними — не питання смаку.
f.read()— увесь файл одним рядком. Просто й зручно, доки файл невеликий.f.readline()— один рядок разом із\nу кінці; у кінці файлу повертає порожній рядок. Потрібен рідко: коли перший рядок особливий, а решту читає щось інше.f.readlines()— список усіх рядків. Виглядає найзручніше, а насправді найдорожче: у памʼяті опиняється і весь текст, і окремий обʼєкт на кожен рядок.for рядок in f:— обхід циклом. Файловий обʼєкт сам є послідовністю рядків. Це правильний спосіб за замовчуванням.
Обхід циклом тримає в памʼяті один рядок за раз: Python читає з диска блок у кілька кілобайтів, віддає з нього рядки, дочитує наступний — і так до кінця. Скільки б не важив файл, витрата памʼяті лишається сталою.
Інтерактив 4 · Скільки памʼяті просить кожен спосіб
Повзунок задає розмір файлу. Смуги показують памʼять у пікові, шкала — від найбільшого з трьох.
readlines(). А тепер у крайнє праве: readlines()
просить майже десять гігабайтів, а цикл — ті самі вісім кілобайтів. Перевага
обходу не в тому, що він завжди економніший, а в тому, що він не росте.
Модель приблизна: 40 байтів тексту на рядок і ще близько 62 службових на кожен
обʼєкт-рядок у списку.\n лишається в рядку. І
readline(), і обхід циклом віддають рядок разом із переходом на новий
рядок, тому далі майже завжди йде .rstrip("\n"). Забудеш — і остання
ціна буде "28.5\n". float її, до речі, проковтне без скарг
(пробільні символи по краях він ігнорує), а от порівняння
назва == "хліб" тихо стане хибним.07 / ЗаписЗапис і дозапис
Метод write приймає рядок і повертає кількість записаних символів.
Головне про нього — те, чого він не робить:
write не додає перехід на новий рядок. Ніколи. Це прямий наслідок
того, що файл — послідовність символів, а не список рядків: якщо потрібен
\n, пиши його сам.
writelines теж не додає \n.
Назва обіцяє «записати рядки», а метод просто склеює елементи списку без жодного
роздільника. Хочеш рядки — додай переходи сам:
f.writelines(р + "\n" for р in назви). Часто читабельніший другий
варіант — print("хліб;28.5", file=f): print додає
\n сам, як і на екрані.Режим "a" перетворює файл на журнал: кожен запуск програми дописує
свій рядок у кінець, нічого не стираючи. Класичне застосування — протокол обробки:
Межа тут одна: "a" дописує, але не прибирає. Файл журналу росте,
доки хтось його не оберне, — і в маленькому скрипті достатньо про це памʼятати.
08 / Шляхиpathlib: шлях як обʼєкт
У темі 02 ми розібрали, що таке шлях і чим абсолютний відрізняється від відносного. Тоді шлях був для нас рядком — і саме так із ним довго й поводились у Python. Проблема в тому, що рядок нічого не знає про свою будову.
Клеїти шляхи рядками погано з двох причин. Перша: роздільник різний —
/ на Linux і macOS, \ у Windows. Друга, важливіша: рядок
не перевіряє себе. Забув риску — програма спокійно створить файл
daniprays.txt замість того, щоб поскаржитись.
Модуль pathlib розвʼязує це не функціями, а типом:
шлях стає обʼєктом, який знає власну будову.
Оператор / тут — не хак і не гра слів: для Path ділення
перевизначене й означає «додати складову шляху», а правильний роздільник система
підставить сама. Клас теж обирається автоматично — PosixPath на Linux
і macOS, WindowsPath у Windows, — і саме тому той самий код працює скрізь.
Інтерактив 5 · З чого складається Path
Обери властивість — підсвітиться те, що вона поверне. Перемикач стилю показує той самий шлях очима Windows.
.suffixes. У назві
prays.2026.csv дві крапки, і .suffix бере лише останню
частину — .csv. «2026» для pathlib — не розширення,
а шматок назви, тому .stem дає prays.2026. Хто цього
не знає, той пише імʼя.split(".")[0] і одного дня втрачає половину
назви файлу.Найкорисніші властивості одразу списком, на прикладі
Path("/home/olena/dani/prays.2026.csv"):
| вираз | що дає |
|---|---|
| .name | prays.2026.csv — імʼя з розширенням |
| .stem | prays.2026 — імʼя без останнього розширення |
| .suffix | .csv — останнє розширення, з крапкою |
| .parent | /home/olena/dani — тека, у якій лежить |
| .parts | кортеж усіх складових, від кореня |
| .with_suffix(".json") | той самий шлях з іншим розширенням |
| Path.cwd() | поточна тека — від неї рахуються відносні шляхи |
| Path(__file__).parent | тека, у якій лежить сам скрипт |
Path нічого не читає з диска.
Path("немає-такого.txt") створюється без найменшої скарги — це просто
обʼєкт із текстом усередині. Диска він торкається лише тоді, коли ти прямо просиш:
.exists(), .read_text(), .glob(). Це зручно
(шлях можна складати й передавати куди завгодно) і водночас підступно: сам факт, що
обʼєкт створився, нічого не доводить про існування файлу.open("dani/prays.txt"): із теки проєкту все гаразд, з іншої теки —
FileNotFoundError, бо відносний шлях рахується від поточної
теки, а не від теки зі скриптом. Ліки одні:
Path(__file__).parent / "dani" / "prays.txt". Змінна
__file__ знайома нам із теми 16.09 / ТекаТека: створити й обійти
Файли рідко бувають самі по собі — зазвичай їх багато й вони лежать у теках.
pathlib закриває і цю роботу.
Обидва прапорці в mkdir потрібні, бо без них рядок падає через раз.
parents=True створює всі проміжні теки — без нього
zvity/2026 не зʼявиться, якщо немає zvity (це аналог
mkdir -p з терміналу). exist_ok=True велить
не скаржитись, якщо тека вже є: без нього повторний запуск програми
впаде з FileExistsError, а повторний запуск — нормальний сценарій,
не аварія.
Обійти вміст теки можна трьома способами. .iterdir() віддає все,
що лежить безпосередньо всередині. .glob(шаблон) фільтрує за маскою.
.rglob(шаблон) — те саме, що .glob("**/" + шаблон), тобто
з заходом у всі вкладені теки.
Інтерактив 6 · Що саме знайде твій glob
Одне дерево файлів, шість шаблонів. Знайдене підсвічується зліва й перелічується справа.
*/*.csv і **/*.csv. Одна
зірочка — рівно один рівень углиб, тому dani/stari/2024.csv
лишається невидимим. Дві зірочки означають «ця тека і всі вкладені» — знаходяться
всі три файли. І окремо подивись на *: серед пʼяти знахідок дві —
теки, бо glob не відрізняє файл від теки, доки не спитаєш
.is_file().Дві властивості glob, про які легко забути. Він повертає
генератор, а не список: диск читається поступово, у міру обходу, тому
print(тека.glob("*.csv")) надрукує <generator object ...>,
а не файли. І порядок не гарантований — він залежить від файлової
системи, тому тест, який мовчки покладається на алфавіт, одного дня почервоніє
на чужому компʼютері. Треба порядок — sorted(...).
Видалення теж на місці: файл.unlink(missing_ok=True) прибирає файл,
тека.rmdir() — порожню теку. Для теки з вмістом потрібен
shutil.rmtree(), і це та функція, перед викликом якої варто двічі
подивитись на змінну зі шляхом.
10 / СкороченняЯрлики read_text і write_text
Для найчастішого випадку — «прочитати невеликий файл цілком» — pathlib
має готові методи, і вони самі відкривають та закривають файл:
Аргумент encoding тут потрібен рівно так само, як в open:
це скорочення запису, а не скасування правил. Не варто ними користуватись
у двох випадках. Великий файл — read_text тягне все
в памʼять, точно як read(), і інтерактив 4 показав, чим це
закінчується. Дозапис — write_text завжди перезаписує
файл з нуля, це "w", а не "a".
11 / ГлибшеДля тих, хто вже пише код
Кілька речей, які на робочому проєкті трапляються щотижня.
Буферизація й коли потрібен flush()
write не пише на диск. Він кладе дані в буфер у памʼяті, і лише коли
буфер заповнився, вміст іде до операційної системи одним шматком. Системний виклик
коштує на порядки дорожче за копіювання в памʼять — тому тисяча дрібних записів
по 14 байтів мала б тисячу викликів замість двох.
Інтерактив 7 · Де насправді твої дані
Повзунок — скільки рядків уже записано. Буфер — 8 192 байти; дивись, скільки з них дійшло до диска.
with робить те
саме одним скиданням у кінці — і це та причина, чому with не просто
охайніший, а правильніший.Отже, f.flush() потрібен там, де файл читає хтось інший
одночасно з записом: журнал під tail -f або файл прогресу
довгої обробки. У решті випадків with скине буфер сам. І уточнення,
яке рятує від хибної впевненості: flush() віддає дані
операційній системі, а не кладе їх на пластину диска — в ОС теж
є свій кеш. Гарантія переживання вимкнення живлення — це
os.fsync(f.fileno()), і коштує вона відповідно.
newline="" для CSV
У текстовому режимі Python нормалізує кінці рядків: при читанні перетворює
\r\n на \n, а при записі у Windows робить навпаки. Для
звичайного тексту це благо. Для модуля csv — біда, бо він керує
роздільниками рядків сам, і два шари нормалізації дають зайві порожні рядки
через один. Тому файл під csv відкривають так:
Це не забобон: newline="" вимикає нормалізацію, лишаючи роботу
з \r\n самому модулю csv. Знадобиться вже
в темі 21.
Чому os.path досі трапляється
pathlib зʼявився в Python 3.4, але os.path старший
на два десятиліття, і код із ним нікуди не подівся. Відповідники прямі:
os.path.join(a, b) — це Path(a) / b,
os.path.basename — .name,
os.path.splitext(x)[1] — .suffix. Читай як інший запис
тієї самої думки; у новому коді пиши pathlib. Сумісність повна:
майже будь-яка функція, що чекає шлях-рядок, приймає й Path — через
протокол os.PathLike. Навпаки теж просто: str(шлях).
Ціна запитання до диска й атомарний запис
Path нічого не кешує: кожен .exists(),
.is_file(), .stat() — окремий системний виклик. У циклі
по сотні тисяч файлів це помітно, і тоді беруть os.scandir(): він
віддає записи, у яких тип уже відомий з одного читання теки.
Друга річ, про яку згадують пізно: якщо програму вбити посеред write,
на диску лишиться половина файлу — формально цілого, фактично зіпсованого. Захист
один: писати в тимчасовий файл поруч, а потім перейменувати його
на місце цільового. Перейменування в межах однієї файлової системи атомарне —
або старий файл, або новий, проміжного стану не існує.
Знайти пропущений encoding автоматично
З Python 3.10 запуск python3 -X warn_default_encoding скрипт.py
друкує EncodingWarning на кожен open без явного кодування.
Прогнати цим прапорцем чужий проєкт — найшвидший спосіб знайти майбутні проблеми
з кирилицею. Довгострокове рішення обговорюють у PEP 686: зробити UTF-8 кодуванням
за замовчуванням, після чого пастка зникне сама. Доти — пиши encoding явно.
12 / ПідсумокЩо забрати з теми
openне читає файл — вона повертає обʼєкт із позицією всередині. Читання зсуває позицію.- Режим — це намір.
rвимагає наявного файлу,wстирає вміст у мить відкриття,aдописує в кінець,xвідмовляється затирати наявне. with— цеtry/finally. Файл закриється при будь-якому виході з блоку, включно з винятком. Але сам винятокwithне ловить.encoding="utf-8"— завжди. Без нього Python бере налаштування системи, і той самий код поводиться по-різному на різних машинах. Тиха біда — мотлох, гучна —UnicodeDecodeError.- Великий файл читають циклом.
read()іreadlines()ростуть разом із файлом, обхід тримає сталі кілька кілобайтів. writeне додає\n— іwritelinesтеж не додає.- Шляхи не клеять рядками.
Path("a") / "b"знає про роздільники й про будову імені. mkdir(parents=True, exist_ok=True)створює всі проміжні теки й не падає при повторному запуску.globвіддає генератор у невизначеному порядку, і*не перетинає роздільника, а**перетинає.
Тепер програма вміє читати з диска й писати на диск. Але подивись, чим ми досі
займались із прочитаним: рядок.rstrip("\n").split(";") — розбираємо
текст руками. Це працює для трьох рядків, а щойно в назві товару трапиться сама
крапка з комою або лапки, розбір розсиплеться. Домовленостей про те, як пакувати
дані в текст, люди придумали чимало, і дві перемогли решту: CSV
для таблиць і JSON для вкладених структур. Обидві мають готові
модулі в стандартній бібліотеці, обидві правильно розбирають лапки й екранування —
і про них тема 21. Файл ми відкривати вже вміємо; лишилось
навчитись розуміти, що в ньому написано.
practice.ipynb ти зробиш усе
це руками в тимчасовій теці, яка сама прибереться після роботи: запишеш прайс
і прочитаєш його трьома способами, побачиш на власні очі, як "w"
знищує вміст, доведеш через assert, що with закрив файл
навіть після винятку, відтвориш мотлох із інтерактиву 3 байт у байт, збудуєш дерево
тек через mkdir(parents=True) і перевіриш усі шість шаблонів
glob з інтерактиву 6 на справжніх файлах.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.