Obsługa plików jest ważną częścią programowania, umożliwiając zapis naszych danych do pliku, bądź wczytywanie do programu danych z zewnątrz.

Aby rozważyć temat plików musimy zdawać sobie sprawę w jaki sposób są one skonstruowane. W najprostszym podejściu możemy wyróżnić dwa rodzaje plików:

  • binarne, czyli zawierające dane numeryczne,
  • tekstowe, czyli takie, których dane można w sposób bezpośredni interpretować jako tekst.

Powyższa informacja uświadamia nam, że pliki tekstowe są również specyficzną (okrojoną do znaków drukowalnych i sterujących) wersją plików binarnych, których zawartością są ciągi tekstowe. Pliki binarne zaś mogą zawierać dane numeryczne, których interpretacja bajtowa w postaci tekstu nie ma sensu, bowiem są one zakodowaną w odpowiednim formacie liczbą, obrazem itp.

Python zawiera szereg bibliotek wspomagających obsługę plików, np. csv. Tym niemniej na początek zajmiemy się podstawowym dostępem do plików.

Zarządzaniem dostępu do plików możemy administrować z użyciem kilku poleceń funkcyjnych1polecenie funkcyjne to takie, które przyjmuje parametr i zwraca przez siebie wartość:

  • open(nazwa_pliku_ze_scieżką_dostępu, tryb_otwarcia) – zwraca tzw. uchwyt do pliku (przypisuje do zmiennej odnośnik do fizycznego pliku znajdującego się na dysku, tak aby było możliwe odniesienie się do jego zawartości w programie.
  • read(), readline() – metody stworzonego obiektu za pomocą open() umożliwiające odczyt ze wskazanego strumienia2powiązanie z plikiem tworzy z niego źródło, a dane ze źródła do programu płyną tzw. strumieniem aż do jego wyczerpania tj. odczytu pliku do jego końca.
  • write(), writelines() – metody stowrzonego obiekty za pomocą open() umożliwiające zapis do wskazanego strumienia.

open()

Użycie polecenia open() wiąże się z koniecznością podania nazwy pliku, który chcemy otworzyć, wraz ze ścieżką dostępu do niego3chyba, że nasz plik znajduje się w aktualnym katalogu naszego projektu np: C:\Users\us\AppData\Roaming\JB\PyCharm\scratches oraz trybem dostępu do pliku. Tryby dostępne to:

  • “r” – Read – wartość domyślna, funkcja zwróci błąd jeśli plik nie istnieje we wskazanej lokacji,
  • “a” – Append – otwiera plik do zapisu i ustawia znacznik na końcu pliku. W przypadku braku pliku zostanie stworzony nowy.
  • “w” – Write – otwiera plik do zapisu. Jeśli istnieje już taki plik, to jego zawartość zostanie nadpisana. W przypadku braku pliku zostanie stworzony nowy.
  • “x” – Create – otwiera plik do zapisu. Jeśli istnieje już taki plik, to funkcja zwraca błąd.

Dodatkowo możemy wyspecyfikować jak traktować skojarzony plik:

  • “t” – plik tekstowy – wartość domyślna,
  • “b” – plik binarny.

Przykład stworzenia pliku do dalszych z nim eksperymentów jest zamieszczony poniżej:

plik=open("plikTestowy.txt","w", encoding="utf-8")

plik.writelines(("Ala ma kota", "Kot Ali ma na imię Bucefał", "Koty nie zawsze lubią mleko", "Bucefałowi mleko nie służy, bo jest już dorosłym kotem."))


### otwarcie pliku i stworzenie go jeśli nie było takiego we wskazanej lokacji:
plik=open("C:/Python/plikTestowy.txt","w")

plik.writelines(("Ala ma kota.\n","Kot Ali ma na imię Bucefał.\n", "Koty nie zawsze lubią mleko.\n", "Bucefałowi mleko nie służy, bo jest już dorosłym kotem.\n"))

Podany wyżej kod daje nam dwie ważne informacje. Po pierwsze – próba zapisu pliku w lokacji, do której program nie ma praw dostępu4np.: główny katalog dysku C: skończy się błędem PermissionError: [Errno 13] Permission denied. Po drugie – zapis zdań do pliku nie gwarantuje, że pojawią się w nim znaczniki końca linii – o to musimy sami zadbać.

Ostatnim, opcjonalnym parametrem open() jest tryb kodowania jaki przypisujemy do danych w pliku. Standardem obecnie jest utf-8 i w większości przypadków właśnie on będzie właściwym. Jeśli pominiemy ten parametr, to tryb wymuszony przez środowisko operacyjne będzie użyty jako domyślny5np. w Windows jest to strona kodowa win-1250, czyli ANSI, co może powodować niezgodność trybów i błędy odczytu pomiędzy różnymi środowiskami operacyjnymi. Taki błąd zazwyczaj kończy wygenerowaniem wyjątku: UnicodeDecodeError i przerwaniem działania programu o ile nie zostanie obsłużony.

read()

Odczyt z pliku ma sens… po to w końcu uprzednio zapisywaliśmy do pliku dane, by teraz móc je odczytać. Można zrobić to np tak:

plik=open("C:/Python/plikTestowy.txt","r")
print(*plik.readlines(),sep="")

Użycie metody read() powoduje wczytanie od razu całej struktury pliku do pamięci.

write()

Metody write(), writelines() służą do skierowania danych podanych jako argument do przypisanego strumienia wyjściowego.

close()

Ważną praktyką jest zamknięcie połączenia z plikiem fizycznym. Co prawda pliki są zamykane po zakończeniu działania programu, ale… w przypadku nieprzewidzianego wypadku powstania jakiegoś nieobsłużonego błędu program terminuje się i w takim przypadku jest duże prawdopodobieństwo, że bufory wykorzystywane do obsługi naszego strumienia nie zostaną doń opróżnione, a dane te po prostu stracimy. Dlatego też właściwym jest zamknąć plik tak szybko jak tylko skończymy jego obsługę.

O buforach już rozmawialiśmy podczas omawiania pisania/czytania z terminala. Wielkość predefiniowanych w systemie buforów danych, zazwyczaj przyjmujących wielkość 4kB lub 8kB, można sprawdzić w poniższy sposób:

import io
print(io.DEFAULT_BUFFER_SIZE)

Obsługa plików bez wykonania na jej końcu close() jest wadliwą praktyką, proszącą się o błędy w sytuacjach, gdy nawet poprawne zakończenie programu może spowodować, że nie wszystkie dane zostaną zapisane do pliku.

Dobrą praktyką jest także użycie with dla zapewnienia poprawnego zamknięcia pliku nawet w sytuacji, gdy wygenerowany zostanie wyjątek:

with open("C:/Python/plikTestowy.txt","r") as plik
    print(*plik.readlines(),sep="")

seek()

Funkcja ta umożliwia poruszanie się znacznikiem pozycji wewnątrz pliku zmieniając jego wskazania po to by np. powrócić na początek pliku – seek(0). Trzeba jednak pamiętać, że:

  • przesuwanie znacznika odbywa się podając kolejny bajt na którym ma się ustawić znacznik, licząc od początku pliku,
  • przesuwanie znacznika nie działa w przypadku pisania do pliku, a tylko podczas pracy na pliku otwartym do odczytu.

tell()

Skoro wiemy już, że seek() pozwala nam na poruszanie się po pliku, to możliwe, że przydałaby się informacja, gdzie w takim pliku aktualnie się znajdujemy. Jest to możliwe za pomocą metody tell(), która zwraca informację o pozycji znacznika pliku. Co więcej, umożliwia nam ona ustawienie offsetu dla jego zastosowania z seek() np: seek(50, tell()). W innym przypadku wartość offsetu może być tylko jako 0.

delete()

Kasowanie plików czy katalogów nie jest dostępne bezpośrednio z metod obiektu, ale… jest możliwe z wykorzystaniem poleceń systemowych biblioteki os. Metoda ta jest opisana w dodatku know-how.

Najprościej można to przedstawić następująco:

import os
os.remove("plikTestowy.txt") 

#lub:

os.system("del plikTestowy.txt")

Przydaje się do powyższego znajomość poleceń systemowych systemu operacyjnego np. del, deltree, rmdir itp.

Trzeba pamiętać o zapobieganiu błędom, gdy nie ma elementów do skasowania… np.: tak:

import os
if os.path.exists("plikTestowy.txt"):
    os.remove("plikTestowy.txt") 
else:
  print("Nie ma takiego pliku!") 

Przy temacie dostępu do plików nie sposób nie wspomnieć o obsłudze możliwych do przewidzenia, lub nieoczekiwanych błędów. Ten temat omówiony jest w bloku know-how.

Zadanie 1: Papuga

Napisz program, który po uruchomieniu pyta o nazwę pliku na którym chcesz pracować, a następnie pyta się w którym z dwu trybów chcesz pracować… Obsłuż ewentualne warianty, aby program był możliwie mało awaryjny.
1) Emuluje edytor liniowy i zapisuje do wskazanego pliku wszystko co napiszesz na klawiaturze, linia po linii… aż do momentu, gdy jedynym znakiem w linii będzie “.” (kropki ma do pliku nie zapisywać!);
2) Odczytuje ze wskazanego pliku jego zawartość i wyświetla na konsoli.

Przykładowe rozwiązanie powyższego zadania

Spytaj prowadzącego o hasło
To view this protected content, enter the password below: