W niniejszym rozdziale skupiamy się na wykrywaniu i usuwaniu błędów z kodu źródłowego. Jest on rozszerzeniem rozdziału o środowisku wykonawczym i edycyjnym, lecz lektura wymaga znajomości typów danych i instrukcji sterujących. Techniki i umiejętności przedstawione w tym rozdziale są wtórne: nie byłyby potrzebne, gdyby nie ludzka omylność. Dlatego warto je opanować.
W idealnym przypadku algorytm zapisany jakimkolwiek sposobem realizuje zamiar jego twórcy. W praktyce nikt nie jest nieomylny, dlatego mamy do czynienia z różnego rodzaju błędami. Oto ich podstawowa klasyfikacja:
Unikanie i eliminacja błędów są poważnymi wyzwaniami związanymi z budową i opisem schematów przetwarzania. Dla ich realizacji podejmuje się działania na różnych poziomach.
Weryfikacja algorytmu polega na zbadaniu, czy robi on to, co robić powinien z punktu widzenia specyfikacji danych, tzn. czy jest skuteczny. Analiza taka wymaga pracy intelektualnej nad schematem działania algorytmu. Jest to zatem czynność, którą musi wykonywać podmiot myślący, a nie automat.
Testowanie polega na badaniu, czy wyniki podawane przez schemat przetwarzania są takie, jak powinny być, tzn. zgodne z deklarowanym celem działania algorytmu. Dlatego należałoby znać wyniki, jakich oczekuje się dla używanych przez siebie zestawów danych testowych.
Użytkowanie algorytmu polega na przygotowywaniu dla niego danych, zlecaniu komuś (np. automatowi) jego wykonania, i odbieraniu wyników, które następnie można wykorzystywać. Najczęściej użytkuje się nie tyle abstrakcyjną postać algorytmu, co opracowany na jego podstawie program. Użytkowanie programu zawierającego błędy logiczne nie ma sensu, gdyż otrzymywane wyniki są bezwartościowe.
W trakcie użytkowania kodu ujawniają się niewykryte do tej pory błędy. Mogą się też pojawić nowe potrzeby ze strony użytkowników. Dla całokształtu działań mających na celu eliminację błędów oraz rozbudowę i wszelkiego rodzaju usprawnienia, używa się terminu konserwacja algorytmu i/lub realizującego go programu.
Wystarczy jedno użycie edytora programisty, by zauważyć, że jest on wyposażony w funkcje ułatwiające tworzenie i kontrolowanie kodu źródłowego. Dlatego właśnie warto z takich narzędzi korzystać. Zakres i sposób realizacji tych ułatwień jest nieco inny w każdym środowisku. Możliwe więc, że używany przez Ciebie edytor zachowuje się nieco inaczej, niż w poniższym opisie.
Najbardziej typowe funkcje edytorów programisty pomocne przy zarządzaniu kodem źródłowym zostały przedstawione w galerii ilustracji.
Wizualizacja dopasowania nawiasów otwierających i zamykających. Po ustawieniu kursora na nawiasie, odpowiadający mu przeciwległy nawias jest wyróżniany barwą lub krojem pisma.
Wizualizacja elementów składni języka: słów kluczowych, komentarzy, instrukcji, wartości — np. za pomocą barw i kroju pisma.
Rozwijanie i zwijanie elementów blokowych składni (komentarzy, podprogramów, bloków instrukcji podrzędnych). Elementy blokowe mogą być wizualnie ujęte w ramki z przyciskiem umożliwiającym ukrywanie i rozwijanie.
Automatyzacja wcięć kodu źródłowego podczas pisania instrukcji: bieżąca głębokość wcięcia jest utrzymywana po utworzeniu nowego wiersza; po użyciu klawisza BackSpace, kiedy kursor znajduje się na początku instrukcji, następuje powrót do poprzedniej głębokości wcięć.
Zwiększanie i zmniejszanie głębokości wcięcia zaznaczonego bloku, np. za pomocą klawiszy Tab i Shift Tab.
Podpowiedzi słów w trakcie pisania: rozwijanie listy słów kluczowych, funkcji standardowych, dostępnych zmiennych, metod itp.; klawisz Enter lub Tab powoduje wstawienie słowa z listy do tekstu programu.
Podpowiedzi składni poleceń (funkcji, metod, klas) w trakcie tworzenia kodu; klawisz Enter lub Tab powoduje wstawienie słowa z listy do tekstu programu.
Automatyczne kończenie fraz składniowych języka: w Pythonie rzadko stosowane, bo frazy składniowe tego języka, z wyjątkiem nawiasów i cudzysłowów, nie wymagają zakończenia.
Wykrywanie błędów logicznych i eliminowanie ich z kodu źródłowego jest bardzo istotnym etapem tworzenia programu. W procesie tym pomocne są narzędzia pozwalające symulować wykonywanie programu, przerwać je lub wznowić w dowolnej chwili i szczegółowo badać stan pamięci. Narzędzia takie noszą nazwę debuggerów (odpluskwiaczy).
Do najważniejszych operacji dostępnych za pomocą debuggerów należą:
Step over
,
Step into
);
Evaluate
, Modify
)
Na ilustracji poniżej przedstawiono sesję debuggera
Pythona w środowisku PythonWin.
Program rkw.py
jest w niej wykonywany w trybie krokowym
(File/Run/Step-through in the Debugger
, ikonka
biegnącego ludzika na dolnej listwie narzędziowej). Instrukcja z ustawioną
pułapką (File/Debug/Toggle Breakpoint
, ikonka dłoni na górnej
listwie narzędziowej) wyróżniona jest czerwonym znakiem. Instrukcja, która zostanie
wykonana w następnej kolejności (File/Debug/Step
,
File/Debug/Step over
, File/Go
,
ikonki ze strzałkami na górnej listwie narzędziowej) wyróżniona jest żółtą strzałką.
W lewym panelu (Watch
, ikonka okularów na górnej
listwie narzędziowej) mamy wgląd w stan pamięci programu i w wartości wyrażeń wpisanych
do panelu przez użytkownika. W dolnym panelu interaktywna konsola
(Interactive window
, ikonka z symbolem zgłoszenia
interpretera >>> na dolnej listwie narzędziowej) pozwala
na doraźne przeprowadzenie obliczeń pomocniczych, modyfikację stanu zmiennych, itp.
Przy testowaniu programów Pythona można korzystać także z innych debuggerów. Kilka z nich zostało zamieszczonych w spisie oprogramowania. Oferują one równoważne funkcje, jednak mogą się różnić wyglądem okna roboczego i sposobem jego obsługi.
Szerszy wgląd w narzędzia i środowiska programistyczne, pozwalające na tworzenie, uruchamianie i dogłębne testowanie kodu źródłowego oraz na generowanie z niego programów wykonywalnych daje osobna galeria ilustracji.
W kolejnych sekcjach krótko omówimy podstawową metodykę diagnozowania kodu źródłowego. Diagnozowany program musi być poprawny pod względem składniowym; w przeciwnym razie jego uruchomienie nie będzie możliwe.
Sygnalizację błędów składniowych w kodzie źródłowym demonstruje osobna galeria ilustracji.
Domyślnie program jest uruchamiany w trybie ciągłym (non stop mode, go, run), tak samo jak bez użycia debuggera.
W trybie pracy krokowej po wykonaniu każdej instrukcji, ew. każdego wiersza kodu źródłowego, praca jest wstrzymywana. Niektóre debuggery mają kilka trybów pracy krokowej. Różnią się one traktowaniem podprogramów: albo wywołują je w jednym kroku (step over), albo instrukcja po instrukcji (step into).
W nowoczesnych debuggerach wysokiej klasy oferowana jest nawet funkcja kroku wstecz, czyli anulowania skutków ostatnio wykonanej instrukcji (o ile w odniesieniu do pamięci programu jest to możliwe, nie sposób wyobrazić sobie cofnięcia wykonanej operacji systemowej, takiej jak nadpisanie pliku lub przesłanie wydruku do urządzenia).
Przejście z trybu krokowego do trybu ciągłego jest możliwe w dowolnym momencie (go, continue).
Niektóre debuggery mają też funkcję wymuszającą natychmiastowe przejście z trybu ciągłego w tryb krokowy (break). Przy tej metodzie nie sposób przewidzieć, przy której instrukcji program zostanie wstrzymany; zależy to m.in. od chwili, w której zażądano przerwania pracy.
Pułapki (breakpoints) są miejscami w kodzie źródłowym, po dojściu do których wykonywanie programu w trybie ciągłym ma zostać wstrzymane.
Na ogół pułapki są ustawiane za pomocą funkcji menu lub skrótów klawiatury, a ich lokalizacja jest czytelnie oznaczona (np. na marginesie edytora lub przez wyróżnienie wiersza kodu).
Inny wariant ustawiania pułapek wykorzystuje położenie kursora w edytorze kodu źródłowego w chwili uruchomienia programu (go to cursor).
Podgląd zawartości zmiennych i wyrażeń (Watch, ‘czujki’) pozwala wizualnie badać stan pamięci i konfrontować go z zamiarami autora programu. Jest to narzędzie pomocne przy wykrywaniu uporczywych błędów, a także nieocenione w procesie nauki programowania.
W większości debuggerów Pythona konsola interaktywna jest do dyspozycji operatora. Da się z niej korzystać w trybie krokowym lub po przerwaniu trybu ciągłego.
Pierwszy sposób jej wykorzystania wiąże się z kontrolą wartości wyrażeń. Jest to funkcja zbliżona do tej oferowanej przez podgląd pamięci programu.
Druga możliwość użycia konsoli interpretera wiąże się z modyfikacją obiektów programu.
Dotychczas traktowaliśmy błędy jako uciążliwą przypadłość, będącą czy to efektem pomyłek, czy błędnie przygotowanych danych. W praktyce nie da się ani przewidzieć wszystkich przypadków użycia programu, ani wynikających z tego błędów i ich konsekwencji. Błędy są codziennością i trzeba sobie z nimi radzić.
W nowoczesnych językach programowania, w tym w Pythonie, istnieją mechanizmy umożliwiające wykrycie próby niedozwolonej operacji i zapobieżenie jej wykonaniu. Noszą one ogólną nazwę mechanizmów obsługi wyjątków. Nie będziemy wgłębiać się w szczegóły techniczne ich stosowania — wymaga to znacznie szerszych ram czasowych — jednak porównanie dwóch podejść do konkretnego zagadnienia pokaże, w jaki sposób obsługa wyjątków ułatwia tworzenie czytelnego i niezawodnego kodu.
Przyjmijmy, że program ma pobrać od użytkownika nazwę pliku, następnie otworzyć ten plik, odczytać z jego zawartości kolumnę liczb, po czym obliczyć logarytmy tych liczb. Możliwe są następujące sytuacje:
'::\\\\:?:'
)W tradycyjnym ujęciu prosty ciąg czynności
nazwa = podaj nazwę pliku otwórz plik nazwa x = lista liczb odczytanych z pliku zamknij plik y = lista logarytmów liczb z x
uzupełniony o zabezpieczenia na wypadek przewidzianych błędów, mógłby wyglądać następująco:
nazwa = podaj nazwę pliku if (podano nazwę): otwórz plik nazwa if (plik jest otwarty): czytaj zawartość pliku if (zawartość składa się z liczb): utwórz listę liczb x if (wszystkie liczby są dodatnie): utwórz listę logarytmów liczb y zamknij plik
(zauważmy zaburzenie kolejności instrukcji w porównaniu z pierwotnym projektem). Komplikacje narastają w miarę przewidywania coraz to nowych możliwości błędów.
W przypadku zastosowania metod obsługi wyjątków kod znacznie się upraszcza, a co najważniejsze — jego zasadnicza część, wyróżniona kolorem, jest taka sama lub prawie taka sama, jak w pierwotnym projekcie:
try: # wykonujnazwa = podaj nazwę pliku otwórz plik nazwa x = lista liczb odczytanych z pliku zamknij plik y = lista logarytmów liczb z x
except: # zaś w sytuacjach nadzwyczajnych: jeśli nie podano nazwy: wykonaj czynności przewidziane w przypadku tego typu błędu jeśli plik nie istnieje: wykonaj czynności przewidziane w przypadku tego typu błędu jeśli dana nie jest liczbą: wykonaj czynności przewidziane w przypadku tego typu błędu jeśli argument logarytmu nie jest liczbą dodatnią: wykonaj czynności przewidziane w przypadku tego typu błędu przy każdym innym rodzaju wyjątku: przekaż komunikat o nieprzewidzianym błędzie
Fragment oznaczony kolorem zielonym będzie wykonywany zgodnie z następstwem instrukcji. Każda sytuacja nadzwyczajna, która domyślnie spowodowałaby przerwanie działania programu, w tym przypadku jedynie przerwie ciągłość wykonywania tego podstawowego bloku, i spowoduje przeskok do obsługi danego typu wyjątku.
Z obsługą wyjątków w Pythonie
związane jest użycie słów kluczowych:
try
dla oznaczenia podstawowego toku wykonywanej czynności,
except
dla oznaczenia instrukcji postępowania dla poszczególnych rodzajów błędów,
finally
dla oznaczenia końcowych czynności, wykonywanych po (pomyślnym lub nie) przejściu przez cały blok try
,
oraz raise
dla sprowokowania wystąpienia danego typu wyjątku w określonej sytuacji.
Przy tym instrukcja postępowania, z czysto proceduralnej (co i w jakiej kolejności robić) przeradza się częściowo w deklaratywną (co byśmy chcieli, żeby było zrobione).
Kod z obsługą wyjątków można np. umieścić wewnątrz pętli, której zakończenie jest możliwe jedynie w przypadku pomyślnego przejścia przez wszystkie wymagane instrukcje. Na przykład
zrobione = False while not zrobione: try: to, co trzeba zrobić ... zrobione = True except typ błędu: postępowanie na wypadek tego rodzaju błędu ...
Przykład powyższy jest zaledwie szkicem w pseudojęzyku. Zagadnienia obsługi wyjątków wykraczają poza zamierzony zakres tego opracowania.
Dobrym nawykiem jest narzucenie takiego stylu pracy, w którym dbałość o poprawność logiczną jest stałym elementem. Pełne omówienie tego tematu nie jest możliwe w krótkich ramach tego kursu. Ale kilka podstawowych reguł warto znać:
print
i używaj debuggerów do sprawdzania
pośrednich etapów obliczeń. Konfrontuj je z wynikami, jakie uważasz za poprawne.