Работа с файлове в Python#

План на лекцията:

  • Кратка интродукция за работата с пътища, Windows и Unix

  • Представяне на файловете в Python

  • Четене на файлове

  • Писане на файлове

  • Работа с файлове и пътища

  • Използване на with

  • tell и seek

  • Примери

  • Задачи

Предварителна подготовка#

!mkdir files
!curl -o files/lorem_ipsum.txt https://raw.githubusercontent.com/lyubolp/PythonCourse2022/08_files/08%20-%20Files/files/1.txt
!echo "hello" > files/hello.txt

Кратка интродукция за работата с пътища, Windows и Unix.#

Път в една файлова система посочва локацията и името на даден обект (бил той файл или директория). Пример за път в Linux/MacOS е /home/user/myfile.txt, а в Windows - C:\Users\user\myfile.txt.

Забелязва се, че пътищата в Windows и Unix-базираните ОС се разделят с различни черти. Нашият Python код трябва да е съвместим и с двата начина за разделя на пътища.

За наше улеснение, Python предлага функцията os.path.join, която по подадени имена на директории/файлове, конструира правилния спрямо нашия OS път.

import os

os.path.join('/home', 'lyubo', 'myfile.txt')

Понеже кодът е изпълнен под Linux, получаваме Unix-ски път. Ако изпълним обаче същия код под Windows, ще получим правилен Windows-ки път.

Представяне на файловете в Python#

Python предоставя API за работа с файлове и потоци. Python работи с т.нар. “file objects” - това може да са файлове на диска, sockets, pipes и други потоци.

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

Можем да отворим един файл за работа, с помощта на функцията open. В най-простия си вид, тя приема път към файл.

fd = open(os.path.join('files', 'lorem_ipsum.txt'))

print(fd)

fd.close()

След като приключим работа с един файл, не трябва да забравяме да го затворим. Затварянето на един файл става с помощта на close метода.

Забелязваме, че обекта който получаваме като резултат от open има име (files/lorem_ipsum.txt), режим (r) и кодиране (UTF-8).

Един файл може да бъде отворен в няколко различни режима:

  • r - отваря файла за четене

  • w - отваря файла за писане, като файла първо бива зачистен

  • a - отваря файла за писане, като новото съдържание се записва в края на файла

  • x - създава файла, ако не съществува. Ако файла вече съществува, се хвърля FileExistsError

  • b - отваря файл в бинарен режим

  • t - отваря файл в текстови режим

  • + - отваря файла за четене и писане

Освен режима на отваряне, можем да променим и кодирането, с което се опитваме да четем файла. По подразбиране, използваме UTF-8.

Ако се опитаме да запишем файл в несъществуваща директория, ще получим грешка

fd = open(os.path.join('files', 'non_existing_dir', 'new_file.txt'), 'w')

fd.write('content')

fd.close()

Четене на файлове#

В Python имаме три метода, чрез които можем да четем от файлове: read, readline и readlines.

Нека първо разгледаме метода read. Той прочита целия файл и запазва съдържанието му в променлива, като един голям низ.

import os

fd = open(os.path.join('files', 'lorem_ipsum.txt'))

content = fd.read()
print(f'content={content}')

fd.close()

Веднъж прочетен един файл, следващото прочитане ще ни върне празен низ.

fd = open(os.path.join('files', 'lorem_ipsum.txt'))

content = fd.read()
print(f'content={content}')

content = fd.read()
print(f'content={content}')

fd.close()

Друг вариант за четене на файл, е ред по ред - това става с помощта на метода readline.

import os
fd = open(os.path.join('files', 'lorem_ipsum.txt'))

content = fd.readline()
print(f'content={content}')

content = fd.readline()
print(f'content={content}')

fd.close()

След прочитане на последния ред, readline връща празен низ.

import os
fd = open(os.path.join('files', 'lorem_ipsum.txt'))

content = fd.readline()
print(f'content={content}')

while content != '':
    content = fd.readline()
    print(f'content={content}')

fd.close()

Ако искаме да получим списък от всички редове във файл, можем да използваме readlines.

import os
fd = open(os.path.join('files', 'lorem_ipsum.txt'))

content = fd.readlines()
print(f'content={content}')

fd.close()
import os
fd = open(os.path.join('files', 'lorem_ipsum.txt'))

content = fd.readlines()

for line in content:
    print(f'content={line}')

fd.close()

Вместо да използваме readlines, можем да итерираме директно по файл обекта.

import os
fd = open(os.path.join('files', 'lorem_ipsum.txt'))

for line in fd:
    print(f'content={line}')

fd.close()

Писане на файлове#

За писане на файлове в Python може да използваме методите write и writelines

Методът write записва низ във файла. Позицията зависи от начина по който е отворен файла (с или без изтриване на текущото съдържание).

import os
fd = open(os.path.join('files', 'hello_world.txt'), 'w')

fd.write('Hello world')

fd.close()
import os
fd = open(os.path.join('files', 'hello_world.txt'))

content = fd.read()
print(content)
fd.close()

Припомням, че ако отворим файла в режим w, то ще изтрием текущото му съдържание.

import os

fd = open(os.path.join('files', 'my_important_file.txt'), 'w')
fd.write('Really important')
fd.close()

# Oops, forgot to write something down

fd = open(os.path.join('files', 'my_important_file.txt'), 'w')
fd.write('Should really not forget this')
fd.close()
import os
fd = open(os.path.join('files', 'my_important_file.txt'))

content = fd.read()
print(content)
fd.close()

Ако искаме да пишем в края на файла, трябва да използваме режим a

import os

fd = open(os.path.join('files', 'my_important_file_2.txt'), 'a')
fd.write('Really important\n')
fd.close()

# Oops, forgot to write something down

fd = open(os.path.join('files', 'my_important_file_2.txt'), 'a')
fd.write('Should really not forget this\n')
fd.close()
import os
fd = open(os.path.join('files', 'my_important_file_2.txt'))

content = fd.read()
print(content)
fd.close()

Методът writelines приема списък от “редове”, които да бъдат записани във файла.

Забележка: writelines не добавя автоматично нови редове след всеки елемент, затова се очаква всеки елемент от списъка да съдържа нов ред в себе си.

import os

fd = open(os.path.join('files', 'writelines_example.txt'), 'w')

lines_to_write = ['hello\n', 'this\n', 'are\n', 'my\n', 'lines\n']
fd.writelines(lines_to_write)

fd.close()
import os
fd = open(os.path.join('files', 'writelines_example.txt'))

content = fd.read()
print(content)
fd.close()

Тук е добре да се отбележи, че можем да записваме и други типове данни, освен низове (е не точно, но…).

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

import os

other_types_in_files = os.path.join('files', 'other_types_in_files.txt')

fd = open(other_types_in_files, 'w')

fd.write(str(2) + '\n')
fd.write(str([2, 3, 4]) + '\n')
fd.write(str({'a': 2, 'b': 3, 'c': 4}) + '\n')
fd.write(str((2, 3)) + '\n')

fd.close()

fd = open(other_types_in_files)
content = fd.read()
print(content)
fd.close()

Работа с файлове и пътища#

Python предлага удобен начин за работа с файлове. Вградената библиотека os съдържа всичко необходимо за работата с файлове и директории.

Ще разгледаме как можем да:

  • Прегледаме съдържанието на директория

  • Преместим файл

  • Изтрием файл

  • Създадем директория

  • Преместим директория

  • Изтрием директория

  • Обща работа с пътища

  • Обхождане на директории

Преглеждане на съдържание на директория#

Можем да видим съдържанието на директория като използваме os.listdir метода. Той ни връща списък от низове, съдържащи имената на директориите и файловете в исканата от нас папка.

import os

print(os.listdir('files'))

Преместване на файл#

Преместването на файл става чрез “преименуването му” (или всъщност, преименуването на файл е преместването му като файл с друго име 🤔) - това в Python става с помощта на функцията os.rename. Тя приема два аргумента - source път и destination път (т.е. старото и новото име на файла)

import os

file_to_be_moved = os.path.join('files', 'to_be_moved.txt')

fp = open(file_to_be_moved, 'w')
fp.write('This file is to be moved')
fp.close()

print(f'Before = {os.listdir("files")}')

file_moved = os.path.join('files', 'file_moved.txt')

os.rename(file_to_be_moved, file_moved)

print(f'After = {os.listdir("files")}')

Ако вече съществува файл със същото име се случват едно от две неща:

  • Ако кодът се изпълнява под Windows, се хвърля FileExistsError.

  • Ако кодът се изпълнява под Linux/MacOS и имаме права върху файла върху който ще пишем, той ще бъде презаписан

Нека разгледаме следната ситуация: имаме файл files/a/file.txt, който искаме да преместим в files/b, но директорията b не съществува.

!mkdir files/a
import os

file_to_be_moved = os.path.join('files', 'a', 'file.txt')

fp = open(file_to_be_moved, 'w')
fp.write('This file is to be moved')
fp.close()

os.rename(file_to_be_moved, os.path.join('files', 'b', 'file.txt'))

Изтриване на файл#

Изтриването на файл се случва чрез метода os.remove. Той приема един аргумент - пътят към файла, който ще бъде изтрит.

import os

file_path = os.path.join('files', 'to_removed.txt')
fp = open(file_path, 'w')
fp.write('This file will be deleted')
fp.close()

print(f'Before = {os.listdir("files")}')

os.remove(file_path)

print(f'After = {os.listdir("files")}')

Ако се опитаме да изтрием директория с os.remove, ще получим грешка IsADirectoryError.

import os

os.remove('files')

А ако опитаме да изтрием файл, който не съществува, ще получим грешка FileNotFoundError.

import os

non_existant_path = os.path.join('files', 'this_file_does_not_exist.txt')

os.remove(non_existant_path)

Копиране на файлове#

В Python можем да копираме файлове с помощта на shutil библиотеката. Тя ни предоставя методи за копиране на файлове.

Ще разгледаме част от тях:

  • shutil.copy() - копира файл от едно място на друго. Приема два аргумента - source и target пътища. Ако файлът вече съществува на новото място, ще бъде презаписан. Ако target е директория, ще се запише копие на файла в тази директория със същото име.

  • shutil.copy2() - работи по същия начин като shutil.copy(), но запазва и метаданните на файла (например времето на създаване)

  • shutil.copystat() - копира само метаданните на файла

  • shutil.copytree() - копира директория и всички файлове в нея. Приема два аргумента - source и target пътища.

import os
import shutil

print(f'Before = {os.listdir("files")}')

shutil.copy(os.path.join('files', '1.txt'), os.path.join('files', '1_1.txt'))
shutil.copy2(os.path.join('files', '1.txt'), os.path.join('files', '1_2.txt'))

print(f'After = {os.listdir("files")}')
Before = ['1.txt']
After = ['1_2.txt', '1.txt', '1_1.txt']

Създаване на директория#

В Python създаването на директория става чрез метода os.mkdir. Той приема пътя към директорията, която да бъде създадена.

import os

print(f'Before = {os.listdir("files")}')

file_path = os.path.join('files', 'mkdir_example')
os.mkdir(file_path)

print(f'After = {os.listdir("files")}')

Само за информация: Можем да зададем права на директорията, с помощта на аргумента mode.

Ако се опитаме да създадем директория, която вече съществува, ще получим FileExistsError.

import os

file_path = os.path.join('files', 'mkdir_existing_directory')
os.mkdir(file_path)

os.mkdir(file_path)

Нека опитаме да създадем няколко нови директории, една под друга.

import os

print(f'Before = {os.listdir("files")}')

file_path = os.path.join('files', 'mkdir_example_parent', 'mkdir_example_child')
os.mkdir(file_path)

print(f'After = {os.listdir("files")}')

Тук получаваме грешка - os.mkdir не може да създаде несъществуващите директории над последната. За целта трябва да използваме метода os.makedirs. Той приема отново като аргумент пътят към директорията, която искаме да създадем, както и права на директорията/директориите.

import os

print(f'Before = {os.listdir("files")}')

file_path = os.path.join('files', 'mkdir_example_parent', 'mkdir_example_child')
os.makedirs(file_path)

print(f'After = {os.listdir("files")}')

Един допълнителен аргумент, който makedirs приема, е аргумента exist_ok. Той контролира дали да се хвърли грешка, ако крайната директория която искаме да създадем, вече съществува.

Преместване на директория#

Освен за преместване на файлове, os.rename работи и за директории.

import os

file_path = os.path.join('files', 'rename_dir_example_1')
os.makedirs(file_path)

print(f'Before = {os.listdir("files")}')

target_file_path = os.path.join('files', 'renamed_dir_example_1')
os.rename(file_path, target_file_path)
print(f'After = {os.listdir("files")}')

Както споменахме по-горе, ако дестинацията съществува, под Windows os.rename хвърля FileExistsError.

Под Unix системи (Linux, MacOS) поведението е малко по-различно:

  • Ако source пътя е файл, а destination пътя е директория, се хвърля IsADirectory грешка

  • Ако source пътя е директория, а destination пътя е файл, се хвърля NotADirectory грешка

  • Ако destination е съществуваща директория:

    • Ако е празна, преместването е успешно

    • Ако не е празна, се хвърля OSError грешка

Изтриване на директория#

Изтриването на директория става чрез функцията os.rmdir. Тя приема пътя към директорията, която трябва да бъде изтрита.

import os

file_path = os.path.join('files', 'remove_dir_example_1')
os.makedirs(file_path)

print(f'Before = {os.listdir("files")}')

os.rmdir(file_path)
print(f'After = {os.listdir("files")}')

Ако директорията, която се опитаме да изтрием, не е празна, ще получим OSError. А ако тя не съществува, ще получим FileNotFoundError.

import os

non_existing_dir = os.path.join('files', 'non_existing_dir')

os.rmdir(non_existing_dir)
os.rmdir('files')
import os

os.rmdir('files')

Освен os.rmdir, Python предлага и друга функция за изтриване на директории - os.removedirs. Тя обаче работи по специфичен начин. os.removedirs приема път до директория, като освен да премахва последната директория от пътя, функцията се опитва да премахне и всички празни директории нагоре, като спира при първата директория, която не е успяла да премахне успешно.

Нека за целта създадем следната структура - files/remove_dirs_example ще е основната ни директория. В нея ще имаме две поддиректории - first и second. В first ще създадем още няколко поддиректории, като всичките ще са празни. В second ще създадем един файл и една поддиректория.

Ще се опитаме да извикаме os.removedirs върху най-долната директории в first и second

import os

main_dir = os.path.join('files', 'remove_dirs_example')

first_dir = os.path.join(main_dir, 'first')
empty_first_subdirs = os.path.join(first_dir, 'a', 'b', 'c')

second_dir = os.path.join(main_dir, 'second')
empty_second_subdir = os.path.join(second_dir, 'd')
empty_second_dir_file = os.path.join(second_dir, 'e')

os.makedirs(empty_first_subdirs)
os.makedirs(empty_second_subdir)


fd = open(empty_second_dir_file, 'w')
fd.write('Hi')
fd.close()

print(f'Before = {os.listdir(main_dir)}')

os.removedirs(empty_first_subdirs)
os.removedirs(empty_second_subdir)

print(f'After = {os.listdir(main_dir)}')

Как работи os.removedirs в този случай ?

Върху files/first/a/b/c:

  1. Опитваме да изтрием c => тя е празна, изтрива се

  2. Опитваме да изтрием b => тя е празна, изтрива се

  3. Опитваме да изтрием a => тя е празна, изтрива се

  4. Опитваме да изтрием first => тя е празна, изтрива се

  5. Опитваме да изтрием remove_dirs_example => тя не е празна, спираме

Върху files/second/d:

  1. Опитваме да изтрием d => тя е празна, изтрива се

  2. Опитваме да изтрием second => тя не е празна, спираме

os.removedirs е много подходяща когато имаме много празни директории една в друга, но е редно да се използва внимателно.

За любопитните: Съществува функция rmtree, намираща се в shutil библиотеката, която изтрива папка и всички в нея. Нея няма да я разглеждаме в курса.

Обща работа с пътища#

В началото видяхме, че за да построим един път до файл или директория в Python, трябва да използваме os.path.join. Освен join, os.path предлага много други полезни функции за работа с пътища.

Ще се спрем върху една част от тях, която е по-вероятно да използвате във всекидневната си работа с Python:

  • os.path.exists - проверка дали пътят сочи към валиден файл или директория

  • os.path.isdir - дали пътят сочи към валидна директория

  • os.path.isfile - дали пътят сочи към валиден файл

  • os.path.split - отделя последната част от път

import os
print(f'Does the files directory exist: {os.path.exists("files")}')
print(f'Does the files/lorem_ipsum.txt file exist: {os.path.exists(os.path.join("files", "lorem_ipsum.txt"))}')
print(f'Does the files/ala-bala.txt file exist: {os.path.exists(os.path.join("files", "ala-bala.txt"))}')
import os
print(f'Is "files" a directory: {os.path.isdir("files")}')
print(f'Is "files/lorem_ipsum.txt" a directory: {os.path.isdir(os.path.join("files", "lorem_ipsum.txt"))}')
print(f'Is "files/ala-bala.txt" a directory: {os.path.isdir(os.path.join("files", "ala-bala.txt"))}')  # Although the file does not exist, isdir returns False
import os
print(f'Is "files" a file: {os.path.isfile("files")}')
print(f'Is "files/lorem_ipsum.txt" a file: {os.path.isfile(os.path.join("files", "lorem_ipsum.txt"))}')
print(f'Is "files/ala-bala.txt" a directory: {os.path.isfile(os.path.join("files", "ala-bala.txt"))}')  # Although the file does not exist, isfile returns False

os.path.split отделя последната част от пътя от останалия. Връща ни наредена двойка от head и tail, където tail съдържа последната част от пътя, а head останалото

import os

long_path = os.path.join('/', 'foo', 'bar', 'baz')
print(f'long_path = {long_path}')

print(f'long_path splitted: {os.path.split(long_path)}')

Важно е да отбележим няколко по-специални случая:

  • Ако пътят е празен, split ще ни върне празни head и tail

  • Ако пътят завършва с / (или \ под Windows), tail ще е празен низ

  • Ако пътят не съдържа /(или \ под Windows), head ще е празен низ

empty_path = ''
print(f'split with empty path returns = {os.path.split(empty_path)}')

ending_with_separator = '/foo/bar/'
print(f'split with path ending in / returns = {os.path.split(ending_with_separator)}')

no_separators = 'foo'
print(f'split with no separators returns = {os.path.split(no_separators)}')

Обхождане на директории#

Понякога искаме да обходим цялото директорийно дърво отдадена точка надолу. Python ни позволява да направи това сравнително лесно, с помощта на функцията os.walk.

Тя приема като аргумент директорията, от което започва обхождането. Като резултат, тя ни връща генератор съдържащ името на текущата директория, имената на директориите в текущата директория и имената на файловете в текущата директория.

import os

# Setup
example_root_dir = os.path.join('files', 'walk_example')

d_dir = os.path.join(example_root_dir, 'a', 'b', 'c', 'd')
os.makedirs(d_dir, exist_ok=True)

os.makedirs(os.path.join(example_root_dir, 'a', 'b1'), exist_ok=True)

b2_dir = os.path.join(example_root_dir, 'a', 'b2')
os.makedirs(b2_dir, exist_ok=True)

os.makedirs(os.path.join(example_root_dir, 'a', 'b', 'c', 'd1'), exist_ok=True)
os.makedirs(os.path.join(example_root_dir, 'a', 'b', 'c', 'd2'), exist_ok=True)

files_for_d = [os.path.join(d_dir, f'file{i}') for i in range(5)]

for file_for_d in files_for_d:
    with open(file_for_d, 'w') as fp:
        fp.write(f'Content for {file_for_d}')

files_for_b2 = [os.path.join(d_dir, f'file{i}') for i in range(3)]

for file_for_b2 in files_for_b2:
    with open(file_for_b2, 'w') as fp:
        fp.write(f'Content for {file_for_b2}')


# os.walk

for dirname, subdirs, files in os.walk(example_root_dir):
    print(f'In {dirname}, which has subdirs: {subdirs} and files: {files}')

Използване на with#

Досега в работата ни с файлове, забелязахме че трябва да подсигурим, че сме затворили файла. Проблемът идва, когато трябва да се справим с грешки, които могат да бъда хвърлени от различните операции с файлове. Дори и при хвърлена грешка, ние все пак трябва да си затворим файла.

Python ни позволява два начина да се справим с този проблем - try/finally и with.

След всеки try блок, може да поставим един друг блок, който да се изпълнява винаги след try или except блоковете. Този допълнителен блок се казва finally.

def raiser():
    raise ValueError('Hello there')

try:
    raiser()
finally:
    print('After the exception, this will be printed')

По подобен начин можем да използваме finally за да затворим отворения файл.

import os
fp = open(os.path.join('files', 'lorem_ipsum.txt'))
try:
    print(fp.read())
finally:
    fp.close()

Така, дори и да получим грешка при четене (или каквато и да е друга операция в try блока), файла ще бъде затворен.

От тук можем да стигнем до заключението - при работата с ресурси, имаме процес по отваряне и процес по затваряне (независимо от грешки). Python ни предлага и друга синтактична конструкция за работа с такива структури - with.

with ни позволява отварянето на нов контекст. В него може да отворим ресурс, а след излизането от блока, този ресурс се затваря автоматично.

Общият вид на with е следния:

with <expression> as <variable>:
    f(variable)

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

import os
with open(os.path.join('files', 'lorem_ipsum.txt')) as fp:
    print(fp.read())

print(f'Is the file closed ? {fp.closed}')

Дори и при грешка, файлът ще бъде затворен. Ако искаме обаче да хванем грешката, все пак трябва да изпозлваме try/catch.

import os

try:
    with open(os.path.join('files', 'lorem_ipsum.txt')) as fp:
        print(fp.read())
except OSError as err:
    print(err)

Начинът по който with работи е, че извиква два специални магически метода - __enter__ и __exit__, който се изпълняват при влизане и излизане съответно от контекст мениджъра.

По-конкретно, __enter__ метода се изивиква в as частта на with. В него връщаме обекта, който ще бъде присвоен на променливата след as.

Файловите обекти имплементират __exit__ метода, където затварят отворения файл.

Ако искаме и нашите класове да работят с контекстните мениджъри, трябва да имплементираме __enter__ и __exit__ методите.

class ContextableClass:
    def __init__(self, some_var=42):
        self.some_var = some_var
    
    def __enter__(self):
        print('Entering the context manager')
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        print('Exiting the context manager')
with ContextableClass(5) as instance:
    print(instance.some_var)

tell и seek#

Писането и четенето от файлове всъщност се осъществява символ по символ. Така например за да запишем ‘hello’, трябва първо да запишем h, после e и т.н.

Както при писането с клавиатура имаме позиция върху която пишем, така и при работата с файлове имаме позиция, на която четем или пишем.

Python ни позволява да вземем текущата позиция за четене/писане, както и да преместим тази позиция на друго място.

Можем да вземем позиция на курсора във файла с помощта на метода tell. Той ще ни върне като резултат цяло число, символизиращо броя символи след началото на файла.

import os

file_path = os.path.join('files', 'tell_exampe.txt')

# First lets create the file
with open(file_path, 'w') as fp:
    fp.write('abc\n')
    fp.write('def\n')
    fp.write('ghi\n')

# Now lets read 
with open(file_path) as fp:
    print(f'Current position = {fp.tell()}')
    print(f'Reading a line... {fp.readline()}')

    print(f'Current position = {fp.tell()}')
    print(f'Reading another line... {fp.readline()}')

    print(f'Current position = {fp.tell()}')

Можем да зададем позиция на курсора във файла с помощта на метода seek. Той приема два аргумента - какво отместване (offset) да направим и от къде (whence).

whence приема три стойност:

  • 0 (или os.SEEK_SET) - означава спрямо началото на файла

  • 1 (или os.SEEK_CUR) - означава спрямо текущата позиция

  • 2 (или os.SEEK_END) - означава спрямо края на файла

При работа с текстови файлове, отместването е само спрямо началото на файла. Можем единствено да преместим курсора до края на файла с seek(0, 2)

import os

file_path = os.path.join('files', 'seek_exampe.txt')

# First lets create the file
with open(file_path, 'w') as fp:
    fp.write('abcdefghijk')

with open(file_path, 'r+') as fp:
    fp.seek(2)
    fp.write('!')

    fp.seek(0, 2)
    fp.write('@')

with open(file_path) as fp:
    print(fp.read())

Бележка: Ако бяхте отворили файла за писане с режим a, щяхме да можем да пишем само в края. Режим r+ ни дава права за четене и пренаписване.

Примери#

Пример 1#

Напишете функция split_path, която приема път (като низ), и го разделя на съставните му части. Използвайте os.path.split.

Решение на пример 1#

import os
from typing import List

def split_path(file_path: str) -> List[str]:
    head, tail = os.path.split(file_path)

    result = [tail]
    while head != '' and head != file_path:
        file_path = head
        head, tail = os.path.split(file_path)
        result.append(tail)

    result.reverse()
    return result
print(split_path('/foo/bar/baz'))
print(split_path('/foo/bar/baz/'))
print(split_path('/foo'))
print(split_path('foo'))

Пример 2#

Даден е клас Person, който съдържа информация за човек - неговите имена, рожденна дата, възраст и работа.

class Person:
    def __init__(self, first_name: str='', last_name: str='', birthdate: str='', age: int=0, job: str=''):
        self.__first_name = first_name
        self.__last_name = last_name
        self.__birthdate = birthdate
        self.__age = age
        self.__job = job

    @property
    def first_name(self) -> str:
        return self.__first_name
    
    @property
    def last_name(self) -> str:
        return self.__last_name
    
    @property
    def birthdate(self) -> str:
        return self.__birthdate

    @property
    def age(self) -> str:
        return self.__age

    @property
    def job(self) -> str:
        return self.__job

Напишете клас PersonSerializer, съдържащ следните методи:

  • Метод, който приема обект от тип Person и път. Методът записва във файл информацията за човека

  • Метод, който приема път и връща обект от тип Person, който е създаден на база данните от файла

  • Метод, който приема списък от Person и път. Методът записва във файл информация за хората

  • Метод, който приема път и връща списък от Person, които са създадени на база на информацията във файла

При грешка при писането или четенето класът да хвърля ValueError.

Може да приемем, че информацията за един човек ще бъде записана на един ред.

Решение на пример 2#

from typing import List

class Person:
    def __init__(self, first_name: str='', last_name: str='', birthdate: str='', age: int=0, job: str=''):
        self.__first_name = first_name
        self.__last_name = last_name
        self.__birthdate = birthdate
        self.__age = age
        self.__job = job

    @property
    def first_name(self) -> str:
        return self.__first_name
    
    @property
    def last_name(self) -> str:
        return self.__last_name
    
    @property
    def birthdate(self) -> str:
        return self.__birthdate

    @property
    def age(self) -> str:
        return self.__age

    @property
    def job(self) -> str:
        return self.__job

    # Adding a __str__ magic method helps us with printing the info
    def __str__(self) -> str:
        return f'{self.first_name}, {self.last_name}, {self.birthdate}, {self.age}, {self.job}'
    
    @staticmethod
    def from_string(person_info: str):
        first_name, last_name, birthdate, age, job = person_info.strip().split(', ')
        return Person(first_name, last_name, birthdate, age, job)
    
class PersonSerializer:
    @staticmethod
    def save_person(person: Person, filepath: str, mode: str='w'):
        with open(filepath, mode) as fp:
            fp.write(str(person) + '\n')
    
    @staticmethod
    def create_person_from_file(filepath: str) -> Person:
        if not os.path.exists(filepath):
            raise ValueError(f'File not found at {filepath}')
        
        with open(filepath) as fp:
            person_info = fp.read()
            return Person.from_string(person_info)

    @staticmethod
    def save_people(people: List[Person], filepath: str):
        for person in people:
            PersonSerializer.save_person(person, filepath, mode='a')
    
    @staticmethod
    def create_people_from_file(filepath: str) -> List[Person]:
        if not os.path.exists(filepath):
            raise ValueError(f'File not found at {filepath}')
        
        with open(filepath) as fp:
            return [Person.from_string(person_info) for person_info in fp]
me = Person('Lyubo', 'Karev', '20-09-1998', 24, 'Developer')
my_filepath = os.path.join('files', 'lyubo.txt')

PersonSerializer.save_person(me, my_filepath)
me_again = PersonSerializer.create_person_from_file(my_filepath)  # Hello me, meet the real me

print(me)
print(me_again)
bunch_of_people = [
    Person('Lyubo', 'Karev', '20-09-1998', 24, 'Developer'),
    Person('Alex', 'Ignatov', '22-10-1998', 24, 'Developer'),
    Person('Ivan', 'Luchev', '28-04-1998', 24, 'Student')
]

our_filepath = os.path.join('files', 'us.txt')

PersonSerializer.save_people(bunch_of_people, our_filepath)
us_again = PersonSerializer.create_people_from_file(our_filepath)

print('Bunch of people:')
for person in bunch_of_people: 
    print(person)

print('Us again:')
for person in us_again: 
    print(person)

Пример 3#

Както споменахме по-горе, writelines не добавя автоматично нови редове след всеки подаден елемент от списъка.

Напишете функция better_writelines, която поправя тази грешка.

Решение на Пример 3#

def better_writelines(lines: list[str], filepath: str):
    with open(filepath, 'w') as fp:
        fp.writelines([line + '\n' for line in lines])

Пример 4#

os.rmdir изтрива директория, само ако е празна.

Напишете функция rm_rf, която по подадена директория, изтрива съдържанието ѝ, както и самата директория.

Решение на Пример 4#

def rm_rf(filepath: str):
    if os.path.isdir(filepath):
        for file in os.listdir(filepath):
            rm_rf(os.path.join(filepath, file))
        os.rmdir(filepath)
    else:
        os.remove(filepath)

Пример 5#

Напишете програма за управление на музикални файлове.

Програмата трябва да поддържа следните функционалности:

  • Задаване на работна директория (директория, в която са ни музикалните файлове)

  • Извеждане на списък с всички песни (приемете, че всеки файл ще е с име във формат artist-song_name.mp3

  • Търсене на песен по изпълнител (връща точно съвпадение)

  • Търсене на псене по име на песен (връща точно съвпадение)

Потребителския интефейс може да бъде следния:

  • Потребителя има достъп до 5 команди (set, list, find artist, find song и exit)

  • Потребителя може да въвежда (и изпълнява) команди, до въвеждане на exit, при което програмата спира работа. (Ако имате други идеи за интерфейса, също ще бъдат приети)

Примерни песни може да свалите от тук