TI/Wejście i wyjście

Z Brain-wiki

Wprowadzenie

Twój program, jeśli ma robić coś użytecznego, zazwyczaj musi komunikować się z użytkownikiem. Na przykład, na podstawie wprowadzonych przez użytkownika danych program wykonuje pewne obliczenia i następnie wypisuje wynik, przekazując go tym samym użytkownikowi. Ten moment kiedy informacja zostaje wprodzona do programu, lub kiedy program zapisuje wyniki swojego działania gdzieś indziej niż w swojej pamięci, nazywamy operacjami wejścia/wyjścia.

W najprostszym wypadku komunikację z otoczeniem można wykonać przy użyciu funkcji raw_input i wyrażenia print, które odpowiednio wczytują linijkę i wypisują linijkę tekstu.

W bardziej zaawansowanej wersji komunikacji z otoczeniem, program odczytuje i zapisuje informację w plikach na dysku. Pozwala to na przechowywanie danych w sposób trwały — pliku w przeciwieństwie do napisów na ekranie przeżywają wyłączenie i włączenie komputera. Tworzenie plików, odczytywanie i zmiana ich zawartości to podstawowe narzędzie, które zostanie przedstawione w tym rozdziale.

Wprowadzanie danych przez użytkownika

Przykład:
#!/usr/bin/python
# -*- coding:utf-8 -*-
# user_input.py

def reversed(text):
    return text[::-1]

def palindrom(text):
    return text == reversed(text)

napis = input('Wprowadź napis: ')
if palindrom(napis):
    print("Tak, to palindrom")
else:
    print("Nie, to zwykłe słowo")
Wynik:
$ python user_input.py
Wprowadź napis: abrakadabra
Nie, to zwykłe słowo

$ python user_input.py
Wprowadź napis: kajak
Tak, to palindrom

$ python user_input.py
Wprowadź napis: 1234321
Tak, to palindrom

Jak to działa?

Funkcja reversed zwraca swój argument w odwrotnej kolejności wykorzystując zaawansowane indeksowanie. Poprzednio była mowa o indeksowaniu z dwoma argumentami [a:b] — czyli braniu podsekwencji od pozycji a do pozycji b.

Tutaj pierwsze dwa parametry są pomięte, czyli przyjmują wartości domyślne. Jako trzeci parametr, czyli krok, podajemy –1. Ujemny krok oznacza przejście od końca do początku, w ten sposób otrzymujemy sekwencję w odwrotnej kolejności.


Funkcja raw_input używa podanego argumentu jako zachęty dla użytkownika i czeka na jego odpowiedź. Po tym użytkownik coś wpisze, a tak naprawdę po tym jak naciśnie klawisz Enter, funkcja raw_input kończy swoje działanie i zwraca wpisany tekst.

Program porównuje wpisany tekst z jego odwrotnością. Porównywanie sekwencji oznacza przyrównanie ich element po elemencie, czyli w tym wypadku litera po literze. W wypadku gdy porównanie nie znalazło różnic, możemy oznajmić, że tekst jest słowem lustrzanym.


Pliki

Różne typy plików opisane są w materiałach do wykładu. Dostęp do plików na dysku uzyskuje się w Pythonie poprzez klasę file, która reprezentuje otwarty plik. Plik otwieramy wywołując funkcję open podając nazwę pliku. Otwarcie pliku oznacza, że system operacyjny zezwolił nam na dostęp do niego, i możemy wykonywać operacje odczytu i zapisu.

Przykład:

Część pierwsza użycia pliku:

#!/usr/bin/python
# -*- coding: utf-8 -*-

tekst = '''\
Litwo! Ojczyzno moja! ty jesteś jak zdrowie;
Ile cię trzeba cenić, ten tylko się dowie,
Kto cię stracił. Dziś piękność twą w całej ozdobie
Widzę i opisuję, bo tęsknię po tobie.
'''

f1 = open('wiersz.txt', 'w')
                # 'w' oznacza, że będziemy pisać do pliku
f1.write(tekst)
f1.close()       # 'wypchnięcie' zmian do pliku przez jego zamknięcie
Rezultat:
$ python zapis_pliku.py

Część druga użycia pliku:

#!/usr/bin/python
# -*- coding: utf-8 -*-

f2 = open('wiersz.txt')
                # brak drugiego argumentu oznacza 'tylko odczyt'
for wers in f2:
    print wers,
Rezultat:
$ python odczyt_pliku.py
Litwo! Ojczyzno moja! ty jesteś jak zdrowie;
Ile cię trzeba cenić, ten tylko się dowie,
Kto cię stracił. Dziś piękność twą w całej ozdobie
Widzę i opisuję, bo tęsknię po tobie.
Jak to działa

Tworzymy plik wiersz.txt otwierając go w trybie do zapisu (z argumentem 'w'). Jeśli plik jeszcze nie istniał, to zostaje stworzony pusty, a jeśli już istniał, to jego zawartość zostaje wykasowana w momencie otwarcia. Do tego pliku zapisujemy tekst jako treść.

Następnie otwieramy ten sam plik ponownie, tym razem w trybie do odczytu. Następnie linijka po linijce odczytujemy plik i wypisujemy każdą linijkę.

Program jest bardzo prosty, ale jest tu parę drobiazgów, na które warto zwrócić uwagę:

  1. Zmienna tekst jest wielo-linijkowym ciągiem znaków stworzonym przez użycie potrójnego apostrofu ''' jako ogranicznika.
  2. Bezpośrednio po otwarciu tego napisu jest użyta kontynuacja linii \, po to, by napis miał naturalne wcięcia (czyli ich brak). Gdyby znaku kontynuacji nie było, napis zawierałby dodatkową pustą linjkę na samym początku.
  3. Po zapisaniu zmian „wypychamy” je na dysk przez użycie metody close. Normalnie operacje na plikach są wykonywane dopiero wtedy, gdy uzbiera się ich większa ilość. Niemniej, tutaj zaraz chcemy zacząć odczytywać ten sam plik i musimy się upewnić, że nasze zmiany naprawdę znalazły się w pliku zanim zaczniemy ten plik czytać .
  4. Iteracja po obiekcie file oznacza iterację po linijkach, co tutaj wykorzystujemy.
  5. Każda linijka (zmienna wers) jest zakończona swoim znakiem nowej linii i dlatego wywołujemy print tak, by nie dodało nowego znaku końca linii, czyli z , na końcu.
  6. Gdybyśmy nie wywołali close, to zmiany w pliku i tak zostałyby zapisane automatycznie najpóźniej w momencie zakończenia programu. Gdy program kończy swoje działanie, wszystkie zmiany są zapisywane i pliki są zamykane przez system operacyjny.

Więcej o otwieraniu plików

Pliki mogą być otwierane w różny sposób — mówimy, że są otweirane w danym trybie. Dostępne tryby to:

  • r {{ang|read otwarcie do czytania
  • w {{ang|write otwarcie do pisania, jeśli plik już istnieje to domyślnie kasujemy jego zawartość
  • a {{ang|append otwarcie do dopisywania. W tym trybie jeśli pliku nie było to zostanie utworzony, a jeśli był to nasze wpisy są dodawane na końcu.

Jeśli pominiemy tryb to domyślnie przyjmuje on wartość r (tylko do odczytu).

W niektórych systemach operacyjnych rozróżniane są pliki tekstowe i binarne. Każdy plik można otworzyć w jednym i w drugim trybie, efektem jest nieco inna obsługa plików przez program.

Niemniej otwieranie plików binarnych w trybie tekstowym nie jest dobrym pomysłem.

Różnica między dwoma trybami jest taka, że w trybie „tekstowym” biblioteka obsługująca odczytywanie z i zapisywanie do pliku w szczególny sposób obsługuje znaki końca linii. W przypadku trybu „binarnego” taka specjalne traktowanie nie ma miejsca i program dostaje dokładnie takie dane jakie odczytał z pliku.

Jak w pliku tekstowym jest zapisany „koniec linii”? Na kartce początek nowego wiersza oznacza się poprzez umieszczenie pierwszej litery blisko lewego krańca tekstu. W pliku coś takiego nie jest możliwe — bo każdy plik, również tekstowy jest po prostu ciągiem bajtów. Sytuacja jest podobna do cytowania wierszy w felietonach w gazecie, gdzie znaki nowej linii oznacza się używając kreski:

 To jest pierwsza linijka/To jest druga linijka.

W plikach do rozdzielania wierszy używa się pewnej ustalonej sekwencji. Kiedy program natrafi na taki bajt czy bajty w pliku zawierającym tekst, to rozumie się, że jest to koniec poprzedniej i początek następnej linijki.

Wszystko byłoby dobrze, gdyby nie fakt, że w różnych systemach operacyjnych używa się różnych sekwencji do oznaczenia końca linii. W systemach UNIXowych (takich jak Linux czy BSD) używa się pojedynczego bajtu o wartości 0x0a. Na Macintoshach też używa się jednego bajtu, ale o wartości 0x10. Natomiast w systemach Microsoftu używa się dwóch bajtów — 0x0d 0x0a.

Aby programista nie musiał pamiętać tych wartości, w kodzie Pythona, tak samo jako w C, Perlu i wielu innych językach, \n i \r oznaczają odpowiednio 0x0d i 0x0a. Na dodatek jeśli otworzy się plik w trybie tekstowym, po odczytaniu danych z pliku, wszystkie specyficzne dla danego systemu operacyjnego znaki końca linii są zamieniane na 0x0a. Pozwala to na wygodną obsługę plików tekstowych.

Przykłady

Wczytanie danych z pliku tekstowego i wykreślenie sygnału

Proszę pobrać plik http://brain.fuw.edu.pl/~jarek/SYGNALY/TF/c4spin.txt.

 
import pylab
import numpy

fi = open('c4spin.txt')
x=[]
for linijka in fi:
    x.append(int(linijka))

signal = numpy.array(x)    
pylab.plot(signal/20)
pylab.show()
Jak to działa

Otwieramy plik tekstowy c4spin.txt. Każda linijka w tym pliku zawiera jedną próbkę sygnału EEG. W pętli kolejno odczytujemy z pliku linie tekstu, interpretujemy je jako liczby całkowite (int), a następnie dodajemy na koniec listy x. Po zakończeniu odczytu z  pliku zamykamy go. Dla większej wygody operacji na sygnale dobrze jest przekształcić listę w tablicę modułu numpy. Teraz łatwo możemy np. skalibrować sygnał. Załóżmy, że dane pochodziły z przetwornika analogowo-cyfrowego, o którym wiemy, że na napięcie 1 µV na jego wejściu daje na wyjściu liczbę 20. Zatem żeby wyskalować nasz sygnał w µV trzeba podzielić go przez 20.


Wczytanie danych z pliku tekstowego z użyciem funkcji bibliotecznej

W poprzednim przykładzie próbki były wczytywane w pętli — ale jest też funkcja która robi dokładnie to samo.

import pylab
import numpy

signal = numpy.loadtxt('c4spin.txt')    
pylab.plot(signal/20)
pylab.show()


Te dane mogą być też przechowywane np. w postaci binarnej jako dwubajtowe liczby całkowite (typu short, plik http://brain.fuw.edu.pl/~jarek/SYGNALY/c4spin.bin) lub np. czterobajtowe liczby zmiennoprzecinkowe (typu single, plik http://brain.fuw.edu.pl/~jarek/SYGNALY/c4spinf.bin).

Proszę podejrzeć wszystkie trzy pliki w powłoce systemu poleceniem less nazwa_pliku. Następnie proszę napisać program rysujący jednakowe wykresy wczytując dane z każdego z trzech plików.
Wskazówka: pliki binarne wczytuj funkcją fromfile z modułu numpy.

Wczytanie danych z pliku binarnego i wykreślenie sygnału

Proszę pobrać plik http://brain.fuw.edu.pl/~jarek/SYGNALY/prawahj12 — są tam zapisane dane: 24 kanały pomiaru (próbkowane z częstością 128 Hz), w każdym po 1024 punkty pomiarowe, eksperyment powtórzono 57 razy, zapisano w pliku binarnym jako multipleksowane szesnastobitowe liczby całkowite (short).

Z pliku proszę wybrać kanał 1 i wykreślić pierwsze powtórzenie.

import pylab
import numpy

x=numpy.fromfile('prawahj12','short')
signal = x[0::24]    
pylab.plot(signal[:1024])
pylab.show()

Inny sposób:

import pylab as p
import numpy

x=numpy.fromfile('prawahj12','short')
x=numpy.reshape(x,(57,1024,24))
p.plot(x[0,:,0])
p.show()


Wczytanie danych z pliku binarnego i wykreślenie sygnału

Ten sam plik co w poprzednim przykładzie: http://brain.fuw.edu.pl/~jarek/SYGNALY/prawahj12 — 24 kanały, 128 Hz, 57 powtórzeń po 1024 punkty, format liczb: szesnastobitowe całkowite. Z pliku tego wybieramy kanał 1 i rysujemy na wykresie pierwsze powtórzenie.

import pylab
import numpy

x = numpy.fromfile('prawahj12','short')
signal = x[0::24]    
pylab.plot(signal[:1024])
pylab.show()

x.reshape(-1, 24)
pylab.figure()
pylab.plot(x[0:1024,0])
pylab.show()

Pickle'owanie i podobne czynności

O module pickle

Standardowa biblioteka Pythona zawiera moduł pickle (słowo to oznacza sposób konserwowania m. in. ogórków). Służy on do zapisywania dowolnych obiektów Pythona w pliku, i odwrotnie, do późniejszego oczytywania i rekreacji obiektów. W języku angielskim mówi się o persistent storage („pamięci trwałej”), by podkreślić, że obiekty są zachowane w sposób trwały, w przeciwieństwie do obiektów w programie, które znikają po jego zakończeniu.

Przykład
#!/usr/bin/python
# -*- coding:utf-8; -*-
# Filename: pickling.py
import pickle

# the name of the file where we will store the object
trwala_lista_zakupow = 'lista_zakupow.data'
# lista rzeczy do kupienia
lista_zakupow = ['jabłka', 'ziemniaki', 'pomidory']

# zapisanie do pliku
f = open(trwala_lista_zakupow, 'wb')
pickle.dump(lista_zakupow, f)
f.close()

# usunięcie zmiennej 'nie-trwałej'
del lista_zakupow

# odzyskanie zmiennej
f = open(trwala_lista_zakupow, 'rb')
lista_zakupow = pickle.load(f)
for rzecz in lista_zakupow:
  print rzecz
$ python pickling.py
jabłka
ziemniaki
pomidory
Jak to działa

Aby zapisać dane „zakonserwowane” do pliku, otwieramy plik w trybie do zapisu. Parametr 'wb' oznacza write-binary, czyli dodatkowo mówimy, że dane mają być zapisane do pliku dokładnie tak, jak je podajemy. W przypadku wersji Python 2.x brak tego parametru nie ma znaczenia, ale nie szkodzi. Natomiast w nowszych wersjach Pythona (3.x), jest on potrzebny. Właściwego zapisu dokonujemy funkcją pickle.dump

Aby odczytać „zakonserwowane” dane z pliku, otwieramy plik w trybie do odczytu. Funkcja pickle.load pozwala nam odczytać dokładnie to co zapisaliśmy.

Zapisywanie danych w numpy

W module numpy dostępnych jest kilka funkcji obsługujących wygodny zapis i odczytywanie obiektów, a w szczególności macierzy: save,savetxt. Poniżej opiszemy nieco dokładniej funkcję numpy.savez. Jej składnia jest następująca:

 
numpy.savez(plik, *argumenty, **argumenty_nazwane)

Funkcja ta umożliwia zapis kilku macierzy do jednego spakowanego archiwum w formacie .npz. Jeśli wywołamy funkcję z argumentami nazwanymi nazwy to nazwy te zostaną zapisane razem z zawartością macierzy. Jeśli nie podamy nazw, tzn wywołamy funkcję numpy.savez tylko z lista zmiennych do zapisania to zostaną one zapisane z domyślnymi nazwami arr_0, arr_1, ...

Parametry:

plik
napis zawierający nazwę pliku lub obiekt plik (wytworzony np. przez wywołanie funkcji open). Do nazwy pliku zostanie dodane rozszerzenie .npz (chyba, że już wcześniej nazwa pliku je zawierała)
argumenty
wszystkie pozostałe argumenty na liście po nazwie pliku są traktowane jako zmienne do zapisania, mogą to być też dowolne wyrażenia. Argumenty przekazane na tej liście będą zapisane z domyślnymi nazwami: arr_0, arr_1, itd.
argumenty_nazwane
argumenty te przekazywane są jako pary klucz=wartość. Zmienna wartość zostanie zapisana razem z nazwą klucz


Uwaga

Format pliku .npz to spakowane archiwum. Każdy plik w tym archiwum przechowuje jedna z zapisywanych zmiennych w formacie .npy i ma nazwę zmiennej, którą przechowuje.

Do wczytywania plików .npz służy funkcja numpy.load. Obiekt, który ona zwraca zawiera pole files zawierające listę dostępnych kluczy. Klucze te mogą być użyte do indeksowania przechowywanych w tym obiekcie zmiennych.

Przykład

import numpy as n
x = n.arange(10)
y = n.sin(x)
#użycie  savez z  *argumentami, macierze zapisywane są z domyślnymi nazwami         
n.savez('plik_testowy', x, y)

wczytane = n.load('plik_testowy1.npz')
wczytane.files
    ['arr_1', 'arr_0']
wczytane['arr_0']
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
#użycie savez z **argumentami_nazwanymi, macierze zapisywane są z nazwami podanymi jako klucze.
n.savez('plik_testowy2', x=x, y=y)
wczytane = n.load('plik_testowy2.npz')
wczytane.files
 ['y', 'x']
wczytane['x']
 array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Pliki matlabowe

W pracy z programem Matlab firmy Mathworks można zachować zmienne z bieżącego środowiska pracy do pliku. Możliwości są bardzo podobne do numpy.savez. Format jest natomiast zupełnie inny.

Pakiet Scipy pozwala na zapis i odczyt takich plików, mają one zwyczajową końcówkę .mat.

import scipy.io as io

D = io.loadmat('test1.mat')
# D zawiera słownik {zmienna:wartość}

io.savemat('test2.mat', {'nowa_zmienna': 666})

Scipy nie obsługuje wszystkich wersji format plików matlabowych i nie wszystkie struktury danych z Matlaba daje się jednoznacznie i czysto przełożyć na struktury Pythonowe, więc funkcje loadmat i savemat mają sporo opcji regulujących ten proces. (zob. help(io.loadmat) lub docs.scipy.org)


Porównanie danych „świeżych” i „konserwowych”

Powyżej zostały przedstawione dwa mechanizmy zachowywania danych w postaci obiektów bezpośrednio do plików. Matlabowy format '.mat' jest przewidziany do przechowywania struktur danych z Matlaba, a ponieważ menażeria obiektów w Pythonie jest bogatsza, to nie wszystko da się zachować. Niemniej założenie jest takie samo — by zapisać obiekty jako ciąg bajtów w pliku. Przecież już normalnie można dane odczytywać z i zapisywać do plików, więc kiedy programista powinien zdecydować się na picklowanie?

Kiedy zapisujemy dane do pliku, używamy pewnego określonego formatu. Takim formatem może być np. XML, czy jakaś odmiana tzw. CSV (Comma Seperated Values). Takim formatem jest też pickle czy mat-file. Istotna różnica jest taka, że w przypadku formatów przeznaczonego do przechowywania stanu z danego języka, nie ma osobnej warstwy tłumaczącej, która bierze dane odczytane z pliku i wywołuje funkcje konstruujące obiekty. Ponieważ dane są w formacie tak blisko powiązanym z gotowymi obiektami, to sam moduł pickle może zrekonstruować obiekty.

To którego formatu należy użyć, zależy od celu w jakim dane zapisujemy i od tego z kim chcemy te dane wymieniać.

O formacie pickle można powiedzieć, że:

  • Służy tylko do wymiany pomiędzy różnymi programami w Pythonie.

Wynika to z tego, że zarówno implementacja jest dostępna tylko na Pythona, jak i z tego, że sam format jest bezpośrednio związany z pythonowymi strukturami danych.

  • Można go używać do wymiany pomiędzy różnymi wersjami Pythona, ale
  • nie nadaje się do naprawdę trwałego przechowywania danych.

Wynika to z tego, że wersja Pythona za rok czy dwa na pewno te dane przeczyta, ale nie ma pewności czy za 20 lat się uda.

  • Jeśli pickluje się obiekty, które nie należą do podstawowego zestawu takiego jak napisy, liczby, listy, słowniki, to zachowane dane są silnie związane z używaną przez nas hierarchią klas.

Można pokrótce powiedzieć, że picklowanie to użyteczny mechanizm roboczy, na teraz, do komunikacji w określonym ekosystemie programów. Do komunikacji z innymi ludźmi, czy przetrzymywania danych archiwalnych, pickle się nie nadaje.


dane.txt
mymat.txt