Rozwiązywanie problemów

Wprowadzenie

Poznaliśmy już coś niecoś Pythona, więc teraz zróbmy sobie małą przerwę i nauczmy posługiwać się tymi wszystkimi mechanizmami, które dotąd poznaliśmy. W tym rozdziale zaprojektujemy i napiszemy nasz pierwszy naprawdę użyteczny program. Nauczysz się w ten sposób, drogi Czytelniku, w jak pisać własne skrypty w Pythonie.

Problem

Na początku sformułujemy nasz problem: Potrzebuję programu, który stworzy kopię zapasową wszystkich moich ważnych plików.

Co prawda jest on dosyć prosty, ale mimo tego potrzebujemy więcej informacji, aby zacząć myśleć o rozwiązaniu. Musimy dokonać analizy problemu. Nie wiemy jeszcze na przykład, w jaki sposób ustalimy, które pliki konkretnie mają zostać zarchiwizowane. W jaki sposób będą one przechowywane? Gdzie?

Kolejnym krokiem po dogłębnej analizie problemu jest zaprojektowanie naszego programu. Najwygodniej jest stworzyć listę cech, jakie powinien posiadać nasz program. Możemy także do tej listy dopisać, w jaki sposób przynajmniej część funkcji będzie zrealizowana. Poniżej zobaczysz listę, która opisuje sposób, w jaki ja chciałbym ten problem rozwiązać. Oczywiście gdybyś to Ty robił tę listę, wyglądałaby ona zapewne trochę inaczej i nie ma w tym nic złego, bo w końcu każdy ma swój własny sposób myślenia i robienia różnych rzeczy.

  1. Pliki i katalogi do zarchiwizowania są zawarte w jednej liście.
  2. Wszystkie kopie zapasowe są przechowywane w głównym katalogu specjalnie do tego przeznaczonym.
  3. Wszystkie pliki z pojedynczej kopii są przechowywane w archiwum zip.
  4. Nazwą pliku kopii zapasowej jest data i godzina jej utworzenia.
  5. Użyjemy standardowego polecenia zip, dostępnego domyślnie w niemal każdej dystrybucji Linuksa bądź innym uniksopodobnym systemie operacyjnym. Użytkownicy systemu Windows mogą zainstalować ten program ze strony projektu GnuWin32 i dodać ścieżkę C:\Program Files\GnuWin32\bin do zmiennej środowiskowej PATH, podobnie jak już to zrobili, aby móc używać polecenia python w wierszu poleceń Windows. Oczywiście możesz użyć dowolnego innego programu do tworzenia archiwów, o ile tylko da się ten program uruchomić z linii poleceń, podając mu odpowiednie argumenty.

Rozwiązanie

Skoro nasz projekt jest już w miarę przemyślany, możemy zabrać się do pisania kodu, czyli implementacji naszego rozwiązania.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: kopia_wer1.py

import os
import time

# 1. Pliki i katalogi do zarchiwizowania są zawarte w jednej liście.
zrodlo = ['"C:\\Moje dokumenty"', 'C:\\Kod']
# Zauważ, że musieliśmy użyć podwójnych cudzysłowów wewnątrz stringów zawierających ścieżki ze spacjami

# 2. Wszystkie kopie zapasowe są przechowywane w głównym katalogu specjalnie do tego przeznaczonym.
katalog_docelowy = 'E:\\Backup' # Pamiętaj, aby zmienić to na coś, co będzie pasować do Twojego systemu

# 3. Wszystkie pliki z pojedynczej kopii są przechowywane w archiwum zip.
# 4. Nazwą pliku kopii zapasowej jest data i godzina jej utworzenia.
plik_docelowy = katalog_docelowy + os.sep + time.strftime('%Y%m%d%H%M%S') + '.zip'

# 5. Użyjemy standardowego polecenia zip, aby utworzyć archiwum.
zip_polecenie = "zip -q -r {0} {1}".format(plik_docelowy, ' '.join(zrodlo))

# Wykonanie kopii zapasowej
if os.system(zip_polecenie) == 0:
    print 'Kopia zapasowa pomyslnie zapisana do pliku', plik_docelowy
else:
    print 'NIEPOWODZENIE podczas tworzenia kopii zapasowej'

Rezultat:

$ python kopia_wer1.py
Kopia zapasowa pomyslnie zapisana do pliku E:\Backup\20080702185040.zip

Znaleźliśmy się wreszcie w fazie testowania, czy nasz program działa prawidłowo. Jeśli jednak nie zachowuje się on tak, jak byśmy tego oczekiwali, musimy zabrać się za jego debugowanie, czyli usuwanie błędów.

Jeśli powyższy program nie działa na Twoim systemie, dodaj polecenie print zip_polecenie tuż przed wywołaniem funkcji os.system i uruchom program. Teraz sprawdź, czy wypisane na wyjście polecenie używające programu zip działa w powłoce (wierszu poleceń, terminalu) Twojego systemie. Jeśli nie, poszukaj informacji, jak poprawnie użyć programu zip i odpowiednio zmodyfikuj program, a jeśli jednak to polecenie działa, to najprawdopodobniej źle przepisałeś treść programu z tej książki.

Jak to działa:

Teraz zobaczysz, jak krok po kroku przekształciliśmy nasz projekt w kod.

Będziemy używać modułów os oraz time, więc na początku programu je importujemy. Następnie zapisujemy ścieżki do ważnych plików i katalogów w liście zrodlo. Zmienna katalog_docelowy zawiera ścieżkę do katalogu, gdzie będą przechowywane wszystkie kopie zapasowe. Zgodnie z naszym projektem, nazwy plików zip będą zawierały aktualną datę i czas, które uzyskujemy za pomocą funkcji time.strftime. Ścieżkę do pliku konstruujemy pamiętając, że będzie on się znajdował w katalogu katalog_docelowy i oczywiście będzie miał rozszerzenie .zip.

Zauważ użycie przez nas zmiennej os.sep — zawiera ona separator specyficzny dla systemu operacyjnego, pod którym program będzie uruchamiany (czyli '\\' w Windows, ':' w Mac OS X, a w systemach linuksowych i wielu innych '/'). Staraj się w przyszłości jak najczęściej używać tej zmiennej, aby Twoje programy były jak najbardziej przenośne, tzn. aby działały na jak największej liczbie różnych systemów.

Funkcja time.strftime jako argument przyjmuje format w postaci stringa, według którego zostanie zwrócona data. %Y oznacza rok, z wiekiem, %m jest zamieniane na miesiąc (jako liczba: od 01 do 12) i tak dalej. Kompletna lista takich specyfikacji jest dostępna w Podręczniku Pythona.

Nazwę docelowego pliku tworzymy przez użycie operatora dodawania, który łączy dwa stringi i zwraca jeden długi. W ten sposób powstaje zmienna zip_polecenie zawierająca polecenie, które zostanie wykonane.

Program (polecenie) zip przyjmuje różne argumenty, dzięki czemu może dla nas wykonać żądaną czynność bez zbędnej interakcji ze strony użytkownika. I tak opcja -q oznacza, że program ma nie wypisywać żadnych informacji na wyjście, to znaczy że ma działać cicho (ang. quietly). Opcja -r nakazuje programowi działaćr**ekursywnie dla katalogów, czyli pakować do archiwum wszystkie podkatalogi i pliki wewnątrz nich. Opcje mogą być dla wygody łączone w skróty, na przykład -qr. Kolejnym argumentem jest nazwa pliku zip, do którego pliki mają być spakowane, a na końcu podajemy przedzielaną spacjami listę plików i katalogów, które mają zostać spakowane. W tym celu używamy funkcji join dla klasy string, którą już potrafimy się posługiwać.

W końcu możemy wykonać skonstruowane przez nas polecenie za pomocą funkcji os.system, która uruchamia podane jej w argumencie polecenie tak, jakbyśmy to my je wykonali w powłoce systemowej. Jeśli wykonanie polecenia zakończy się sukcesem, zwracane jest 0, w przeciwnym wypadku niezerowy numer błędu.

W zależności od powodzenia komendy wypisujemy na ekran stosowną informację o sukcesie bądź niepowodzeniu stworzenia kopii zapasowej.

To wszystko — właśnie sami napisaliśmy skrypt archiwizujący za nas ważne pliki.

Uwaga dla użytkowników Windows
Zamiast znaków specjalnych '\\' możesz używać łańcuchów surowych, na przykład r'C:\Dokumenty' zamiast 'C:\\Dokumenty'. Nigdy tylko nie próbuj używać 'C:\Dokumenty', ponieważ w ten sposób za pomocą znaku ucieczki \ tworzysz nieznany znak specjalny \D.

Skoro więc już mamy działający skrypt, możemy go używać, kiedykolwiek tylko zapragniemy. Użytkownicy Linuksa i innych systemów uniksowych mogą nadać skryptowi prawo do wykonywania, jak już zostało to omówione. To wszystko składa się na fazę eksploatacji bądź wdrażania oprogramowania.

Powyższy program działa prawidłowo, ale pierwsze wersje programów zazwyczaj nie działają tak, jak tego oczekujemy. Przykładowo mogą pojawić się problemy, jeśli popełniliśmy błąd podczas projektowania lub pisania kodu. W takiej sytuacji trzeba odpowiednio zaprojektować program od nowa lub debugować kod, aby znaleźć i poprawić w nim błąd.

Druga wersja

Pierwsza wersja naszego skryptu działa, ale nie oznacza to, że jest on już idealny. Nadal możemy poczynić kilka ulepszeń, aby program działał lepiej w praktyce. Nazywamy to fazą utrzymywania oprogramowania.

Jednym z ulepszeń, które uznałem za użyteczne, jest lepszy system nazewnictwa plików. Aktualny czas może być nazwą plików, które będą przechowywane wewnątrz katalogów zawierających w swojej nazwie aktualną datę. Dzięki temu po pierwsze pliki będą ułożone w pewnej hierarchii, co pozwoli nam na łatwiejsze nimi zarządzanie. Drugą zaletą takiego rozwiązania jest fakt, że nazwy plików będą znacznie krótsze. Ponadto dzięki takiemu mechanizmowi łatwo będzie sprawdzić, czy każdego dnia stworzyliśmy kopię zapasową, bo przecież katalog dla danego dnia będzie tylko wtedy zakładany.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: kopia_wer2.py

import os
import time

# 1. Pliki i katalogi do zarchiwizowania są zawarte w jednej liście.
zrodlo = ['"C:\\Moje dokumenty"', 'C:\\Kod']
# Zauważ, że musieliśmy użyć podwójnych cudzysłowów wewnątrz stringów zawierających ścieżki ze spacjami

# 2. Wszystkie kopie zapasowe są przechowywane w głównym katalogu specjalnie do tego przeznaczonym.
katalog_docelowy = 'E:\\Backup' # Pamiętaj, aby zmienić to na coś, co będzie pasować do Twojego systemu

# 3. Wszystkie pliki z pojedynczej kopii są przechowywane w archiwum zip.
# 4. Nazwą podkatalogu wewnątrz katalogu głównego jest aktualna data.
dzisiaj = katalog_docelowy + os.sep + time.strftime('%Y%m%d')
# Nazwą archiwum zip jest aktualny czas
teraz = time.strftime('%H%M%S')

# Tworzymy podkatalog, o ile jeszcze nie istnieje
if not os.path.exists(dzisiaj):
    os.mkdir(dzisiaj) # make directory
    print 'Utworzono katalog', dzisiaj

# Nazwa pliku zip
plik_docelowy = dzisiaj + os.sep + teraz + '.zip'

# 5. Użyjemy standardowego polecenia zip, aby utworzyć archiwum.
zip_polecenie = "zip -q -r {0} {1}".format(plik_docelowy, ' '.join(zrodlo))

# Wykonanie kopii zapasowej
if os.system(zip_polecenie) == 0:
    print 'Kopia zapasowa pomyslnie zapisana do pliku', plik_docelowy
else:
    print 'NIEPOWODZENIE podczas tworzenia kopii zapasowej'

Rezultat:

$ python kopia_wer2.py
Utworzono katalog E:\Backup\20080702
Kopia zapasowa pomyslnie zapisana do pliku E:\Backup\20080702\202311.zip
$ python kopia_wer2.py
Kopia zapasowa pomyslnie zapisana do pliku E:\Backup\20080702\202325.zip

Jak to działa:

Większość kodu pozostała niezmieniona. Dodaliśmy sprawdzenie za pomocą funkcji os.path.exists, czy wewnątrz katalogu z kopiami zapasowymi istnieje katalog o nazwie z bieżącą datą. Jeśli nie, posługujemy się funkcją os.mkdir do jego utworzenia.

Trzecia wersja

Druga wersja działa nieźle, kiedy wykonuję dużo kopii, ale jeśli jest ich naprawdę dużo, trudno jest mi rozróżnić, po co poszczególne kopie w ogóle były tworzone. Przykładowo, jeśli poczyniłem jakieś znaczące zmiany w programie czy w prezentacji, wolałbym zawrzeć opis tych zmian w nazwie pliku zip.

Uwaga
Poniższy program nie działa, więc nie martw się, jeśli dostaniesz komunikat o błędzie. Niech ten przykład będzie dla Ciebie lekcją.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: kopia_wer3.py

import os
import time

# 1. Pliki i katalogi do zarchiwizowania są zawarte w jednej liście.
zrodlo = ['"C:\\Moje dokumenty"', 'C:\\Kod']
# Zauważ, że musieliśmy użyć podwójnych cudzysłowów wewnątrz stringów zawierających ścieżki ze spacjami

# 2. Wszystkie kopie zapasowe są przechowywane w głównym katalogu specjalnie do tego przeznaczonym.
katalog_docelowy = 'E:\\Backup' # Pamiętaj, aby zmienić to na coś, co będzie pasować do Twojego systemu

# 3. Wszystkie pliki z pojedynczej kopii są przechowywane w archiwum zip.
# 4. Nazwą podkatalogu wewnątrz katalogu głównego jest aktualna data.
dzisiaj = katalog_docelowy + os.sep + time.strftime('%Y%m%d')
# Nazwą archiwum zip jest aktualny czas
teraz = time.strftime('%H%M%S')

# Pobierz komentarz od użytkownika w celu ustalenia nazwy pliku zip
komentarz = raw_input('Wprowadz komentarz --> ')
if len(komentarz) == 0: # sprawdź, czy cokolwiek zostało w ogóle wprowadzone
    plik_docelowy = dzisiaj + os.sep + teraz + '.zip'
else:
    plik_docelowy = dzisiaj + os.sep + teraz + '_' +
        komentarz.replace(' ', '_') + '.zip'

# Tworzymy podkatalog, o ile jeszcze nie istnieje
if not os.path.exists(dzisiaj):
    os.mkdir(dzisiaj) # make directory
    print 'Utworzono katalog', dzisiaj

# 5. Użyjemy standardowego polecenia zip, aby utworzyć archiwum.
zip_polecenie = "zip -q -r {0} {1}".format(plik_docelowy, ' '.join(zrodlo))

# Wykonanie kopii zapasowej
if os.system(zip_polecenie) == 0:
    print 'Kopia zapasowa pomyslnie zapisana do pliku', plik_docelowy
else:
    print 'NIEPOWODZENIE podczas tworzenia kopii zapasowej'

Rezultat:

$ python kopia_wer3.py
  File "kopia_wer3.py", line 28
    plik_docelowy = dzisiaj + os.sep + teraz + '_' +
                                                   ^
SyntaxError: invalid syntax

Jak to (nie) działa:

Ten program nie działa! Python informuje nas o błędzie składni (ang. syntax), co oznacza że nasz skrypt nie jest poprawnie zbudowanym plikiem Pythona. Kiedy jesteśmy świadkami błędu wykonywania skryptu Pythona, interpreter podaje nam także dokładne miejsce, gdzie wystąpił błąd. Debugowanie programu zaczynamy właśnie od tej linii.

Przyglądając się uważnie, zauważamy, że pojedyncza linia logiczna została podzielona na dwie linie fizyczne, ale nie zaznaczyliśmy tego w odpowiedni sposób. W praktyce interpreter napotkał operator dodawania (+) bez operandu z prawej strony i nie wiedział, co ma z tym począć, więc poinformował nas o błędzie w programie. Pamiętasz oczywiście, że aby dać znać Pythonowi, że dana linia logiczna rozciąga się dalej na następną linię fizyczną, na końcu tej pierwszej linii fizycznej umieszczamy ukośnik wsteczny \. Poprawiamy to w naszym programie. Poprawianie błędów w kodzie nazywamy usuwaniem błędów (po angielsku bug fixing).

Czwarta wersja

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: kopia_wer4.py

import os
import time

# 1. Pliki i katalogi do zarchiwizowania są zawarte w jednej liście.
zrodlo = ['"C:\\Moje dokumenty"', 'C:\\Kod']
# Zauważ, że musieliśmy użyć podwójnych cudzysłowów wewnątrz stringów zawierających ścieżki ze spacjami

# 2. Wszystkie kopie zapasowe są przechowywane w głównym katalogu specjalnie do tego przeznaczonym.
katalog_docelowy = 'E:\\Backup' # Pamiętaj, aby zmienić to na coś, co będzie pasować do Twojego systemu

# 3. Wszystkie pliki z pojedynczej kopii są przechowywane w archiwum zip.
# 4. Nazwą podkatalogu wewnątrz katalogu głównego jest aktualna data.
dzisiaj = katalog_docelowy + os.sep + time.strftime('%Y%m%d')
# Nazwą archiwum zip jest aktualny czas
teraz = time.strftime('%H%M%S')

# Pobierz komentarz od użytkownika w celu ustalenia nazwy pliku zip
komentarz = raw_input('Wprowadz komentarz --> ')
if len(komentarz) == 0: # sprawdź, czy cokolwiek zostało w ogóle wprowadzone
    plik_docelowy = dzisiaj + os.sep + teraz + '.zip'
else:
    plik_docelowy = dzisiaj + os.sep + teraz + '_' + \
        komentarz.replace(' ', '_') + '.zip'

# Tworzymy podkatalog, o ile jeszcze nie istnieje
if not os.path.exists(dzisiaj):
    os.mkdir(dzisiaj) # make directory
    print 'Utworzono katalog', dzisiaj

# 5. Użyjemy standardowego polecenia zip, aby utworzyć archiwum.
zip_polecenie = "zip -q -r {0} {1}".format(plik_docelowy, ' '.join(zrodlo))

# Wykonanie kopii zapasowej
if os.system(zip_polecenie) == 0:
    print 'Kopia zapasowa pomyslnie zapisana do pliku', plik_docelowy
else:
    print 'NIEPOWODZENIE podczas tworzenia kopii zapasowej'

Rezultat:

$ python kopia_wer4.py
Wprowadz komentarz --> dodano nowe przyklady
Kopia zapasowa pomyslnie zapisana do pliku E:\Backup\20080702\202836_dodano_nowe_przyklady.zip

$ python kopia_wer4.py
Wprowadz komentarz --> 
Kopia zapasowa pomyslnie zapisana do pliku E:\Backup\200807022\202839.zip

Jak to działa:

Nasz program znowu działa! Omówmy teraz zmiany wprowadzone w wersji trzeciej. Pobieramy komentarz od użytkownika za pomocą funkcji raw_input, a następnie sprawdzamy, czy rzeczywiście coś zostało wprowadzone. Możemy tego dokonać dzięki funkcji len, która zwraca nam długość łańcucha. Jeśli więc użytkownik po prostu wcisnął Enter bez wprowadzania czegokolwiek (bo na przykład była to rutynowa kopia, bez jakichś istotnych zmian), program zachowuje się tak samo, jak poprzednia wersja.

Jeśli jednak użytkownik wprowadził komentarz, jest on dodawany do nazwy pliku zip tuż przed rozszerzeniem. Spacje zamieniamy na podkreślniki, ponieważ zarządzanie plikami o nazwach bez spacji jest o wiele łatwiejsze.

Więcej ulepszeń

Czwarta wersja jest zadowalająco sprawnie działającym skryptem dla większości użytkowników, ale oczywiście zawsze pozostaje miejsce dla kolejnych usprawnień. Przykładowo, można pomyśleć o implementacji stopnia gadatliwości (ang. verbosity) programu, którą można by kontrolować przez opcję -v podaną jako argument wywołania programu. Opcja ta dawałaby programowi znać, aby wypisywał więcej informacji na wyjście. Argumenty wywołania programu znajdują się w liście sys.argv.

Innym możliwym ulepszeniem byłoby pozwolenie na podanie przez użytkownika dodatkowych plików lub katalogów jako argumentów wywołania programu. Można to wygodnie zrealizować poprzez użycie metody extend z klasy list na naszej zmiennej zrodlo.

Największą wadą naszego programu jest używanie przez niego funkcji os.system do tworzenia archiwów. O wiele lepiej byłoby skorzystać z wbudowanych w Pythona modułów zipfile bądź tarfile. Są one częścią biblioteki standardowej, więc nasz skrypt nie musiałby więcej polegać na istnieniu w systemie dodatkowych zewnętrznych programów.

Mimo tego w pełni świadomie używałem sposobu z os.system w powyższych przykładach — głównie w celach edukacyjnych, aby każdy mógł z łatwością zrozumieć ten kod, ale żeby był on jednocześnie użyteczny.

Możesz teraz spróbować napisać piątą wersję, która będzie korzystała z modułu zipfile zamiast z wywołania os.system.

Proces opracowywania oprogramowania

Przeszliśmy przez różne fazy procesu tworzenia oprogramowania. Można je podsumować w ten sposób:

  1. Co? (Analiza)
  2. Jak? (Projekt)
  3. Zrób to (Implementacja)
  4. Testuj (Testowanie i debugowanie)
  5. Używaj (Eksploatacja i wdrażanie)
  6. Utrzymuj (Ulepszanie)

Polecam zawsze pisać programy według procedury, którą właśnie poznaliśmy: Zanalizuj problem i zaprojektuj jego rozwiązanie. Zaimplementuj proste rozwiązanie tak, aby można było je łatwo rozwijać. Przetestuj Twój program i usuń błędy. Używaj go przez jakiś czas, aby upewnić się, że działa, jak należy. Teraz możesz zacząć dodawać kolejne funkcjonalności i powtarzać cykl <Zrób to>–<Testuj>–<Używaj> aż do uzyskania spodziewanego rezultatu. Zapamiętaj sobie, że programy pisze się etapami — nie można zrobić wszystkiego dobrze naraz.

Podsumowanie

Zobaczyliśmy, w jaki sposób pisać nasze własne programy w Pythonie oraz z jakich faz ich tworzenie się składa. Mam nadzieję, że szybko nauczysz się pisać programy w Pythonie do rozwiązywania różnych problemów tak, jak to zrobiliśmy wspólnie w tym rozdziale. To bardzo pomoże Ci w zaprzyjaźnieniu się z tym językiem.

Teraz porozmawiamy o programowaniu orientowanym obiektowo.

results matching ""

    No results matching ""