Към съдържанието
wannacode

Файлове

Проблем

Програмата забравя

Пишеш програма за разходите си. Въвеждаш ги вечер, затваряш я, а на другия ден тя не знае нищо за вчера. Ето най-краткият пример:

visits = 0
visits += 1
print(f"Пусната {visits} път.")
Изход
Пусната 1 път.

Колкото пъти и да я пуснеш, казва „1 път“. Променливите живеят в паметта, докато програмата работи. Когато свърши, паметта се освобождава и всичко изчезва.

За да помни, програмата трябва да запише данните там, където остават и след нея: във файл на диска.

Идеята

Файл с open и with

Ето цяла програма, която записва бележка във файл и я чете обратно:

notes.py
try:
with open("notes.txt", "w", encoding="utf-8") as file:
file.write("Купи мляко\nПлати тока до 15-ти\n")
with open("notes.txt", encoding="utf-8") as file:
text = file.read()
print(text, end="")
except OSError as e:
print(f"Проблем с файла: {e}")
Изход
Купи мляко
Плати тока до 15-ти
  • open(име, режим) отваря файла. "notes.txt" е файл с това име в папката, от която е пусната програмата. Режимът "w" (write) е за писане: създава файла, ако го няма, и го изпразва, ако го има. Без режим файлът се отваря за четене.
  • with ... as file: дава отворения файл под името file и го затваря сам, когато отместените редове свършат. Защо затварянето е важно, ще видиш по-нататък.
  • file.write(текст) записва, а file.read() чете целия файл като един текст.
  • encoding="utf-8" казва как буквите стават байтове. Пиши го винаги: без него Python ползва кодировката на компютъра, а на Windows тя често не е UTF-8 и кирилицата излиза сбъркана.
  • OSError е общото изключение за проблем с файл: дискът е пълен, файлът е заключен или го няма. Python не те задължава да го хванеш (компилаторът на Java го изисква), но без except програмата спира.

Пусни програмата, после отвори папката: notes.txt е там и може да се отвори с всеки текстов редактор. Следващото пускане ще го намери.

Идеята

Добави и прочети редовете

Разходите се добавят един по един. Режимът "a" (append, „добави“) дописва в края, вместо да записва файла наново. read().splitlines() разделя прочетения текст на редове:

try:
with open("expenses.txt", "w", encoding="utf-8") as file:
file.write("Обяд;12.50\n")
with open("expenses.txt", "a", encoding="utf-8") as file:
file.write("Такси;8.20\n")
with open("expenses.txt", "a", encoding="utf-8") as file:
file.write("Книга;18.90\n")
with open("expenses.txt", encoding="utf-8") as file:
lines = file.read().splitlines()
total = 0
for line in lines:
parts = line.split(";")
total += float(parts[1])
print(f"{parts[0]}: {parts[1]} €")
print(f"{len(lines)} разхода, общо {total:.2f} €")
except OSError as e:
print(f"Проблем с файла: {e}")
Изход
Обяд: 12.50 €
Такси: 8.20 €
Книга: 18.90 €
3 разхода, общо 39.60 €

lines е списък от текстове, по един за всеки ред, без знака за нов ред. Обхождаш го с for, а len(lines) е броят на редовете. Повече за списъците има в следващия модул, „Колекции“.

По-точно, за любопитните
  • "a" създава файла, ако го няма, затова първото добавяне не дава грешка.
  • read() чете целия файл в паметта. За файл от няколко гигабайта е по-добре да четеш ред по ред. Виж картата за for line in file.
  • В чужд код ще видиш и file.readlines(). Той също дава списък с редовете, но всеки е със своя "\n" в края.

Пробвай

Две смени

Какво ще отпечата програмата?

try:
with open("shift.txt", "w", encoding="utf-8") as file:
file.write("Първа смяна: Иво\n")
with open("shift.txt", "w", encoding="utf-8") as file:
file.write("Втора смяна: Мила\n")
with open("shift.txt", encoding="utf-8") as file:
print(file.read(), end="")
except OSError as e:
print(e)
Отговор

Идеята

Когато файла го няма

Настройките на програмата са във config.txt, а някой го е изтрил. Ако никой не хване изключението:

app.py
def read_config():
with open("config.txt", encoding="utf-8") as file:
return file.read()
print(read_config())
Грешка
Traceback (most recent call last):
File "/app/app.py", line 6, in <module>
print(read_config())
^^^^^^^^^^^^^
File "/app/app.py", line 2, in read_config
with open("config.txt", encoding="utf-8") as file:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'config.txt'

Python подрежда извикванията обратно на Java: най-горе е най-външното, ред 6, където е извикана read_config(). Под него е ред 2 в read_config, където open не намира файла. Най-отдолу е самата грешка: FileNotFoundError и съобщението. Затова в началото пише „most recent call last“: последното извикване е най-отдолу.

FileNotFoundError е наследник на OSError. Хвани го отделно, преди общия случай:

import os
def read_config():
with open("config.txt", encoding="utf-8") as file:
return file.read()
try:
print(read_config())
except FileNotFoundError as e:
print(f"Няма файл {e.filename}. Ползвам настройките по подразбиране.")
except OSError as e:
print(f"Не мога да прочета настройките: {e}")
print(os.path.exists("config.txt"))
Изход
Няма файл config.txt. Ползвам настройките по подразбиране.
False

e.filename е името на файла, който липсва. os.path.exists(path) проверява дали файлът съществува. Проверката не замества except: файлът може да изчезне между проверката и четенето, а и има други причини четенето да не стане.

Идеята

Ред по ред и with

Файлът от open може да се пише на части и да се чете ред по ред, без целият да е в паметта. Той държи файла отворен, докато работиш с него, и затова трябва да се затвори накрая:

try:
with open("report.txt", "w", encoding="utf-8") as out:
for day in range(1, 4):
out.write(f"Ден {day}: {day * 120} €\n")
except OSError as e:
print(f"Не мога да запиша: {e}")
try:
with open("report.txt", encoding="utf-8") as file:
for line in file:
print(line.rstrip("\n"))
except OSError as e:
print(f"Не мога да прочета: {e}")
Изход
Ден 1: 120 €
Ден 2: 240 €
Ден 3: 360 €
  • write записва текст. Нов ред сам не идва: \n го пишеш ти.
  • for line in file дава редовете един по един, всеки със своя \n в края. rstrip("\n") го маха, иначе print би сложил празен ред между тях.
  • with затваря файла, когато отместените редове свършат, по какъвто и да е начин: и при изключение.

Затварянето не е формалност: докато файлът е отворен, текстът може още да е в буфера му в паметта, а не на диска. За да е кодът кратък, тук няма try:

import os
with open("report.txt", "w", encoding="utf-8") as out:
for day in range(1, 4):
out.write(f"Ден {day}: {day * 120} €\n")
print(f"В блока: {os.path.getsize('report.txt')} байта")
print(f"След блока: {os.path.getsize('report.txt')} байта")
with open("report.txt", encoding="utf-8") as file:
for line in file:
print(line.rstrip("\n"))
Стъпка по стъпка, с думи
  1. os има функцията за размера на файла. Отляво е паметта на програмата, отдясно е дискът.
  2. open с "w" създава празния файл report.txt и файлов обект с буфер в паметта.
  3. write пише в буфера. До диска още не стига нищо.
  4. Файлът на диска все още е 0 байта.
  5. Блокът with свършва и затваря файла. Буферът отива на диска: файлът става 54 байта.
  6. 54 байта за 39 знака с новите редове: в UTF-8 всяка буква на кирилица е 2 байта, а € е 3.
  7. for line in file чете трите реда обратно от диска.

Обещахме го в урока за finally. Ето същото записване без with, с finally:

out = open("report.txt", "w", encoding="utf-8")
try:
out.write("Ден 1: 120 €\n")
finally:
out.close()

with прави точно това вместо теб: затваря файла, каквото и да стане в блока.

По-точно, за любопитните
  • В едно with може да има няколко файла, разделени със запетая: with open("app.log", encoding="utf-8") as src, open("errors.log", "w", encoding="utf-8") as out:.
  • Буфер. Файлът събира текста в паметта и го праща към диска на големи парчета. Всяко отделно писане на диска е бавно, а така са малко. file.flush() праща буфера веднага, без да затваря файла.
  • Байтове и знаци. Отдолу файлът е поредица от байтове. Режимите "rb" и "wb" четат и пишат байтове: за снимки, архиви, всичко, което не е текст. Без b файлът е текстов и превръща байтовете в знаци по encoding.
  • file.readline() чете един ред. Когато файлът свърши, връща празен текст "". Празен ред във файла е "\n", затова двете не се бъркат.

Пробвай

Кога се затваря?

Всеки клас с методите __enter__ и __exit__ може да стои във with. __enter__ се вика в началото на блока, а __exit__ в края му, и при изключение. Ето принтерът на касата от урока за finally, като ресурс:

Какво ще отпечата програмата?

class Printer:
def __enter__(self):
print("Принтерът е зает.")
return self
def __exit__(self, exc_type, exc, tb):
print("Принтерът е свободен.")
try:
with Printer():
print("Печатам бележката...")
qty = int("три")
print(f"Количество: {qty}")
except ValueError:
print("Грешно количество.")
Отговор

Пробвай

Намери грешката

Програмата записва поръчка и брои редовете във файла. Отпечатва 0. На кой ред е грешката?

Избери реда с грешката.

Идеята

Папки

Резервните копия на касата отиват в папка, по едно на ден:

import os
backup = "backup"
try:
os.makedirs(os.path.join(backup, "old"), exist_ok=True)
with open(os.path.join(backup, "2026-10-01.csv"), "w", encoding="utf-8") as file:
file.write("Кафе,2.40\n")
with open(os.path.join(backup, "2026-10-02.csv"), "w", encoding="utf-8") as file:
file.write("Кафе,2.40\nЧай,1.80\n")
for name in sorted(os.listdir(backup)):
p = os.path.join(backup, name)
if os.path.isdir(p):
print(f"{name}/")
else:
print(f"{name} ({os.path.getsize(p)} байта)")
os.remove(os.path.join(backup, "2026-10-01.csv"))
print(os.path.exists(os.path.join(backup, "2026-10-01.csv")))
except OSError as e:
print(e)
Изход
2026-10-01.csv (14 байта)
2026-10-02.csv (26 байта)
old/
False
Функция Какво прави
os.makedirs(p, exist_ok=True) създава папката и всички папки над нея, ако ги няма
os.path.join(dir, "име") пътят до име вътре в dir
os.path.isdir(p), os.path.exists(p) папка ли е, има ли го
os.path.getsize(p) размерът в байтове
os.remove(p) изтрива файл (os.rmdir(p) изтрива празна папка)
os.listdir(dir) списък с имената на всичко в папката

Размерите са 14 и 26 байта, макар че символите са по-малко: всяка буква на кирилица е 2 байта в UTF-8.

os.listdir дава имената в произволен ред, затова ги подреждаме със sorted.
По-точно, за любопитните
  • os.rmdir изтрива само празна папка. Иначе дава OSError: [Errno 39] Directory not empty. shutil.rmtree(p) (модулът shutil) изтрива папката с всичко в нея, затова внимавай с него.
  • shutil.copy(от, до) копира файл, а os.rename(от, до) го мести или преименува.
  • В много код ще видиш модула pathlib: Path("backup") / "old" е пътят до папката old в backup, а p.exists() и p.read_text(encoding="utf-8") са методи на пътя. Това е същото, с обекти вместо функции.
  • Пътят "notes.txt" е относителен: спрямо папката, от която е пусната програмата. "/home/ana/notes.txt" е абсолютен. os.path.abspath("notes.txt") показва къде точно е файлът.

Упражнение

Програми, които помнят

Реши задачите в редактора си. Във всяка задача програмата първо сама създава входния файл с open(..., "w"), за да можеш да я пробваш веднага.

1. Конфигурационен файл. Създай server.conf с това съдържание, после го прочети и отпечатай всяка настройка като ключ: стойност. Празните редове и редовете, които започват с #, се пропускат. Подсказка: split("=", 1) разделя само на първото = (числото е колко пъти да среже).

server.conf
# настройки на сървъра
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Изход
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. Филтър на лог файл. Създай app.log с четири реда (два INFO и два ERROR). Прочети от клавиатурата ниво, например ERROR, и запиши редовете с това ниво в errors.log ред по ред, с двата файла в едно with. Отпечатай errors.log и колко реда си записал.

Вход
ERROR
Изход
ERROR: Failed to load configuration
ERROR: Null pointer exception
Записах 2 реда в errors.log

3. Съдържание на папка. Създай папка project с файловете document.txt, image.jpg, script.js и папката backup. Запиши имената им, подредени по азбучен ред, в directory_contents.txt, като след имената на папките сложиш /. Отпечатай файла.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. Индексен файл. Създай text.txt с трите реда по-долу. Прочети дума от клавиатурата и запиши в index.txt всяка нейна поява във вида ред:позиция: номерът на реда (от 1) и индексът в реда (от 0). Ако думата е два пъти на един ред, това са два записа.

text.txt
hello world
world hello
hello again, hello
Вход
hello
index.txt
1:0
2:6
3:0
3:13

Още задачи

  • Брояч на пусканията. Поправи програмата от първата карта: брой пусканията във visits.txt. Ако файлът го няма, започни от 0.
  • Намери и замени във файл. Задачата от урока „Търсене и замяна“: прочети файл, замени „старо“ с „ново“ и запиши резултата в нов файл.
  • Дати от файл. Задачата за дати от урока „Регулярни изрази“: чети датите от файл, по една на ред.
  • Копие с номерата на редовете. Прочети файл и запиши копие, в което всеки ред започва с номера си: 1: ..., 2: ....

Тест

Провери се

1. open("config.txt", encoding="utf-8") не намира файла, а около него няма try. Какво става?
2. Отваряш файл с "w" и записваш "А\n". После пак с "w" записваш "Б\n". Какво има във файла?
3. Кога with затваря файла?
4. Какво връща file.readline(), когато файлът свърши?
5. Отваряш файл с open() без with и не го затваряш. Какво може да има във файла, докато програмата още работи?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·