Wiemy już jak zapisywać w plikach napisy. Co jednak należałoby zrobić, gdybyśmy chcieli zapisać w pliku obiekty innych typów? Istnieje co prawda funkcja str, zamieniająca obiekt standardowego typu na postać tekstową:
lista = [1, 2, "trzy", 4]
>>> s=str(lista)
>>> s
"[1, 2, 'trzy', 4]"
Problem w tym, że nie da się łatwo wykonać odwrotnej transformacji, a dokładniej rzecz ujmując, jej rezultat jest daleki od pożądanego:
>>>
l=list(s)
>>> l
['[', '1', ',',
' ', '2', ',', ' ', "'", 't', 'r', 'z', 'y',
"'", ',', ' ', '4', ']']
Na szczęście w Pythonie dostępny jest moduł pickle, służący, jak nazwa wskazuje, do peklowania obiektów. Programiści piszący w językach .NET lub Javie używają na określenie tego procesu bardziej górnolotnego terminu, a mianowicie mówią o serializacji. Serializacja obiektu polega na przekształceniu danych go opisujących w ciąg bajtów (funkcja dumps), z którego można później odtworzyć taki sam obiekt (funkcja loads).
>>>
import pickle
>>> zapis=pickle.dumps(lista)
>>> l=pickle.loads(zapis)
>>> l
[1, 2, 'trzy', 4]
Jak widać powyżej, udało nam się zachować i odtworzyć listę w zmiennej zapis. Sam zapis jest napisem o poniższej zawartości:
>>> zapis
"(lp0\nI1\naI2\naS'trzy'\np1\naI4\na."
Załóżmy teraz, że chcielibyśmy razem z listą zachować i słownik. To również nie jest trudne, pod warunkiem umieszczenia ich w krotce:
>>>
slownik={"a":"b",1:2}
>>> zapis=pickle.dumps((lista,slownik))
>>> del lista
>>> del slownik
>>> (lista,slownik)=pickle.loads(zapis)
>>> lista; slownik
[1, 2, 'trzy', 4]
{'a': 'b', 1: 2}
Dzięki pickle możemy także zachowywać obiekty należące do klas zdefiniowanych przez nas samych:
>>> class wymiary3:
x=0; y=0; z=0
>>> w3=wymiary3()
>>> w3.x=1;
w3.y=2; w3.z=3
>>> zapis=pickle.dumps(w3)
>>> del w3
>>> w3=pickle.loads(zapis)
>>> w3.x;
w3.y; w3.z
1
2
3
Napis reprezentujący zapeklowany obiekt możemy zapisać samodzielnie do pliku, możemy też posłużyć się funkcjami dump i load, które (w odróżnieniu od dumps i loads) zachowują obiekt w pliku (a nie napisie):
>>> f1=file("trzy_rzeczy.txt","w+")
>>> pickle.dump((lista,slownik,w3),f1)
>>> lista=[]; slownik={};
w3=wymiary3()
>>> lista; slownik; w3.x
[]
{}
0
>>> f1.seek(0)
>>> (lista,slownik,w3)=pickle.load(f1)
>>> lista; slownik; w3.x
[1, 2, 'trzy', 4]
{'a': 'b', 1: 2}
1
Zachowywanie wielu obiektów w pojedynczej krotce jest wygodne, dopóki ich liczba nie osiągnie zbyt dużej wartości. Wtedy o wiele wygodniejsze jest użycie słownika. Najprostszym takim rozwiązaniem dostępnym w Pythonie jest baza danych zdefiniowana w module dumbdbm, stanowiąca w istocie implementację pliku o organizacji indeksowo-sekwencyjnej. Metoda dumbdbm.open tworzy na dysku (lub otwiera istniejącą) prostą bazę danych o podanej nazwie (w istocie na dysku tworzone są dwa pliki: indeksowany z rozszerzeniem „.dat” i indeksujący z rozszerzeniem „.dir”). Obsługa bazy jest identyczna jak obsługa słownika, z tą różnicą, że wszystkie zachowane w niej dane przechowywane są nie w pamięci, lecz na dysku:
>>> import dumbdbm
>>> db=dumbdbm.open("prosta_baza")
>>> db['napis']="hej
ho!"
>>> db['napis']
'hej ho!'
Bazy danych typu dbm pozwalają używać tylko napisów jako kluczy (co jest do przyjęcia) i wartości (co stanowi pewien problem). Stąd próba zachowania w niej obiektu innego niż napis typu, nieuchronnie kończy się błędem:
>>> db['lista']=lista
Traceback (most
recent call last):
File "<pyshell#282>", line
db['lista']=lista
File "C:\Python24\lib\dumbdbm.py",
line
raise TypeError, "keys and values must be strings"
TypeError: keys
and values must be strings
Rozwiązaniem jest oczywiście peklowanie, jednak w praktyce nie jest to zbyt wygodne:
>>> db['lista']=pickle.dumps(lista)
>>> pickle.loads(db['lista'])
[1, 2, 'trzy', 4]
dlatego naszą prostą bazę już zamkniemy
>>> db.close()
a zajmiemy się bliżej modułem shelve, który oferuje analogiczny sposób dostępu do danych (podobny słownikowi), umożliwiając jednak zachowywanie obiektów dowolnego typu (nie tylko napisów):
>>>
import shelve
>>> db
= shelve.open ('baza')
>>>
db['lista']=lista
>>>
db['lista']
[1, 2, 'trzy',
4]
>>>
db['slownik']=slownik
>>>
db['slownik']
{'a': 'b', 1: 2}
W opisywanej wersji Pythona, shelve posługuje się lepszym niż dumbdbm motorem bazy danych, a mianowicie dbhash (który z kolei opiera się na motorze BSD). Nadal jednak jest to motor nie pozwalający na obsługę dużych baz danych. W przypadku takiej konieczności właściwym rozwiązaniem jest podłączenie Pythona do zewnętrznej bazy danych (np. poprzez sterowniki ODBC), co również jest czynnością prostą, jednak z pewnością wykraczającą poza podstawy programowania (dla naszych skromnych potrzeb w zupełności wystarczający jest już dumbdbm), stąd problematyki tej nie będziemy tu podejmować, odsyłając zainteresowanych do specjalistycznej literatury.
Bazę danych stworzoną przy pomocy shelve obsługujemy tak jak słownik, a zatem dostępne są wszystkie operacje i metody działające dla prawdziwych słowników:
>>> len(db)
2
>>> 'lista' in db
True
>>> db.keys()
['lista', 'slownik']
>>> db.values()
[[1, 2, 'trzy', 4], {'a': 'b', 1: 2}]
>>> db.items()
[('lista', [1, 2, 'trzy', 4]),
('slownik', {'a': 'b', 1: 2})]
>>> db['lista']=[3,2]
>>> del db['lista']
>>> db.items()
[('slownik', {'a': 'b', 1: 2})]
>>> db.clear()
>>> db.items()
[]
Ponadto, bazę można zamknąć:
>>> db.close()
Przyjrzymy się teraz programowi, w którym wykorzystano opisane wyżej rozwiązania.
Przykład. Program ‘parking.py’ służy do ewidencjonowania samochodów stojących na płatnym parkingu. Realizuje następujące funkcje: wjazd (zapisanie numeru samochodu i godziny zaparkowania), wyjazd (oblicza opłatę należną za czas parkowania), ustalenie opłaty i okresu jej naliczania, wyświetlenie listy pojazdów stojących aktualnie na parkingu.
Kod źródłowy z opisem. Na początku otwieramy w IDLE nowe okno edycji i od razu zapisujemy pod nazwą ‘parking.py’.
Program korzystał będzie z trzech modułów: shelve – do obsługi bazy danych, sys – do wychodzenia z programu, time – do obsługi czasu. Piszemy więc:
import shelve, sys
from time import *
Każdą z funkcji programu ujmiemy w osobnym podprogramie. Zaczniemy od zmiany wysokości opłaty parkingowej.
# zmiana opłat
def zmiana_stawki():
global baza,stawka,okres # zmienne
globalne
print "\nZmiana wysokości opłat\n"
print "Bieżąca stawka wynosi %.2f zł za %i minut(y)\n" %
(stawka,okres)
try:
s=float(raw_input("Podaj nową wysokość opłat: "))
o=int(raw_input("Podaj nowy czas naliczania w minutach: "))
except:
print "Błąd wprowadzania danych! Stawka nie została
zmieniona!"
return
try:
baza['_stawka']=(s,o) #
zapisujemy w bazie
except:
print "Błąd zapisu danych! Stawka nie została zmieniona!"
else:
stawka=s # kopiujemy do
okres=o # zmiennych globalnych
Zmienne stawka i okres są globalne, gdyż korzystać z nich będą również inne funkcje. Wprowadzone wartości próbujemy zapisać w bazie, a dopiero kiedy to się uda – kopiujemy do zmiennych globalnych (taka kolejność jest konieczna by zachować spójność danych w bazie i pamięci w każdym przypadku).
Inicjalizacja bazy danych ma za zadanie otworzyć bazę i załadować z niej wcześnie zapisaną stawkę. Jeżeli baza jest świeża, stawka musi być wprowadzona własnoręcznie przez użytkownika (poprzez wywołanie funkcji zdefiniowanej przed chwilą; wcześniej, aby w niej uniknąć błędu, inicjalizujemy zmienne globalne pierwszymi lepszymi wartościami).
Stawkę zapisujemy w kluczu '_stawka'. Możemy sobie na to pozwolić, przyjmując, że nie jest to dopuszczalny numer rejestracyjny w Polsce.
#inicjalizacja bazy danych
def init():
global baza,stawka,okres
try:
baza = shelve.open ('baza_parkingowa') # otwarcie
except:
print "Błąd krytyczny! Baza danych nie została otwarta!"
sys.exit(0) # wyjście z programu
print "Inicjalizacja udana. Baza danych została otwarta."
if '_stawka' in baza.keys(): #
czy już istnieje?
(stawka,okres)=baza['_stawka'] #
tak - kopiujemy stawki
else:
(stawka,okres)=(0.0,1) # nie -
zmiana_stawki() # wczytujemy od
użytkownika
Kolejna funkcja zajmuje się wyświetlaniem menu głównego programu. Wyświetla ono dostępne funkcje i daje możliwość wyboru jednej z nich, zwracając rezultat na zewnątrz. Zwróćmy uwagę, że z napisu wprowadzonego przez użytkownika bierzemy jedynie pierwszy znak i konwertujemy go na dużą literę (by uniknąć problemów, gdy użytkownik ma wyłączony klawisz CAPSLOCK).
# menu główne programu
def menu():
while True:
print
print '-'*70
print
'PARKING'.center(70)
print '-'*70
print '[W] Wjazd [E] Wyjazd [P] Pojazdy [S] Stawka [K]
Koniec'.center(70)
print '-'*70
w=raw_input()[0].upper() # pierwszy znak (duza litera)
if w in 'WEPSK': # znany?
return w # tak - zwracamy go
print 'Nieznane polecenie -',
Funkcja pojazdy wyświetla listę pojazdów znajdujących się na parkingu. Dane pobierane są z bazy i wyświetlane z użyciem prostego formatowania.
# lista pojazdów na parkingu
def pojazdy():
global baza
print
print 'Lista pojazdów na parkingu'.center(33)
print '-'*33
print '|'+'Nr rej.'.center(10)+'|'+'Godz. parkowania'.center(20)+'|'
print '-'*33
for rej,godz in baza.items():
if rej!='_stawka':
print "|%9s |" %
rej,strftime("%H:%M (%Y-%m-%d)",godz),'|'
print
'-'*33
Wyjazd pojazdu wymaga upewnienia się czy dany pojazd rzeczywiście był zaparkowany, następnie obliczenia należnej opłaty, a wreszcie usunięcia pojazdu z bazy. Aby wyliczyć opłatę musimy: zamienić czas parkowania i obecny na sekundy (przy użyciu mktime), wyliczyć ich różnicę, przeliczyć na minuty (60 sekund w minucie), przeliczyć na jednostki taryfowe (uwzględniając zasadę zaliczania każdej rozpoczętej jednostki – dodajemy po prostu liczbę minut o 1 mniejszą od pełnego okresu), a na końcu przemnożyć rezultat przez wysokość opłaty.
# rejestracja wyjazdu pojazdu
def wyjazd():
global baza,stawka,okres
print 'Wyjazd pojazdu - godzina',strftime("%H:%M (%Y-%m-%d)")
rej=raw_input('Podaj numer rejestracyjny pojazdu: ')
if rej in baza.keys(): # czy
taki był zaparkowany?
godz=baza[rej]
print
"Godzina wjazdu:",strftime("%H:%M (%Y-%m-%d)",godz)
minuty=int(mktime(localtime())-mktime(godz))/60
jednostki=(minuty+okres-1)/okres
# naliczamy za rozpoczeta
print "\nDo zapłaty: %.2f zł" % (jednostki*stawka)
del baza[rej] # usuwamy wpis
else:
print "Błąd! Takiego pojazdu nie ma na parkingu!"
Rejestracja wjazdu pojazdu jest znacznie prostsza, wymaga jedynie upewnienia się, czy dany pojazd aby nie był już zaparkowany i dopisania numeru rejestracyjnego oraz aktualnego czasu do bazy. Przyjmujemy numery rejestracyjne nie dłuższe niż 9 znaków; nie przyjmujemy rejestracji ‘_stawka’, by uniemożliwić uszkodzenie zapisu stawek.
# rejestracja wjazdu pojazdu
def wjazd():
global baza
godz=localtime()
print 'Wjazd pojazdu - godzina',strftime("%H:%M
(%Y-%m-%d)",godz)
rej=raw_input('Podaj numer rejestracyjny pojazdu: ')[:9]
if rej=='_stawka': return # zabezpieczenie
if rej not in baza.keys(): #
nie jest zaparkowany?
baza[rej]=godz
print "Wprowadzono."
else:
print "Błąd! Taki pojazd już jest na parkingu!"
Z programu głównego wyłączyliśmy jeszcze funkcję wywołującą odpowiedni podprogram w zależności od wyboru użytkownika.
# realizacja wyboru użytkownika
def wybor():
while True:
w=menu()
if w=='K':
break
elif w=='S':
zmiana_stawki()
elif w=='P':
pojazdy()
elif w=='E':
wyjazd()
elif w=='W':
wjazd()
Sam program główny jedynie otwiera bazę, wywołuje funkcję wybor, a na końcu zamyka bazę.
# program główny
init() # otwarcie bazy
try:
wybor() # interfejs użytkownika
except:
print "Wystąpił poważny błąd."
baza.close() # zamknięcie bazy
Przykład uruchomienia. Wciskamy przycisk F5. Jako, że jest to pierwsze uruchomienie programu, zostaniemy poproszeni o podanie wysokości opłat.
>>> ================================
RESTART ==============================
>>>
Inicjalizacja udana. Baza danych
została otwarta.
Zmiana wysokości opłat
Bieżąca stawka wynosi 0.00 zł za 1
minut(y)
Podaj nową wysokość opłat:
Wprowadzamy np. poniższe wartości (pamiętajmy o kropce dziesiętnej!).
Podaj nową wysokość opłat: 1.50
Podaj nowy czas naliczania w minutach:
30
----------------------------------------------------------------------
PARKING
----------------------------------------------------------------------
[W] Wjazd [E] Wyjazd [P] Pojazdy [S] Stawka [K] Koniec
----------------------------------------------------------------------
Kiedy pojawi się menu główne wybieramy opcję ‘W’ (potwierdzamy klawiszem ENTER) i wprowadzamy (oczywiście, godziny odpowiadają wprowadzaniu danych przez autora):
Wjazd pojazdu - godzina 21:10
(2006-09-18)
Podaj numer rejestracyjny pojazdu:
ZS39542
Wprowadzono.
Kiedy ponownie pojawi się menu główne wybieramy znowu opcję ‘W’ (potwierdzamy klawiszem ENTER) i wprowadzamy drugie auto:
Wjazd pojazdu - godzina 21:11
(2006-09-18)
Podaj numer rejestracyjny pojazdu:
SZF8510
Wprowadzono.
Zaparkujemy jeszcze trzecie auto:
Wjazd pojazdu - godzina 21:11
(2006-09-18)
Podaj numer rejestracyjny pojazdu: Z0
JOLA
Wprowadzono.
----------------------------------------------------------------------
PARKING
----------------------------------------------------------------------
[W] Wjazd [E] Wyjazd [P] Pojazdy [S] Stawka [K] Koniec
----------------------------------------------------------------------
Tym razem z menu wybieramy opcję ‘P’. Zobaczymy (przypominam, że czasy będą się różnić) mniej więcej taki widok:
Lista pojazdów na parkingu
---------------------------------
| Nr rej. |
Godz. parkowania |
---------------------------------
|
ZS39542 | 21:10 (2006-09-18) |
|
Z0 JOLA | 21:11 (2006-09-18) |
|
SZF8510 | 21:11 (2006-09-18) |
---------------------------------
Sprawdzimy teraz, czy dane rzeczywiście są przechowywane trwale. Wyjdźmy z programu wybierając z menu opcję ‘K’. Następnie uruchommy go ponownie. Powinniśmy zobaczyć:
>>> ================================
RESTART ==============================
>>>
Inicjalizacja udana. Baza danych
została otwarta.
----------------------------------------------------------------------
PARKING
----------------------------------------------------------------------
[W] Wjazd [E] Wyjazd [P] Pojazdy [S] Stawka [K] Koniec
----------------------------------------------------------------------
Jak widać stawka została przeczytana z bazy i nie ma potrzeby jej ponownego wprowadzania. Jeżeli wybierzemy opcję ‘P’, powinniśmy zobaczyć listę identyczną jak przed wyjściem z programu:
Lista pojazdów na parkingu
---------------------------------
| Nr rej. |
Godz. parkowania |
---------------------------------
|
ZS39542 | 21:10 (2006-09-18) |
|
Z0 JOLA | 21:11 (2006-09-18) |
|
SZF8510 | 21:11 (2006-09-18) |
---------------------------------
Spróbujemy teraz ‘wyjechać’ jednym z aut. Wybieramy ‘E’:
Wyjazd pojazdu - godzina 21:15
(2006-09-18)
Podaj numer rejestracyjny pojazdu:
ZS39542
Godzina wjazdu: 21:10 (2006-09-18)
Do zapłaty: 1.50 zł
Sprawdzamy stan bieżący:
Lista pojazdów na parkingu
---------------------------------
| Nr rej. |
Godz. parkowania |
---------------------------------
|
Z0 JOLA | 21:11 (2006-09-18) |
|
SZF8510 | 21:11 (2006-09-18) |
---------------------------------
Dalszą zabawę z programem pozostawiam Wam.
Ćwiczenie I. Napisz program ‘parking2.py’ różniący się od programu ‘parking.py’ tym, że klienci parkingu mogą posiadać miesięczne karty abonentowi (dodać funkcję sprzedaży). Wjazdy i wyjazdy samochodów takich klientów są rejestrowane, jednak przy wyjeździe z parkingu opłata nie jest pobierana, o ile nie abonament nie skończył się (wyświetla się tylko informacja o liczbie pozostałych dni). Jeżeli abonament danego auta skończył się, przy jego najbliższym wjeździe lub wyjeździe użytkownik jest o tym informowany i może wykupić nowy abonament lub z niego zrezygnować (i w konsekwencji wnieść standardową opłatę).
Ćwiczenie II. Napisz program ‘narzedzia.py’ służący do
obsługi narzędziowni. Program ma umożliwiać wykonywanie następujących funkcji:
dopisanie nowego narzędzia na listę, zmiana liczby sztuk narzędzia na liście (w
tym możliwość kompletnego usunięcia narzędzia, gdy liczba sztuk spadnie do
zera), wydanie narzędzia (zapamiętanie godziny i nazwiska pracownika biorącego
narzędzie), zwrot narzędzia, lista wszystkich narzędzi, lista dostępnych
narzędzi, lista wydanych narzędzi.