LEKCJA 14 – PROSTA BAZA DANYCH

 

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 1, in -toplevel-

    db['lista']=lista

  File "C:\Python24\lib\dumbdbm.py", line 160, in __setitem__

    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.

 

Ćwiczenia kontrolne

Ć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.