Усі дані, з якими ми досі працювали, ми набирали руками. Список покупок у темі 07, словник цін у темі 09, зіпсовані рядки прайсу в темі 19 — усе це літерали, записані просто в коді. Порахували, надрукували, закрили вікно. Наступного разу все починається з тих самих літералів, бо більше нічого й не лишилось.
Це нормально, поки ти вчишся. Але жодна справжня програма так не працює: дані приходять ззовні й мають пережити завершення програми. Прайс вивантажили з бухгалтерії, результат треба віддати колезі, журнал подій має існувати завтра вранці. Усе це — файли, і сьогоднішня тема відкриває блок «Робота з реальністю»: там, де дані вже не ми вигадуємо.
Наскрізний приклад лишається той самий, що в темі 19, — прайс із трьох рядків.
Тільки тепер він не змінна в коді, а файл prays.txt на диску:
МотивЧому програма все забуває
Коли ти запускаєш скрипт, операційна система виділяє йому шматок оперативної памʼяті. Там живуть усі обʼєкти: числа, рядки, списки, словники. Коли скрипт завершується, система забирає цей шматок назад і віддає наступній програмі. Нічого не «зберігається саме собою»: памʼять для того й потрібна, щоб її переприділяти.
Побутова аналогія проста. Оперативна памʼять — це стіл: широкий, швидкий, але ввечері прибиральниця змітає з нього все. Диск — це шафа: повільніша, зате вранці все на місці.
Файл — це названа послідовність байтів, яку зберігає операційна система. Байт — число від 0 до 255, і сама система не знає, що ті байти означають: текст прайсу, фотографію чи музику. Тлумачення — цілком наша справа, і саме звідси візьметься половина сьогоднішніх пасток.
У файлів є друга роль, не менш важлива: це спосіб обміну між програмами. Бухгалтерка вивантажила таблицю, ти прочитав її Python-ом, записав результат, вона відкрила його в Excel. Жодна з трьох програм нічого не знає про дві інші — домовились лише про формат файлу.
Робота з файлом складається з трьох дій: відкрити, прочитати або записати, закрити. Найважча — остання, бо про неї забувають, а наслідки видно не одразу.
Основаopen(): файл як обʼєкт
Уся робота починається з однієї вбудованої функції:
Зверни увагу на те, чого тут немає: вмісту файлу. open
не читає нічого — вона повертає обʼєкт, через який читати можна.
Це принципово: якби open одразу читала файл, відкрити фільм на чотири
гігабайти було б неможливо.
Точніша аналогія — не «прочитати книжку», а «покласти закладку». Файловий обʼєкт памʼятає дві речі: який файл відкрито і на якій позиції ми зараз стоїмо. Кожне читання зсуває позицію вперед.
Аргументів у open три: шлях (рядок або обʼєкт
Path із розділу 08), режим — що саме ми збираємось
робити, за замовчуванням "r", тобто «читати», — і
encoding, тобто за якою таблицею перекладати байти
в символи. Останній формально не обовʼязковий, а фактично обовʼязковий завжди:
чому — у розділі 05.
Текстовий режим і двійковий
Літера b в режимі ("rb", "wb") перемикає
файл у двійковий режим, і різниця більша, ніж здається:
текстовий ("r") | двійковий ("rb") | |
|---|---|---|
| що віддає | str — символи | bytes — числа 0…255 |
| кодування | декодує за encoding | не чіпає взагалі |
| кінці рядків | будь-які перетворює на \n | лишає як є |
| для чого | текст, CSV, JSON, код | картинки, архіви, бази, мережа |
Правило вибору без винятків: текст для людини — текстовий режим
з encoding, усе інше — двійковий. Спроба відкрити картинку
в текстовому режимі майже напевно дасть UnicodeDecodeError, і це ще
щасливий кінець.
НаміриРежими r, w, a, x
Другий аргумент open — це оголошення намірів. Ти
кажеш системі, що збираєшся робити, а вона на основі цього вирішує, чи створювати
файл і що зробити з наявним вмістом. Чотири базові літери:
| режим | файлу немає | файл уже є | дозволяє |
|---|---|---|---|
| r | FileNotFoundError | відкрити з початку | лише читати |
| w | створити порожній | стерти вміст | лише писати |
| a | створити порожній | стати в кінець | лише дописувати |
| x | створити порожній | FileExistsError | лише писати |
Найважливіший рядок таблиці — другий. "w" стирає файл у мить
відкриття, ще до першого write. Не після, не «якщо запис вдався» —
одразу. Рядок open("prays.txt", "w"), виконаний випадково, знищує прайс
навіть тоді, коли наступний рядок програми падає з винятком. Відновити немає звідки.
Що робить кожен режим із твоїм файлом
Обери режим і те, чи файл уже існує. Дивись на середню колонку: вона показує стан одразу після open(), ще до запису.
w на наявному файлі й подивись на середню колонку.
Було три рядки — стало нуль, і жодного попередження. Порівняй із x:
той самий намір, але замість тихого знищення — FileExistsError
і жоден байт не втрачено.Ще дві деталі. "a" пише тільки в кінець:
навіть якщо перемотати позицію на початок, наступний write усе одно
опиниться у хвості — так працює прапорець O_APPEND в операційній системі.
І плюс відкриває друге вміння: "r+" — читати й писати
в наявному файлі, "w+" — писати й читати, попередньо стерши. Новачкові
вони не потрібні майже ніколи, і це добре: змішувати читання із записом в одному
обʼєкті — надійний спосіб заплутатись у позиції.
Гарантіяwith: обіцянка з теми 19
У темі 19 ми домовились, що
with — це try/finally, спакований у два слова, і що
докладно буде тут. Час виконувати обіцянку.
Чому взагалі треба закривати файл? Три причини, і всі практичні:
- Дані не на диску, доки файл не закритий. Запис іде через буфер
у памʼяті — так швидше.
close()скидає буфер на диск. - Файловий дескриптор — обмежений ресурс. Система дає процесу
скінченну кількість відкритих файлів (типово тисячу-дві), і цикл, що відкриває
й не закриває, впаде з
OSError: Too many open files. - Файл заблокований. У Windows відкритий файл часто не можна ані перейменувати, ані видалити — і незрозуміло, хто його тримає.
Найпростіший спосіб — дописати f.close() у кінці. Він працює рівно
доти, доки між open і close нічого не падає. А падає,
як ми знаємо з теми 19, регулярно: виняток пропускає решту блоку, і рядок
close() просто не виконується.
Той самий код трьома способами
Перемкни сценарій на «виняток усередині» й порівняй три колонки: чи закрито файл і скільки рядків справді долетіло до диска.
Обʼєкт, який уміє стояти після with, називають менеджером
контексту (context manager). Він знає дві дії: що зробити на вході в блок
і що зробити на виході. Файловий обʼєкт на вході віддає сам себе (тому
as f — це той самий обʼєкт, що повернула open), а на
виході викликає close(). Вихід відбувається завжди:
через кінець блоку, return, break чи виняток.
Тепер видно, чому with кращий за try/finally, а не просто
коротший. Різниця в тому, хто знає, як прибирати. У варіанті з
finally це знання лежить у тебе в коді, і ти повторюєш його на кожному
місці, де відкриваєш файл, — а отже маєш шанс щоразу забути, написати не той метод
або поставити close() не в ту гілку. У варіанті з with
прибирання описав сам обʼєкт, один раз і назавжди; тобі лишилось
позначити межу блоку. Тому те саме слово with однаково правильно закриває
файл, звільняє блокування чи фіксує транзакцію бази — забути «правильну» дію
неможливо, бо її вибирає не той, хто пише блок.
with закриває, але не рятує. Виняток,
що стався всередині блоку, летить далі вгору рівно так само, як без
with: менеджер контексту гарантує прибирання, а не тишу. Хочеш ще
й обробити виняток — потрібен try/except навколо блоку.Після виходу з блоку обʼєкт лишається, але вже закритий, і спроба скористатись ним дає впізнаваний виняток:
Кілька файлів в одному with пишуть через кому — обидва закриються,
навіть якщо впаде посередині:
ПасткаКодування: чому 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, разом вісім. Той самий текст, різна довжина —
і жодного натяку всередині файлу на те, за якою таблицею його писали. Кодування
у файлі не зберігається. Його треба знати наперед.
Записали одним кодуванням, читаємо іншим
Той самий український рядок. Обери, чим писали й чим читаємо, — і подивись на нижній ряд.
хліб;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.ЧитанняТри способи прочитати
Файл відкрито. Далі є кілька способів дістати з нього текст, і вибір між ними — не питання смаку.
f.read()— увесь файл одним рядком. Просто й зручно, доки файл невеликий.f.readline()— один рядок разом із\nу кінці; у кінці файлу повертає порожній рядок. Потрібен рідко: коли перший рядок особливий, а решту читає щось інше.f.readlines()— список усіх рядків. Виглядає найзручніше, а насправді найдорожче: у памʼяті опиняється і весь текст, і окремий обʼєкт на кожен рядок.for рядок in f:— обхід циклом. Файловий обʼєкт сам є послідовністю рядків. Це правильний спосіб за замовчуванням.
Обхід циклом тримає в памʼяті один рядок за раз: Python читає з диска блок у кілька кілобайтів, віддає з нього рядки, дочитує наступний — і так до кінця. Скільки б не важив файл, витрата памʼяті лишається сталою.
Скільки памʼяті просить кожен спосіб
Повзунок задає розмір файлу. Смуги показують памʼять у пікові, шкала — від найбільшого з трьох.
readlines(). А тепер у крайнє праве: readlines()
просить майже десять гігабайтів, а цикл — ті самі вісім кілобайтів. Перевага
обходу не в тому, що він завжди економніший, а в тому, що він не росте.
Модель приблизна: 40 байтів тексту на рядок і ще близько 62 службових на кожен
обʼєкт-рядок у списку.\n лишається в рядку. І
readline(), і обхід циклом віддають рядок разом із переходом на новий
рядок, тому далі майже завжди йде .rstrip("\n"). Забудеш — і остання
ціна буде "28.5\n". float її, до речі, проковтне без скарг
(пробільні символи по краях він ігнорує), а от порівняння
назва == "хліб" тихо стане хибним.ЗаписЗапис і дозапис
Метод write приймає рядок і повертає кількість записаних символів.
Головне про нього — те, чого він не робить:
write не додає перехід на новий рядок. Ніколи. Це прямий наслідок
того, що файл — послідовність символів, а не список рядків: якщо потрібен
\n, пиши його сам.
writelines теж не додає \n.
Назва обіцяє «записати рядки», а метод просто склеює елементи списку без жодного
роздільника. Хочеш рядки — додай переходи сам:
f.writelines(р + "\n" for р in назви). Часто читабельніший другий
варіант — print("хліб;28.5", file=f): print додає
\n сам, як і на екрані.Режим "a" перетворює файл на журнал: кожен запуск програми дописує
свій рядок у кінець, нічого не стираючи. Класичне застосування — протокол обробки:
Межа тут одна: "a" дописує, але не прибирає. Файл журналу росте,
доки хтось його не оберне, — і в маленькому скрипті достатньо про це памʼятати.
Шляхиpathlib: шлях як обʼєкт
У темі 02 ми розібрали, що таке шлях і чим абсолютний відрізняється від відносного. Тоді шлях був для нас рядком — і саме так із ним довго й поводились у Python. Проблема в тому, що рядок нічого не знає про свою будову.
Клеїти шляхи рядками погано з двох причин. Перша: роздільник різний —
/ на Linux і macOS, \ у Windows. Друга, важливіша: рядок
не перевіряє себе. Забув риску — програма спокійно створить файл
daniprays.txt замість того, щоб поскаржитись.
Модуль pathlib розвʼязує це не функціями, а типом:
шлях стає обʼєктом, який знає власну будову.
Оператор / тут — не хак і не гра слів: для Path ділення
перевизначене й означає «додати складову шляху», а правильний роздільник система
підставить сама. Клас теж обирається автоматично — PosixPath на Linux
і macOS, WindowsPath у Windows, — і саме тому той самий код працює скрізь.
З чого складається 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__ знайома нам із теми 17.ТекаТека: створити й обійти
Файли рідко бувають самі по собі — зазвичай їх багато й вони лежать у теках.
pathlib закриває і цю роботу.
Обидва прапорці в mkdir потрібні, бо без них рядок падає через раз.
parents=True створює всі проміжні теки — без нього
zvity/2026 не зʼявиться, якщо немає zvity (це аналог
mkdir -p з терміналу). exist_ok=True велить
не скаржитись, якщо тека вже є: без нього повторний запуск програми
впаде з FileExistsError, а повторний запуск — нормальний сценарій,
не аварія.
Обійти вміст теки можна трьома способами. .iterdir() віддає все,
що лежить безпосередньо всередині. .glob(шаблон) фільтрує за маскою.
.rglob(шаблон) — те саме, що .glob("**/" + шаблон), тобто
з заходом у всі вкладені теки.
Що саме знайде твій glob
Одне дерево файлів, шість шаблонів. Знайдене підсвічується зліва й перелічується справа.
*/*.csv і **/*.csv. Одна
зірочка — рівно один рівень углиб, тому dani/stari/2024.csv
лишається невидимим. Дві зірочки означають «ця тека і всі вкладені» — знаходяться
всі три файли. І окремо подивись на *: серед пʼяти знахідок дві —
теки, бо glob не відрізняє файл від теки, доки не спитаєш
.is_file().Дві властивості glob, про які легко забути. Він повертає
генератор, а не список: диск читається поступово, у міру обходу, тому
print(тека.glob("*.csv")) надрукує <generator object ...>,
а не файли. І порядок не гарантований — він залежить від файлової
системи, тому тест, який мовчки покладається на алфавіт, одного дня почервоніє
на чужому компʼютері. Треба порядок — sorted(...).
Видалення теж на місці: файл.unlink(missing_ok=True) прибирає файл,
тека.rmdir() — порожню теку. Для теки з вмістом потрібен
shutil.rmtree(), і це та функція, перед викликом якої варто двічі
подивитись на змінну зі шляхом.
СкороченняЯрлики read_text і write_text
Для найчастішого випадку — «прочитати невеликий файл цілком» — pathlib
має готові методи, і вони самі відкривають та закривають файл:
Аргумент encoding тут потрібен рівно так само, як в open:
це скорочення запису, а не скасування правил. Не варто ними користуватись
у двох випадках. Великий файл — read_text тягне все
в памʼять, точно як read(), і інтерактив 4 показав, чим це
закінчується. Дозапис — write_text завжди перезаписує
файл з нуля, це "w", а не "a".
ГлибшеДля тих, хто вже пише код
Кілька речей, які на робочому проєкті трапляються щотижня.
Буферизація й коли потрібен flush()
write не пише на диск. Він кладе дані в буфер у памʼяті, і лише коли
буфер заповнився, вміст іде до операційної системи одним шматком. Системний виклик
коштує на порядки дорожче за копіювання в памʼять — тому тисяча дрібних записів
по 14 байтів мала б тисячу викликів замість двох.
Де насправді твої дані
Повзунок — скільки рядків уже записано. Буфер — 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. Знадобиться вже
в темі 22.
Чому 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 явно.
ПідсумокЩо забрати з теми
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 для вкладених структур. Обидві мають готові
модулі в стандартній бібліотеці, обидві правильно розбирають лапки й екранування —
і про них тема 22. Файл ми відкривати вже вміємо; лишилось
навчитись розуміти, що в ньому написано.