21. (Л) Файловий ввід/вивід¶
Зміст лекції¶
- Навіщо програмі файли
- Файлова система як дерево
- Абсолютний шлях
- Робочий каталог програми
- Відносний шлях,
.та.. - Домашній каталог і
~ - Шлях відносно самого скрипта
- Модуль
pathlib: шлях як обʼєкт - Частини шляху
- Перевірка існування, створення каталогів
- Перегляд вмісту каталогу
- Аргументи командного рядка
- Відкриття файлу:
open() - Менеджер контексту
with - Кодування тексту
- Режими відкриття файлу
- Читання:
read,readline,readlines, перебір - Запис:
write,writelines,print(file=...) - Додавання в кінець і безпечне створення
- Швидкі методи
read_textіwrite_text - Позиція у файлі:
tellіseek - Двійкові файли
- Копіювання, перейменування, видалення
- Приклад: журнал оцінок у файлі
- Типові помилки
Навіщо програмі файли¶
Усі змінні, списки та словники, з якими ми працювали досі, живуть в оперативній памʼяті. Щойно програма завершується, ця памʼять звільняється, і дані зникають. Якщо запустити програму ще раз, вона почне з нуля.
Файл — це іменована послідовність байтів на диску. Дані у файлі переживають завершення програми, перезавантаження компʼютера і навіть перевстановлення Python. Тому файли потрібні, щоб:
- зберігати результати роботи між запусками (налаштування, збережена гра, журнал оцінок);
- читати вхідні дані, які незручно вводити з клавіатури (тисяча рядків з датчика);
- обмінюватися даними з іншими програмами (звіт, який відкриють в Excel);
- вести журнал подій (лог), щоб потім розібратися, що пішло не так.
Робота з файлом завжди складається з трьох кроків: знайти файл (шлях), відкрити його, прочитати або записати і закрити. Починаємо з першого кроку — бо саме на ньому губиться найбільше часу початківця.
Файлова система як дерево¶
Файли на диску впорядковані в каталоги (directories, вони ж «теки», «папки»). Каталог може містити файли і вкладені каталоги — тож уся файлова система утворює дерево. У Linux і macOS у цього дерева один корінь, який позначають однією скісною рискою /.
graph TD
root["/"] --> home["home"]
root --> etc["etc"]
home --> student["student"]
student --> projects["projects"]
projects --> lab21["lab21"]
lab21 --> main["main.py"]
lab21 --> data["data"]
data --> grades["grades.txt"]
data --> names["names.txt"]
Щоб однозначно назвати файл grades.txt, треба вказати повний маршрут від кореня до нього, перелічивши каталоги через /:
Такий запис і називається шляхом. Шляхи бувають двох видів — абсолютні та відносні.
Абсолютний шлях¶
Абсолютний шлях (absolute path) описує маршрут від кореня файлової системи. У Linux і macOS він завжди починається з /.
Головна властивість абсолютного шляху: він означає той самий файл незалежно від того, звідки запущено програму. Це його перевага і водночас недолік — такий шлях прибитий до конкретного компʼютера. Якщо у коді написати /home/student/projects/lab21/data/grades.txt, програма не запрацює на компʼютері одногрупника, у якого користувач зветься інакше.
Одна риска чи дві
У Linux і macOS роздільником каталогів у шляху є звичайна скісна риска /. Не плутайте її зі зворотною \ — у Python зворотна коса риска всередині рядка починає спецсимвол (\n, \t), тому в шляхах її пишуть або подвоєною, або в сирому рядку r"...". Найнадійніший спосіб взагалі не думати про роздільники — будувати шляхи через pathlib, до якого дійдемо за кілька розділів.
Робочий каталог програми¶
Відносні шляхи неможливо зрозуміти без поняття робочого каталогу.
Поточний робочий каталог (current working directory, cwd) — це каталог, від якого програма відлічує всі відносні шляхи. Його визначає не файл програми, а те місце, з якого програму запустили.
# Program: where is the program running from
import os
from pathlib import Path
print(os.getcwd())
print(Path.cwd())
os.getcwd() і Path.cwd() роблять те саме; другий варіант повертає обʼєкт Path, з яким зручніше працювати далі. У вас у виводі буде ваш власний шлях.
Робочий каталог легко перевірити в терміналі командою pwd. Спробуйте самі: створіть файл where.py з кодом вище і запустіть його двома способами.
$ cd /home/student/projects/lab21
$ python3 where.py
/home/student/projects/lab21
$ cd /home/student
$ python3 projects/lab21/where.py
/home/student
Файл програми той самий, а робочий каталог різний. Саме через це найпоширеніша помилка початківця звучить так: «у VS Code працювало, а з термінала — FileNotFoundError». Програма шукала файл не там, бо змінився робочий каталог.
Відносний шлях, . та ..¶
Відносний шлях (relative path) описує маршрут від поточного робочого каталогу. Він не починається з /.
data/grades.txt каталог data у робочому каталозі, у ньому grades.txt
grades.txt файл прямо в робочому каталозі
../notes.txt файл у батьківському каталозі
У кожному каталозі є два спеціальні імені:
| Запис | Значення |
|---|---|
. |
сам поточний каталог |
.. |
батьківський каталог (на рівень вище) |
Тому ./data/grades.txt і data/grades.txt — одне й те саме, а ../.. означає «на два рівні вгору».
Якщо робочий каталог /home/student/projects/lab21, то відносні шляхи розгортаються в абсолютні так:
| Відносний шлях | Абсолютний шлях |
|---|---|
main.py |
/home/student/projects/lab21/main.py |
data/grades.txt |
/home/student/projects/lab21/data/grades.txt |
./data/grades.txt |
/home/student/projects/lab21/data/grades.txt |
../lab20/main.py |
/home/student/projects/lab20/main.py |
../../student/notes.txt |
/home/student/notes.txt |
Python уміє робити це перетворення сам:
# Program: absolute and relative paths
import os
from pathlib import Path
relative = Path("data/grades.txt")
print(relative)
print(relative.is_absolute())
absolute = relative.resolve()
print(absolute)
print(absolute.is_absolute())
messy = Path("data/../data/./grades.txt")
print(messy)
print(Path(os.path.normpath(messy)))
data/grades.txt
False
/home/student/projects/lab21/data/grades.txt
True
data/../data/grades.txt
data/grades.txt
is_absolute()відповідає, чи починається шлях з кореня.resolve()перетворює відносний шлях на абсолютний, підставляючи поточний робочий каталог, і прибирає.та... Рядок у вашому виводі буде іншим.os.path.normpath()спрощує запис шляху без звертання до диска і без підстановки cwd — зручно, коли треба просто прибрати..та..
Порівняння двох видів шляху:
| Абсолютний | Відносний | |
|---|---|---|
| Починається з | / |
імені каталогу, . або .. |
| Точка відліку | корінь файлової системи | поточний робочий каталог |
| Залежить від місця запуску | ні | так |
| Переноситься на інший компʼютер | ні | так, разом з каталогом проєкту |
| Коли доречний | системні файли, повністю визначені шляхи | дані всередині свого проєкту |
Практичне правило: у коді пишемо відносні шляхи (проєкт має залишатися переносним), але будуємо їх не від робочого каталогу, а від каталогу самого скрипта — так, як показано у наступному розділі.
Домашній каталог і ~¶
У терміналі символ ~ (тильда) означає домашній каталог поточного користувача, наприклад /home/student. Тому ~/projects/lab21 і /home/student/projects/lab21 — один і той самий каталог.
Важливо: тильду розгортає термінал, а не Python. Для open() рядок "~/notes.txt" — це звичайне імʼя каталогу ~, якого не існує. Розгортати тильду треба явно:
# Program: expand the home directory
import os
from pathlib import Path
print(Path("~/notes.txt"))
print(Path("~/notes.txt").expanduser())
print(os.path.expanduser("~/notes.txt"))
print(Path.home())
Шлях відносно самого скрипта¶
Ми зʼясували, що відносний шлях залежить від місця запуску. Щоб програма знаходила свої дані завжди, шлях будують від файлу самої програми. Змінна __file__ містить шлях до поточного файлу з кодом.
# Program: build paths relative to the script itself
from pathlib import Path
script_path = Path(__file__).resolve()
script_dir = script_path.parent
data_file = script_dir / "data" / "grades.txt"
print(script_path.name)
print(script_dir.name)
print(data_file.name)
print(data_file.is_absolute())
Path(__file__).resolve().parent — це каталог, у якому лежить скрипт. Далі від нього будуємо шлях до даних. Тепер програму можна запускати звідки завгодно.
__file__ існує не завжди
Змінна __file__ доступна у файлі з кодом, який запускають як скрипт. В інтерактивному режимі інтерпретатора (python3 без аргументів) її немає — там буде NameError. Приклади цієї лекції розраховані на запуск файлом.
Модуль pathlib: шлях як обʼєкт¶
Історично шляхи в Python були звичайними рядками, а працювали з ними функції модуля os.path. Сучасний підхід — клас Path з модуля pathlib: шлях стає обʼєктом зі своїми методами.
Головна зручність — оператор /, який зʼєднує частини шляху:
# Program: build a path with the / operator
from pathlib import Path
base = Path("/home/student/projects")
project = base / "lab21"
data_file = project / "data" / "grades.txt"
print(base)
print(project)
print(data_file)
parts = ["reports", "2026", "march.txt"]
print(Path("/home/student").joinpath(*parts))
/home/student/projects
/home/student/projects/lab21
/home/student/projects/lab21/data/grades.txt
/home/student/reports/2026/march.txt
Оператор / тут не ділення: для обʼєктів Path він перевизначений і означає «зайти всередину». Це набагато надійніше за склеювання рядків через +, де легко забути або подвоїти роздільник.
# Program: string concatenation is fragile, Path is not
from pathlib import Path
folder = "data/"
name = "grades.txt"
print(folder + "/" + name)
print(Path("data") / name)
Path приймає і звичайний рядок, і навпаки — будь-яку функцію, що очікує рядок зі шляхом, можна викликати з Path (зокрема open()).
Частини шляху¶
Обʼєкт Path дає доступ до складників шляху без жодного розбору рядка вручну.
# Program: parts of a path
from pathlib import Path
path = Path("/home/student/projects/lab21/data/grades.txt")
print(path.name)
print(path.stem)
print(path.suffix)
print(path.parent)
print(path.parent.parent)
print(path.anchor)
print(path.parts)
print(path.with_suffix(".csv"))
print(path.with_name("marks.txt"))
grades.txt
grades
.txt
/home/student/projects/lab21/data
/home/student/projects/lab21
/
('/', 'home', 'student', 'projects', 'lab21', 'data', 'grades.txt')
/home/student/projects/lab21/data/grades.csv
/home/student/projects/lab21/data/marks.txt
| Властивість | Що дає |
|---|---|
name |
останній компонент шляху (імʼя файлу з розширенням) |
stem |
імʼя без розширення |
suffix |
розширення разом з крапкою |
parent |
каталог, у якому лежить файл |
parts |
кортеж усіх компонентів |
anchor |
корінь шляху |
with_suffix(s) |
новий шлях з іншим розширенням |
with_name(n) |
новий шлях з іншим імʼям файлу |
Зверніть увагу: Path — незмінюваний обʼєкт, як рядок чи кортеж. with_suffix і with_name не змінюють path, а повертають новий шлях.
Перевірка існування, створення каталогів¶
Досі всі приклади лише розбирали шлях як текст і до диска не зверталися. Наступні методи вже дивляться на реальну файлову систему.
# Program: check what exists on disk
from pathlib import Path
work_dir = Path("lab21_demo")
work_dir.mkdir(exist_ok=True)
data_dir = work_dir / "data"
data_dir.mkdir(exist_ok=True)
note = data_dir / "note.txt"
note.write_text("hello\n", encoding="utf-8")
print(work_dir.exists(), work_dir.is_dir(), work_dir.is_file())
print(note.exists(), note.is_dir(), note.is_file())
print(note.stat().st_size)
missing = data_dir / "nothing.txt"
print(missing.exists())
mkdir()створює каталог. Без аргументів він падає з помилкою, якщо каталог уже є;exist_ok=Trueце дозволяє. Аргументparents=Trueдодатково створює всі відсутні проміжні каталоги.exists(),is_file(),is_dir()— перевірки наявності й типу.stat().st_size— розмір файлу в байтах.
Перевірка exists() перед відкриттям файлу — корисна звичка, але не гарантія: між перевіркою і відкриттям файл теоретично може зникнути. Надійний спосіб — обробка винятків, і саме їй присвячена наступна лекція.
Перегляд вмісту каталогу¶
# Program: list files in a directory
from pathlib import Path
work_dir = Path("lab21_list")
work_dir.mkdir(exist_ok=True)
for name in ["grades.txt", "names.txt", "report.md"]:
(work_dir / name).write_text("demo\n", encoding="utf-8")
(work_dir / "backup").mkdir(exist_ok=True)
print("all entries:")
for entry in sorted(work_dir.iterdir()):
kind = "dir " if entry.is_dir() else "file"
print(f" {kind} {entry.name}")
print("only .txt:")
for entry in sorted(work_dir.glob("*.txt")):
print(f" {entry.name}")
all entries:
dir backup
file grades.txt
file names.txt
file report.md
only .txt:
grades.txt
names.txt
iterdir()перебирає все, що лежить у каталозі (без заходу вглиб).glob("*.txt")відбирає за шаблоном:*— будь-яка послідовність символів,?— один символ.rglob("*.txt")шукає за тим самим шаблоном рекурсивно, у всіх вкладених каталогах.
Порядок, у якому iterdir() повертає записи, не визначений — тому в прикладі стоїть sorted().
Аргументи командного рядка¶
Досі шлях до файлу був записаний прямо в коді. Але справжні програми отримують його ззовні — так само, як python3, git чи cp отримують те, з чим мають працювати:
Усе, що написано в терміналі після назви скрипта, потрапляє у програму списком аргументів командного рядка. Python складає їх у список sys.argv.
# Program: what the program receives from the command line
import sys
print(sys.argv)
print("count: ", len(sys.argv))
print("script: ", sys.argv[0])
print("arguments:", sys.argv[1:])
$ python3 show_args.py data/grades.txt 10 --quiet
['show_args.py', 'data/grades.txt', '10', '--quiet']
count: 4
script: show_args.py
arguments: ['data/grades.txt', '10', '--quiet']
$ python3 show_args.py
['show_args.py']
count: 1
script: show_args.py
arguments: []
Три властивості sys.argv, які треба запамʼятати:
sys.argv[0]— це сам скрипт, а не перший аргумент. Власні аргументи починаються з індексу1, тому їх зручно брати зрізомsys.argv[1:].- Аргументів може не бути взагалі. Тоді в списку лише один елемент, а
sys.argv[1]дастьIndexError. Кількість треба перевіряти до звертання. - Усі аргументи — рядки. Навіть
10приходить як"10".
# Program: arguments are always strings
import sys
value = sys.argv[1] if len(sys.argv) > 1 else "10"
print(type(value).__name__, repr(value))
print(value * 2)
print(int(value) * 2)
value * 2 для рядка "7" дає "77", і саме тут ховається помилка, яку легко не помітити: програма не падає, вона просто рахує не те. Число з аргументу треба явно перетворити через int() або float().
Рядок sys.argv[1] if len(sys.argv) > 1 else "10" — це умовний вираз: він повертає перше значення, якщо умова істинна, і друге, якщо ні. Його звичайна форма — звичайний if:
# Program: the same choice written as a plain if
import sys
if len(sys.argv) > 1:
value = sys.argv[1]
else:
value = "10"
print(int(value) * 2)
Розділення аргументів і лапки¶
Термінал розрізає командний рядок за пробілами. Тому імʼя файлу з пробілом усередині треба взяти в лапки, інакше воно приїде двома окремими аргументами:
$ python3 show_args.py my report.txt
['show_args.py', 'my', 'report.txt']
count: 3
script: show_args.py
arguments: ['my', 'report.txt']
$ python3 show_args.py "my report.txt"
['show_args.py', 'my report.txt']
count: 2
script: show_args.py
arguments: ['my report.txt']
Аргумент-шлях відлічується від робочого каталогу¶
Це найважливіша річ у цьому розділі. Користувач набирає шлях у терміналі, а отже, він має на увазі поточний робочий каталог, а не каталог, де лежить скрипт.
Тут data/grades.txt означає /home/student/projects/lab21/data/grades.txt. Якщо ту саму програму запустити з іншого місця, той самий аргумент означатиме інший файл — і це правильна поведінка, саме її очікує користувач.
Отже, правило таке:
| Що за шлях | Від чого будувати |
|---|---|
файл, який назвав користувач (sys.argv) |
від робочого каталогу — тобто Path(sys.argv[1]) без додавання BASE |
| власні дані програми (налаштування, журнал, шаблони) | від каталогу скрипта — Path(__file__).resolve().parent / ... |
Найгрубіша помилка — склеїти каталог скрипта з аргументом користувача: BASE / sys.argv[1]. Тоді користувач напише шлях, який бачить у себе в терміналі, а програма шукатиме зовсім не там.
Приклад: лічильник рядків¶
Зберемо все разом — маленька програма в дусі системної утиліти wc -l.
# Program: count lines in the file named on the command line
import sys
from pathlib import Path
BASE = Path(__file__).resolve().parent
script_name = Path(sys.argv[0]).name
if len(sys.argv) > 1:
path = Path(sys.argv[1])
else:
path = BASE / "argv_demo.txt"
path.write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
print(f"usage: python3 {script_name} <file>")
print(f"no file given, demo file {path.name} created")
if not path.is_file():
print(f"error: file not found: {path}")
sys.exit(1)
lines = 0
words = 0
with open(path, encoding="utf-8") as f:
for line in f:
lines += 1
words += len(line.split())
print(f"{path.name}: {lines} lines, {words} words")
$ python3 count_lines.py
usage: python3 count_lines.py <file>
no file given, demo file argv_demo.txt created
argv_demo.txt: 3 lines, 3 words
$ python3 count_lines.py data/grades.txt
grades.txt: 4 lines, 9 words
$ python3 count_lines.py nothing.txt
error: file not found: nothing.txt
Що тут варто помітити:
Path(sys.argv[0]).nameдає коротку назву скрипта для підказкиusage— не доведеться переписувати її після перейменування файлу.- Перевірка
len(sys.argv) > 1стоїть перед звертанням доsys.argv[1]. path.is_file()відсіює і відсутній файл, і випадок, коли за цим шляхом лежить каталог.sys.exit(1)завершує програму з кодом помилки: нуль означає успіх, будь-яке інше число — збій. За цим кодом термінал та інші програми розуміють, чи все пройшло добре.
Коли аргументів стає багато
Поки аргумент один-два, sys.argv цілком достатньо. Для серйозної утиліти з ключами (--output report.txt, -v, --help) у стандартній бібліотеці є модуль argparse: він сам розбирає ключі, перевіряє типи і генерує довідку. У цьому курсі він не обовʼязковий, але знати про нього варто.
Відкриття файлу: open()¶
Щоб працювати з вмістом файлу, його треба відкрити. Вбудована функція open() повертає обʼєкт файлу — щось на кшталт «труби», через яку тече текст.
file— шлях (рядок абоPath);mode— режим: читання, запис, додавання;encoding— кодування тексту.
Після роботи файл треба закрити методом close(), інакше дані можуть не потрапити на диск, а операційна система витрачатиме ресурси на відкритий дескриптор.
# Program: open, use, close - the long way
f = open("temp_manual.txt", "w", encoding="utf-8")
f.write("line one\n")
f.close()
f = open("temp_manual.txt", "r", encoding="utf-8")
print(f.read())
f.close()
Проблема цього запису в тому, що close() легко забути, а якщо між open() і close() станеться помилка — до закриття справа взагалі не дійде. Тому так майже ніколи не пишуть.
Менеджер контексту with¶
Правильний спосіб — конструкція with:
# Program: the with statement closes the file for you
with open("temp_with.txt", "w", encoding="utf-8") as f:
f.write("line one\n")
f.write("line two\n")
print("closed:", f.closed)
with open("temp_with.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content, end="")
with open(...) as f: відкриває файл, привʼязує обʼєкт файлу до змінної f і гарантовано закриває його при виході з блоку — і після нормального завершення, і після помилки. Змінна f після блоку залишається, але файл уже закритий, тож читати з нього не можна.
Кілька файлів відкривають в одному with через кому:
# Program: two files at once
with open("source_demo.txt", "w", encoding="utf-8") as f:
f.write("alpha\nbeta\ngamma\n")
with open("source_demo.txt", "r", encoding="utf-8") as src, \
open("target_demo.txt", "w", encoding="utf-8") as dst:
for line in src:
dst.write(line.upper())
print(open("target_demo.txt", encoding="utf-8").read(), end="")
Далі у лекції всі приклади використовують лише with.
Кодування тексту¶
Файл на диску — це послідовність байтів, тобто чисел від 0 до 255. Літера — не число, тому потрібна домовленість, яким байтам відповідає який символ. Така домовленість називається кодуванням (encoding).
Сучасний стандарт — UTF-8: латинська літера займає в ньому один байт, кирилична — два, емодзі — чотири.
# Program: text becomes bytes
plain = "Grade: 5"
print(len(plain), len(plain.encode("utf-8")))
accented = "Cafe resume"
print(len(accented), len(accented.encode("utf-8")))
accented = "Café résumé"
print(len(accented), len(accented.encode("utf-8")))
print(accented.encode("utf-8"))
Рядок з 11 символів займає 11 байтів, поки всі символи — звичайна латиниця. Щойно зʼявляються символи поза ASCII (тут — é, у ваших даних — кирилиця), кожен такий символ займає по кілька байтів, і довжина в символах перестає дорівнювати довжині в байтах.
Якщо не вказати encoding, Python візьме кодування, прийняте в системі, а воно на різних компʼютерах різне. Результат — файл, записаний в одного, не читається в іншого: замість тексту зʼявляється UnicodeDecodeError або «кракозябри».
Правило без винятків
Відкриваючи текстовий файл, завжди вказуйте encoding="utf-8". Це один аргумент, який економить години пошуку причини незрозумілих символів.
Режими відкриття файлу¶
Другий аргумент open() визначає, що саме дозволено робити з файлом.
| Режим | Назва | Якщо файлу немає | Якщо файл є | Позиція на старті |
|---|---|---|---|---|
"r" |
читання (за замовчуванням) | FileNotFoundError |
читаємо | початок |
"w" |
запис | створюється | вміст стирається | початок |
"a" |
додавання | створюється | зберігається | кінець |
"x" |
ексклюзивне створення | створюється | FileExistsError |
початок |
"r+" |
читання і запис | FileNotFoundError |
зберігається | початок |
"w+" |
запис і читання | створюється | вміст стирається | початок |
"a+" |
додавання і читання | створюється | зберігається | кінець |
До літери режиму можна додати "t" (текстовий, за замовчуванням) або "b" (двійковий): "rb", "wb".
# Program: w truncates, a appends
with open("mode_demo.txt", "w", encoding="utf-8") as f:
f.write("first run\n")
with open("mode_demo.txt", "w", encoding="utf-8") as f:
f.write("second run\n")
print(open("mode_demo.txt", encoding="utf-8").read(), end="")
print("---")
with open("mode_demo.txt", "a", encoding="utf-8") as f:
f.write("appended line\n")
print(open("mode_demo.txt", encoding="utf-8").read(), end="")
"w" стирає файл мовчки
Режим "w" обрізає файл до нуля в момент відкриття, ще до першого write(). Якщо переплутати "w" з "r", дані зникнуть без жодного запитання і без можливості скасувати. Коли треба дописати — режим "a".
Читання: read, readline, readlines, перебір¶
Створимо файл і прочитаємо його чотирма способами.
# Program: four ways to read a text file
from pathlib import Path
path = Path("students_demo.txt")
path.write_text("Shevchenko 95\nKovalenko 88\nBondar 73\n", encoding="utf-8")
with open(path, encoding="utf-8") as f:
whole = f.read()
print(repr(whole))
with open(path, encoding="utf-8") as f:
first = f.readline()
second = f.readline()
print(repr(first), repr(second))
with open(path, encoding="utf-8") as f:
lines = f.readlines()
print(lines)
with open(path, encoding="utf-8") as f:
for number, line in enumerate(f, start=1):
print(number, line.strip())
'Shevchenko 95\nKovalenko 88\nBondar 73\n'
'Shevchenko 95\n' 'Kovalenko 88\n'
['Shevchenko 95\n', 'Kovalenko 88\n', 'Bondar 73\n']
1 Shevchenko 95
2 Kovalenko 88
3 Bondar 73
| Спосіб | Що повертає | Коли доречний |
|---|---|---|
f.read() |
увесь вміст одним рядком | малий файл, потрібен текст цілком |
f.read(n) |
не більше n символів |
читання порціями |
f.readline() |
наступний рядок разом з \n |
рядок за рядком вручну |
f.readlines() |
список усіх рядків | малий файл, потрібен список |
for line in f |
рядки по одному | основний спосіб, файл будь-якого розміру |
Ключовий момент: кожен прочитаний рядок містить у кінці символ переходу \n (крім, можливо, останнього). Майже завжди його прибирають через strip() або rstrip("\n").
# Program: the newline character at the end of a line
from pathlib import Path
path = Path("strip_demo.txt")
path.write_text("42\n17\n", encoding="utf-8")
with open(path, encoding="utf-8") as f:
for line in f:
print(repr(line), repr(line.strip()), int(line))
int() сам ігнорує пробіли навколо числа, тому тут працює і без strip(). А от порівняння line == "42" без strip() дасть False — про це в розділі типових помилок.
Чому перебір for line in f кращий за readlines(): readlines() завантажує весь файл у памʼять списком. Для файлу на 2 ГБ це означає 2 ГБ памʼяті. Перебір читає по рядку і памʼять майже не витрачає.
# Program: count lines, words and characters without loading the whole file
from pathlib import Path
path = Path("wc_demo.txt")
path.write_text(
"the quick brown fox\n"
"jumps over the lazy dog\n"
"python reads files line by line\n",
encoding="utf-8",
)
lines = 0
words = 0
chars = 0
with open(path, encoding="utf-8") as f:
for line in f:
lines += 1
words += len(line.split())
chars += len(line)
print(f"lines: {lines}")
print(f"words: {words}")
print(f"chars: {chars}")
Запис: write, writelines, print(file=...)¶
# Program: three ways to write text
grades = [95, 88, 73]
with open("write_demo.txt", "w", encoding="utf-8") as f:
f.write("report\n")
f.write("=" * 6 + "\n")
with open("write_demo.txt", "a", encoding="utf-8") as f:
f.writelines([f"grade {g}\n" for g in grades])
with open("write_demo.txt", "a", encoding="utf-8") as f:
average = sum(grades) / len(grades)
print(f"average: {average:.2f}", file=f)
print(open("write_demo.txt", encoding="utf-8").read(), end="")
Три важливі деталі:
write()не додає\n. На відміну відprint(), метод записує рівно те, що йому дали. Перехід на новий рядок пишемо самі.writelines()теж не додає\n— попри назву, це просто «записати послідовність рядків підряд». Без\nусе злипнеться в один рядок.print(..., file=f)пише у файл замість екрана і поводиться як звичайнийprint: додає\n, вмієsepі перетворює числа на текст сам.
# Program: write() needs strings and explicit newlines
numbers = [1, 2, 3]
with open("write_pitfall.txt", "w", encoding="utf-8") as f:
f.writelines(str(n) for n in numbers)
f.write("\n")
f.writelines(f"{n}\n" for n in numbers)
print(*numbers, sep=", ", file=f)
print(open("write_pitfall.txt", encoding="utf-8").read(), end="")
Спроба записати число напряму — f.write(42) — дає TypeError: write() argument must be str, not int. Число треба перетворити на рядок: f.write(str(42)) або f-рядком.
Додавання в кінець і безпечне створення¶
Режим "a" зручний для журналів: кожен запуск дописує рядок, нічого не стираючи.
# Program: a simple event log
from datetime import datetime
from pathlib import Path
log_path = Path("events_demo.log")
if log_path.exists():
log_path.unlink()
def log(message):
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_path, "a", encoding="utf-8") as f:
f.write(f"[{stamp}] {message}\n")
log("program started")
log("data loaded")
log("program finished")
with open(log_path, encoding="utf-8") as f:
for line in f:
print(line.rstrip())
[2026-09-26 10:15:03] program started
[2026-09-26 10:15:03] data loaded
[2026-09-26 10:15:03] program finished
Час у вашому виводі буде свій. Зверніть увагу: файл відкривається і закривається на кожен запис — так рядок гарантовано потрапляє на диск, навіть якщо програма аварійно завершиться.
Режим "x" створює файл лише тоді, коли його ще немає, і цим захищає від випадкового перезапису:
# Program: create a file only if it does not exist yet
from pathlib import Path
path = Path("unique_demo.txt")
if path.exists():
path.unlink()
with open(path, "x", encoding="utf-8") as f:
f.write("created once\n")
print("created:", path.read_text(encoding="utf-8").strip())
if path.exists():
print("second attempt would raise FileExistsError")
Тут ми обійшлися перевіркою exists(). Коректніший спосіб — перехопити FileExistsError через try/except, і це тема наступної лекції.
Швидкі методи read_text і write_text¶
Для невеликих файлів pathlib дає скорочення, які самі відкривають і закривають файл.
# Program: read_text and write_text
from pathlib import Path
path = Path("quick_demo.txt")
path.write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
text = path.read_text(encoding="utf-8")
print(repr(text))
print(text.splitlines())
print(len(text.splitlines()))
write_text() працює як режим "w" — повністю перезаписує файл. splitlines() розбиває текст на рядки і прибирає \n, на відміну від readlines().
Коли що використовувати:
| Ситуація | Інструмент |
|---|---|
| файл малий, потрібен увесь текст | Path.read_text() |
| файл великий або обробка порядкова | with open(...) і for line in f |
| перезаписати малий файл цілком | Path.write_text() |
| писати поступово, дописувати, вести лог | with open(..., "a") |
Позиція у файлі: tell і seek¶
Відкритий файл має позицію — місце, з якого відбудеться наступне читання. Кожна операція читання зсуває її вперед. Саме тому другий read() поспіль повертає порожній рядок: позиція вже в кінці.
# Program: the file position moves as you read
from pathlib import Path
path = Path("seek_demo.txt")
path.write_text("abcdefghij", encoding="utf-8")
with open(path, encoding="utf-8") as f:
print(f.tell())
print(f.read(3), f.tell())
print(f.read(3), f.tell())
print(repr(f.read()), f.tell())
print(repr(f.read()))
f.seek(0)
print(f.tell(), f.read(4))
tell()повідомляє поточну позицію;seek(0)повертає до початку, після чого файл можна прочитати ще раз.
Порожній результат другого read() — часта причина здивування: «чому список порожній, файл же не порожній». Відповідь — позиція. Не зловживайте seek: у переважній більшості задач достатньо один раз прочитати файл у змінну.
Двійкові файли¶
Не всякий файл — текст. Зображення, звук, PDF, архіви — послідовності байтів, які не розбиваються на символи. Для них використовують режими "rb" і "wb", і тоді read() повертає не str, а bytes. Аргумент encoding у двійковому режимі не вказують — його там не буває.
# Program: write and read raw bytes
from pathlib import Path
path = Path("bytes_demo.bin")
with open(path, "wb") as f:
f.write(bytes([80, 89, 84, 72, 79, 78]))
f.write(b"\x00\x01\x02")
with open(path, "rb") as f:
data = f.read()
print(type(data).__name__)
print(data)
print(len(data))
print(list(data))
print(data[:6].decode("ascii"))
Копіювання будь-якого файлу без урахування вмісту робиться саме у двійковому режимі:
# Program: copy a file byte by byte
from pathlib import Path
source = Path("copy_source.bin")
source.write_bytes(bytes(range(32)))
target = Path("copy_target.bin")
with open(source, "rb") as src, open(target, "wb") as dst:
while True:
chunk = src.read(8)
if not chunk:
break
dst.write(chunk)
print(source.stat().st_size, target.stat().st_size)
print(source.read_bytes() == target.read_bytes())
Читання порціями (chunk) дозволяє копіювати файл будь-якого розміру, не завантажуючи його в памʼять цілком.
Копіювання, перейменування, видалення¶
Для дій над файлами як над обʼєктами файлової системи є pathlib і модуль shutil.
# Program: copy, rename and delete files
import shutil
from pathlib import Path
work = Path("fileops_demo")
work.mkdir(exist_ok=True)
original = work / "report.txt"
original.write_text("results\n", encoding="utf-8")
copy = shutil.copy(original, work / "report_backup.txt")
print(Path(copy).name, Path(copy).read_text(encoding="utf-8").strip())
renamed = original.rename(work / "final_report.txt")
print(renamed.name, original.exists())
(work / "report_backup.txt").unlink()
print(sorted(p.name for p in work.iterdir()))
shutil.rmtree(work)
print(work.exists())
| Дія | Виклик |
|---|---|
| копіювати файл | shutil.copy(src, dst) |
| копіювати каталог з вмістом | shutil.copytree(src, dst) |
| перейменувати або перемістити | path.rename(new_path) |
| видалити файл | path.unlink() |
| видалити порожній каталог | path.rmdir() |
| видалити каталог з вмістом | shutil.rmtree(path) |
Видалення незворотне
unlink() і особливо rmtree() не переносять нічого в кошик — дані зникають одразу. Перед запуском коду, що видаляє, тричі перевірте шлях. Особливо небезпечні шляхи, зібрані з даних користувача.
Приклад: журнал оцінок у файлі¶
Зберемо все разом: побудова шляхів, запис, читання, розбір рядків, звіт у новий файл.
# Program: store, read and report a grade journal
from pathlib import Path
def build_paths():
"""Return the data directory and the two file paths."""
base_dir = Path(__file__).resolve().parent
data_dir = base_dir / "journal_data"
data_dir.mkdir(exist_ok=True)
return data_dir / "grades.txt", data_dir / "report.txt"
def save_journal(path, journal):
"""Write the journal as lines 'name: g1,g2,g3'."""
with open(path, "w", encoding="utf-8") as f:
for name, grades in journal.items():
joined = ",".join(str(g) for g in grades)
f.write(f"{name}: {joined}\n")
def load_journal(path):
"""Read the journal back into a dictionary."""
journal = {}
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
name, _, grades_text = line.partition(":")
grades = [int(part) for part in grades_text.split(",") if part.strip()]
journal[name.strip()] = grades
return journal
def add_grade(path, name, grade):
"""Append one more grade for a student."""
journal = load_journal(path)
journal.setdefault(name, []).append(grade)
save_journal(path, journal)
def write_report(path, journal):
"""Write a formatted report with averages."""
with open(path, "w", encoding="utf-8") as f:
print(f"{'student':<14}{'grades':<18}{'average':>8}", file=f)
print("-" * 40, file=f)
for name in sorted(journal):
grades = journal[name]
average = sum(grades) / len(grades) if grades else 0.0
listed = " ".join(str(g) for g in grades)
print(f"{name:<14}{listed:<18}{average:>8.2f}", file=f)
total = [g for grades in journal.values() for g in grades]
print("-" * 40, file=f)
print(f"{'group':<14}{len(total):<18}{sum(total) / len(total):>8.2f}", file=f)
def main():
grades_path, report_path = build_paths()
save_journal(grades_path, {
"Shevchenko": [95, 88, 100],
"Kovalenko": [73, 81],
"Bondar": [60, 67, 72, 90],
})
add_grade(grades_path, "Kovalenko", 94)
add_grade(grades_path, "Marchenko", 85)
journal = load_journal(grades_path)
write_report(report_path, journal)
print("raw file:")
print(grades_path.read_text(encoding="utf-8"), end="")
print()
print("report:")
print(report_path.read_text(encoding="utf-8"), end="")
main()
raw file:
Shevchenko: 95,88,100
Kovalenko: 73,81,94
Bondar: 60,67,72,90
Marchenko: 85
report:
student grades average
----------------------------------------
Bondar 60 67 72 90 72.25
Kovalenko 73 81 94 82.67
Marchenko 85 85.00
Shevchenko 95 88 100 94.33
----------------------------------------
group 11 82.27
Що варто помітити:
- Шляхи будуються від
__file__, тож програма працює з будь-якого робочого каталогу. data_dir.mkdir(exist_ok=True)створює каталог для даних при першому запуску.load_journalпропускає порожні рядки і рядки-коментарі — реальний файл рідко буває ідеальним.partition(":")ділить рядок за першим двокрапкою, тож двокрапка в даних не зламає розбір.- Читання і запис розведені по окремих функціях: так їх легко перевірити і повторно використати.
- Звіт пишеться через
print(..., file=f)зі специфікаторами ширини — вирівняні колонки без ручного підрахунку пробілів.
Типові помилки¶
| Помилка | Причина | Виправлення |
|---|---|---|
FileNotFoundError: [Errno 2] No such file or directory: 'data.txt' |
відносний шлях відлічується від cwd, а не від скрипта | Path(__file__).resolve().parent / "data.txt" |
FileNotFoundError у режимі "w" |
немає каталогу, у який пишемо (сам файл "w" створює, каталог — ні) |
path.parent.mkdir(parents=True, exist_ok=True) |
| Файл був — і став порожній | відкрито в режимі "w" замість "r" або "a" |
режим "a" для дописування |
TypeError: write() argument must be str, not int |
у write() передали число |
f.write(str(n)) або f-рядок |
| Усе записалося в один рядок | write/writelines не додають \n |
додавати \n самому |
UnicodeDecodeError |
файл у UTF-8, а прочитано в іншому кодуванні | encoding="utf-8" при відкритті |
Другий read() повертає "" |
позиція вже в кінці файлу | зберегти текст у змінну або f.seek(0) |
ValueError: I/O operation on closed file |
звертання до f після блоку with |
працювати з файлом усередині блоку |
line == "text" дає False |
у прочитаному рядку лишився \n |
line.strip() == "text" |
ValueError: invalid literal for int() |
порожній рядок у кінці файлу | пропускати порожні рядки перевіркою if not line: continue |
open("~/notes.txt") не знаходить файл |
тильду розгортає термінал, не Python | Path("~/notes.txt").expanduser() |
FileExistsError у mkdir() |
каталог уже існує | mkdir(exist_ok=True) |
IndexError: list index out of range |
звертання до sys.argv[1], коли аргумент не передали |
перевірити len(sys.argv) > 1 |
Аргумент-число поводиться як текст ("7" * 2 дає "77") |
усі елементи sys.argv — рядки |
int(sys.argv[1]) |
| Програма не знаходить файл, який користувач бачить у терміналі | шлях з аргументу склеїли з каталогом скрипта | Path(sys.argv[1]) без BASE |
# Program: the four classic file mistakes and their fixes
from pathlib import Path
path = Path("mistakes_demo.txt")
# 1. write не додає перехід рядка
with open(path, "w", encoding="utf-8") as f:
f.writelines(["a", "b", "c"])
print(repr(path.read_text(encoding="utf-8")))
with open(path, "w", encoding="utf-8") as f:
f.writelines([f"{x}\n" for x in "abc"])
print(repr(path.read_text(encoding="utf-8")))
# 2. прочитаний рядок містить \n
path.write_text("done\n", encoding="utf-8")
with open(path, encoding="utf-8") as f:
line = f.readline()
print(line == "done", line.strip() == "done")
# 3. після першого read позиція в кінці
with open(path, encoding="utf-8") as f:
print(repr(f.read()), repr(f.read()))
# 4. число треба перетворити на рядок
with open(path, "w", encoding="utf-8") as f:
f.write(str(42) + "\n")
print(path.read_text(encoding="utf-8"), end="")
Підсумок¶
- Абсолютний шлях починається з
/і відлічується від кореня файлової системи; відносний — від поточного робочого каталогу, який залежить від місця запуску програми, а не від місця файлу з кодом. .— поточний каталог,..— батьківський,~— домашній каталог (розгортає термінал, у Python —expanduser()).- Робочий каталог:
os.getcwd()абоPath.cwd(). Каталог самого скрипта:Path(__file__).resolve().parent— саме від нього варто будувати шляхи до даних проєкту. - Аргументи командного рядка приходять у
sys.argv:sys.argv[0]— сам скрипт, власні аргументи —sys.argv[1:], усі вони рядки. Кількість перевіряйте до звертання за індексом. - Шлях з аргументу відлічується від робочого каталогу (
Path(sys.argv[1])безBASE), а власні дані програми — від каталогу скрипта. pathlib.Pathзʼєднує частини шляху оператором/і даєname,stem,suffix,parent,parts,resolve(),exists(),is_file(),is_dir(),mkdir(),iterdir(),glob().- Файл відкривають через
with open(path, mode, encoding="utf-8") as f:—withзакриває файл завжди, навіть після помилки. - Кодування вказують завжди:
encoding="utf-8". - Режими:
"r"— читати,"w"— перезаписати (стирає вміст),"a"— дописати в кінець,"x"— створити лише новий,"b"— двійковий. - Читання:
read(),read(n),readline(),readlines(), а основний спосіб — перебірfor line in f(не залежить від розміру файлу). - Кожен прочитаний рядок закінчується на
\n— прибирайте його черезstrip(). - Запис:
write()(лише рядки, без автоматичного\n),writelines()(теж без\n),print(..., file=f)(як звичайнийprint). - Для малих файлів є скорочення
Path.read_text()іPath.write_text(). - Позиція у файлі рухається під час читання;
tell()показує її,seek(0)повертає на початок. - Двійкові файли читають у
bytesрежимами"rb"/"wb", безencoding. - Операції над файлами:
shutil.copy,path.rename,path.unlink,shutil.rmtree— видалення незворотне.
Корисні посилання¶
- Читання і запис файлів — підручник Python
- Функція
open()— довідник - Модуль
pathlib - Модуль
os.path - Модуль
shutil sys.argv— довідник- Модуль
argparse - Unicode у Python
Домашнє завдання¶
Мета — навчитися будувати шляхи, зберігати дані між запусками програми і розбирати текстовий файл на структуровані дані. Усі дані — ваші власні, латиницею. Кожне завдання — окремий запуск програми; шляхи до всіх файлів будуйте від каталогу скрипта.
-
Напишіть програму, яка виводить: поточний робочий каталог, домашній каталог, абсолютний шлях до файлу самої програми і його батьківський каталог. Запустіть її двічі — з каталогу проєкту і з домашнього каталогу (
python3 projects/.../main.py). Збережіть обидва виводи і двома реченнями поясніть, що змінилося і чому. -
Для трьох шляхів —
data/grades.txt,../notes.txtі абсолютного шляху до вашого файлу програми — виведіть: чи шлях абсолютний, результатresolve(),name,stem,suffix,parent. Для кожного напишіть одним реченням, від чого відлічується цей шлях. -
Створіть каталог
my_dataпоруч зі скриптом і запишіть у нього файлabout_me.txt: вашe імʼя, прізвище, група, рік народження і улюблена мова програмування — по одному полю в рядку у форматіkey: value. Потім прочитайте файл, перетворіть його на словник і виведіть цей словник та значення за ключемgroup. -
Запишіть свої оцінки (не менше восьми) у файл
grades.txt, по одній у рядку. Окремою функцією прочитайте їх у список чисел і виведіть: кількість, суму, середнє з двома знаками після коми, найвищу та найнижчу оцінку, а також кількість оцінок вище середнього. Порожні рядки у файлі мають ігноруватися. -
Напишіть функцію
append_visit(path), яка дописує у файлvisits.logрядок з номером запуску і поточним часом (datetime.now()). Номер обчислюйте як кількість уже наявних у файлі рядків плюс один. Запустіть програму пʼять разів і покажіть вміст файлу. -
Створіть файл
students.txt, де кожен рядок має виглядsurname;name;group;grade1,grade2,grade3— ви і щонайменше чотири одногрупники. Напишіть програму, яка читає цей файл і записує уreport.txtтаблицю з вирівняними колонками: прізвище, імʼя, група, середній бал (два знаки після коми), і окремим рядком — середній бал по групі. Рядки, що починаються з#, мають пропускатися. -
Напишіть програму, яка читає
report.txtіз завдання 6 і створює його резервну копію з імʼям видуreport_backup.txt, використавшиwith_name()абоwith_stem(). Виведіть розміри обох файлів у байтах і доведіть рівність їхнього вмісту. -
Напишіть функцію
count_words(path), яка читає текстовий файл і повертає словник «слово → кількість» (без урахування регістру, розділові знаки прибрати). Застосуйте її до файлу з текстом про себе щонайменше на 50 слів і виведіть пʼять найчастіших слів. Результат запишіть уwords.txtу форматіword: count, відсортований за спаданням кількості. -
Напишіть програму, яка перебирає всі файли у каталозі скрипта через
iterdir()і виводить таблицю: імʼя файлу, розширення, розмір у байтах, тип (fileабоdir). Окремо виведіть загальну кількість файлів.pyі.txt, знайдених черезglob(). -
Двійковий режим. Візьміть будь-яке зображення, скопіюйте його побайтово порціями по 1024 байти у новий файл і доведіть, що копія збігається з оригіналом (порівняйте розміри і вміст
read_bytes()). Потім виведіть перші 8 байтів оригіналу у вигляді списку чисел і поясніть одним реченням, чому цей файл не можна відкрити в текстовому режимі.
Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub