TI/Wyjątki
Spis treści
Informowanie o błędach
Czasami w trakcie wykonywania operacji przez program występuje sytuacja, która uniemożliwia jej wykonanie. Typowe sytuacje wymagające specjalnych reakcji programu to:
- błąd w korzystaniu z zewnętrznych zasobów np.:
- program miał za zadanie otworzyć plik o określonej nazwie, a okazało się, że takiego pliku nie ma
- program miał pobrać stronę www ze zdalnego serwera, a nie udaje się nawiązać połączenia
- niemożność poprawnego wykonania
- program miał za zadanie znaleźć długość ósmej linijki w pliku, a plik ma tylko siedem linijek
- program miał obliczyć pierwiastek liczby podanej przez użytkownika, a podana liczba jest ujemna (i program nie jest świadomy istnienia liczb zespolonych)
- pogwałcenie wewnętrznej spójności programu: w miarę jak program się rozrasta rośnie prawdopodobieństwo, że wywołamy funkcję w sposób niezgodny z jej wymaganiami
- np. funkcja znajdująca liczbę w tablicy oczekuje tablicy posortowanej, a wywołamy ją z tablicą nieposortowaną
- np. zmienna opisująca promień koła ma wartość ujemną
Problem obsługi sytuacji wyjątkowych jest dodatkowo skomplikowany przez fakt, że często sytuacja wymagająca specjalnego obsłużenia występuje w funkcji, która została wywołana przez inną funkcję, która została wywołana przez inną funkcję,... Niemniej, właściwym miejscem na reakcję, często nie jest miejsce stwierdzenia błędu, gdzieś głęboko w wywołaniu funkcji wykonującej jedno specjalizowane zadanie, a wręcz przeciwnie, bardzo płytko, w miejscu sterowania całym wykonaniem programu.
Jako przykład rozważmy podany wcześniej błąd otwarcia pliku. Jeśli taki błąd zostanie napotkany przez program, którego zadaniem było odczytać z dysku i wyświetlić jedno zdjęcie, to właściwą reakcją będzie wypisanie informacji o błędzie i zakończenie działania programu. Jeśli taki błąd wystąpi w działaniu przeglądarki ściągającej pliki ze zdalnego serwera i przechowującej tymczasowo pliki na dysku, to właściwą reakcją będzie ponowne ściągnięcie zdjęcia ze zdalnego serwera, bez informowania użytkownika lub przerywania pracy.
Potrzebny jest zatem mechanizm przekazywania informacji o błędzie wzdłuż łańcucha wywołań funkcji. Jednym z takich mechanizmów są tzw. wyjątki.
Jak działają wyjątki w Pythonie — rzucanie
Zasygnalizowanie w programie sytuacji nietypowej, która nie powinna się normalnie wydarzyć (czyli sytuacji wyjątkowej) nazywa się „rzuceniem” lub „podniesieniem” wyjątku. Informacje precyzujące zaistniałą sytuację są zapamiętywane. W tym celu zostaje stworzony obiekt nazywany wyjątkiem (exception). Dzieje się to wszystko z wykorzystaniem instrukcji raise:
raise <typ-wyjątku>(argumenty precyzujące sytuację)
Na przykład żeby zasygnalizować, że funkcja została wywołana z nieodpowiednim argumentem (np. liczbą ujemną gdy oczekiwaliśmy dodatniej), można rzucić wyjątek typu ValueError (zgodnie z konwencją używany w takiej sytuacji).
raise ValueError('liczba ujemna')
Wyjątek może też zostać rzucony nie przez program, a przez samego Pythona (interpreter programu). Np. gdy program spróbuje wykonać niedozwolone dzielenie przez 0, zostaje automatycznie rzucony wyjątek typu ZeroDivisionError.
Jeśli w czasie wykonywania pewnej funkcji zostanie rzucony wyjątek (niezależnie czy explicite, czy automatycznie), to wykonywanie tej funkcji zostanie przerwane i program powróci do miejsca, z którego ta funkcja została wywołana tzn. do funkcji o jeden poziom wyżej. Wykonywanie kolejnych funkcji jest przerywane, aż dojdziemy do najwyższego poziomu. Tam, jako domyślna reakcja na błąd, zostaje wypisana informacja o wystąpieniu wyjątku (jego typ, miejsce wystąpienia, jak też dodatkowe informacje) i program kończy działanie.
Obsługa wyjątków w Pythonie — chwytanie
Domyślna reakcja Pythona na błąd nie zawsze jest pożądana. Jak wynika z przykładu z przeglądarką internetową, czasami właściwą reakcją nie jest przerwanie działania, ale podjęcie działań zaradczych (załadowanie zdjęcia z oryginalnego serwera) i kontynuowanie wykonywania programu. Mechanizm kaskadowego przerywania funkcji (i w konsekwencji całego programu) w momencie wystąpienia wyjątku może być kontrolowany. Służy do tego konstrukcja try...except. W bloku programu pomiędzy try: a except wpisujemy fragment kodu, który może rzucić wyjątek. W bloku programu występującym po except: opisujemy co należy zrobić jeśli pojawi się konkretny typ wyjątku. Podstawowa składnia jest następująca:
try:
<operacje-mogące-rzucić-wyjątek>
except <typ-wyjątku> as <nazwa-zmiennej>:
<operacje-zaradcze>
W przykładzie z przeglądarką internetową:
def wyswietl_obrazek(nazwa):
try:
obrazek = wczytaj_z_przechowalni(nazwa)
except BladOdczytuZPrzechowalni as opis:
zapisz_do_dziennika('nie udało się odczytać z przechowalni:' + nazwa, opis)
obrazek = wczytaj_z_serwera(nazwa)
wyswietl(obrazek)
Jeśli funkcja wczytaj_z_przechowalni() rzuci wyjątek typu BladOdczytuZPrzechowalni, jesteśmy przygotowani na jego obsługę i wywołujemy wczytaj_z_serwera(). Jeśli operacja wczytaj_z_przechowalni() powiedzie się, to wczytaj_z_serwera() nie zostanie użyte. Niezależnie od tego którym sposobem uzyskaliśmy obrazek, zostaje on w końcu wyświetlony przy użyciu funkcji wyswietl(). Jak już wspominaliśmy, wyjątek jest obiektem. W konstrukcji except <typ-wyjątku> as <nazwa-zmiennej>: wiążemy ten przechwycony obiekt z nazwą nazwa-zmiennej. Dzięki temu możemy z tym obiektem coś zrobić. W naszym przykładzie funkcja zapisz_do_dziennika() jako jeden z argumentów dostaje przechwycony wyjątek aby odnotować fakt i okoliczności jego wystąpienia w pliku dziennika.
Rzucanie wyjątków jest faktycznie bardzo proste i zostało powyżej opisane w sposób w miarę kompletny. Natomiast obsługa wyjątków jest bardziej rozbudowana i dopuszcza następujące typy reakcji:
- Po pierwsze, możemy chwytać na raz wiele różnych typów wyjątków i obsługiwać je w ten sam sposób.
- Po drugie, możemy chwytać wiele różnych typów wyjątków i obsługiwać je w różny sposób.
- Po trzecie, możemy chwytać wyjątki wszystkich typów.
- Po czwarte, możemy napisać kod, który zostanie wykonany jeśli nie został rzucony wyjątek, ale poza obszarem łapania wyjątków.
- Po piąte, możemy napisać kod który zostanie wykonany niezależnie od tego czy program wykonał się normalnie czy został rzucony wyjątek.
[przypis: o BaseException i przyjaciołach]
try:
<operacje-mogące-rzucić-wyjątek>
except <typ-wyjątku> as <nazwa-zmiennej>:
# po pierwsze
<operacje-zaradcze>
# Tutaj przechwytujemy wyjątek jednego określonego typu.
# Istnieje też starsza notacja (Python <= 2.5 ?), gdzie
# zamiast 'as' używa się przecinka. Jej wadą mniejsza
# ekspresyjność i możliwość pomylenia sytuacji gdy
# chcielibyśmy przechwycić dwa różne typy wyjątków (patrz
# poniżej) bez zachowywania wyjątku do zmiennej, z sytuacją
# taką jak tutaj, gdzie chcemy przechwycić wyjątek określonego
# typu i zachować go do zmiennej.
except <typ-wyjątku-1>, <typ-wyjątku-2> as <nazwa-zmiennej>:
# po drugie
<operacje-zaradcze>
# Tutaj przechwytujemy wyjątek typu typ-wyjątku-1
# lub typ-wyjątku-2.
except Exception:
# po trzecie
<operacje-zaradcze>
# Tutaj przechwytujemy wyjątek każdego podtypu Exception.
else:
# po czwarte
<operacje-wykonywane-w-przypadku-powodzenia>
# Tutaj należy wpisać kod który jest kontynuacją
# <operacji-mogących-rzucić-wyjątek>. Napisanie go w tym miejscu
# powoduje, że wyjątek rzucony tutaj nie zostanie złapany.
finally:
# po piąte
<operacje-wykonywane-zawsze>
# Tutaj należy wpisać działania takie jak zamknięcie plików
# i zwolnienie blokad, które muszą być wykonane niezależnie
# od tego czy operacja się powiodła czy też nie.
# Innym mechanizmem, który może często zastąpić taki blok
# else, jest konstrukcja 'with'.
W wypadku gdy wykorzystamy gołe except:, powinniśmy zachować dużą dozę ostrożności. W ten sposób przechwycimy np. wyjątki typu ValueError związane z niewłaściwym wywołaniem funkcji gdzieś w kodzie. Ale przechwycimy także wszystkie inne wyjątki, nawet takie których się nie spodziewaliśmy. W tym miejscu często programiści wypisują komunikat o błędzie, a następnie ponownie rzucają ten sam wyjątek aby był dostępny dla kolejnych w hierarchii funkcji (zob. przykład w podręczniku Pythona).
Przykład wykonywalny:
# -*- coding: utf-8 -*-
# plik exc1.py
import random, time
try:
print 'Zaczynam obliczenia... ',
time.sleep(3 * random.random())
print 'obliczenia trwają... ',
wybor = random.randrange(0, 5)
if wybor == 0:
print 'wykonamy dzielenie przez 0'
print 1 / 0
print 'udało się :)'
elif wybor == 1:
print 'rzucimy wyjątek dzielenia przez zero samemu'
raise ZeroDivisionError('jak mogłaś?')
print 'znowu się udało :)'
elif wybor == 2:
print 'rzucimy jeszcze inny wyjątek'
raise ValueError('wylosowałeś krótką słomkę')
print 'udało się ponownie :)'
else:
print '...obliczenia zakończone!'
except ValueError as e:
print "Wiem co było źle podczas obliczeń:", e
raise
except ArithmeticError as e: # obejmuje ZeroDivisionError
print "Coś było źle podczas obliczeń."
import sys
print "Napotkałem taki błąd:", sys.exc_info()[1]
raise # użycie tej instrukcji wytwarza sytuację wyjątkową taką,
# jaka zdarzyła się ostatnio
# czyli rzuca wyjątek taki, jaki właśnie przechwyciliśmy
else:
print "Obliczenia przebiegły pomyślnie."
finally:
print "Koniec bloku try-except-else-finally."
print "What's next?"
Jeśli zachowamy ten przykład do pliku exc1.py to możemy wykonywać go wielokrotnie i patrzeć na wypisywane komunikaty. W szczególności te zakończone śmieszkiem nigdy nie zostaną wypisane :). Z drugiej strony komunikat o końcu bloku zostanie wypisany zawsze.
Hierarchia wyjątków
Inne sposoby
Nie wszystkie języki programowania mają mechanizm wyjątków, nie zawsze też wyjątki są właściwym sposobem obsługi błędów. W zależności od sytuacji wykorzystywane jest parę mechanizmów które warto znać. Są one pokrótce opisane poniżej.
Zwracanie zarezerwowanej wartości
Ustalamy, że pewna szczególna wartość oznacza błąd. Np. w przypadku wczytywaniu obrazków, wynik poprawny jest jakimś obiektem. Możemy wobec tego umówić się, że wynik None oznacza błąd — niemożność wczytania obrazka.
Po wykonaniu operacji mogącej zwrócić wartość szczególną aby zasygnalizować błąd, trzeba sprawdzić czy uzyskaliśmy normalny wynik, czy też nie.
def wyswietl_obrazek(nazwa):
obrazek = wczytaj_z_przechowalni(nazwa)
if obrazek is None:
zapisz_do_dziennika('nie udało się odczytać z przechowalni:' + nazwa)
obrazek = wczytaj_z_serwera(nazwa)
wyswietl(obrazek)
Zwracanie wielu zarezerwowanych wartości
def wyswietl_obrazek(nazwa):
obrazek = wczytaj_z_przechowalni(nazwa)
if obrazek.kod == PUSTY_OBRAZEK:
zapisz_do_dziennika('obrazka nie ma w przechowalni:' + nazwa)
obrazek = wczytaj_z_serwera(nazwa)
elif obrazek.kod == ZLY_OBRAZEK:
zapisz_do_dziennika('obrazek z przechowalni jest niepoprawny:' + nazwa)
obrazek = wczytaj_z_serwera(nazwa)
wyswietl(obrazek)
Ten typ zwracania informacji o błędzie jest używany zwłaszcza wtedy, gdy jest potrzebne dużo możliwych wartości świadczących o niepowodzeniu, a tylko jedna świadcząca o powodzeniu operacji. Powłoka UNIXowa używa konwencji takiej, że programy zwracają 0 jeśli udało im się wykonać poprawnie, a każda inna wartość oznacza błąd. Możliwych błędów jest dużo: brak pliku na którym miała być wykonana operacja, brak pozwolenia, przerwanie operacji przez użytkownika, ...
Zwracanie jednej zarezerwowanej wartości i przechowywanie informacji w zmiennej globalnej
Najbardziej znane użycie tego mechanizmu to zmienna errno używana przy wywoływaniu funkcji systemowych w języku C (takich jak open do otwierania plików czy execve do wywołania innego programu). W wypadku niepowodzenia, wywołanie funkcji zwraca szczególną wartość (–1), a do globalnej zmiennej errno (od numer błędu, ang. error number) zostaje zapisany kod błędu. Program może następnie zajrzeć do tablicy opisów błędów i wydrukować komunikat dla użytkownika.
Wadą tej metody jest to, że zmienna errno jest jedna. Bezpośrednio po wywołaniu funkcji, przed wywołaniem jakiejkolwiek funkcji systemowej, musimy ją zapisać w inne miejsce, tak by nie uległa nadpisaniu w przypadku kolejnego błędu. Generalnie taka obsługa błędów jest bardzo natrętna — po każdym wywołaniu funkcji systemowej musi być parę linijek sprawdzających, czy wartość zwrócona przez funkcję nie świadczy o błędzie, i ewentualnie jaki jest to błąd.
Natychmiastowe kończenie programu
Takie zachowanie — wyświetlenie komunikatu i zakończeniu programu — jest bardzo złym wyjściem gdy zostanie użyte w bibliotece lub kodzie który może być wywołany z więcej niż jednego miejsca. Wynika to z tego, że takie „zaszycie” zachowania w miejscu wykrycia błędu silnie ogranicza możliwości użycia danej funkcji.
Niemniej, jeśli piszemy program od początku do końca i nie zależy nam na ponownym wykorzystaniu tego kodu, to jest to rozwiązanie proste, zwięzłe i szybkie.
def wyswietl_obrazek(nazwa):
"Odczytaj obrazek z dysku i wyświetl. Niepowodzenie kończy program."
obrazek = wczytaj_z_dysku(nazwa)
if obrazek is None:
import sys
print 'nie udało się odczytać:' + nazwa)
sys.exit(1)
wyswietl(obrazek)
W tym przykładzie użyte są dwie konwencje — funkcja wczytaj_z_dysku w przypadku błędu zwraca wartość szczególną (None), natomiast funkcja wywołująca wyswietl_obrazek w reakcji na ten błąd kończy program.
Zastosowanie tego sposobu obsługi błędów ma jeszcze jeden aspekt, który w praktyce okazuje się kłopotliwy, niezależnie od tego, czy zakończeniu programu jest właściwym wyjściem. Przed zakończeniem programu informujemy wypisując komunikat o błędzie na standardowe wyjście. Co się stanie jeśli w pewnym momencie dodamy interfejs graficzny do naszego programu który pozwoli na wywołanie programu nie z konsoli, ale z graficznego menu? Komunikaty wypisywane na konsolę ulegną zagubieniu, bo w sytuacji użycia graficznego interfejsu użytkownika (GUI), właściwym mechanizmem komunikacji z użytkownikiem jest wyświetlenie okienka. Programista dodający GUI musiałby przejść program i zmienić wszystkie miejsca gdzie print zostaje użyte do wypisania błędu, lub też spowodować, że wszystkie użycia print prowadzą do wyświetlenia komunikatu również w GUI.
Wywoływanie funkcji do obsługi błędów
Bardzo ogólnym mechanizmem obsługi błędów jest oddelegowanie obsługi z powrotem do strony wywołującej. W momencie wywołania funkcji bibliotecznej, jako parametr podajemy funkcję (z ang. handler), która zostanie wywołana w wypadku wystąpienia błędu. Decyzja o sposobie obsługi błędu zostaje wtedy podjęta wewnątrz funkcji handler — np. nie ma żadnych ograniczeń na to jak (i czy) komunikat o błędzie będzie wypisany. Funkcja handler zwraca kod, na podstawie którego działanie funkcji w której wystąpił błąd jest kontynuowane lub przerywane.
Silnym plusem jest tutaj możliwość kontynuowania obliczeń. Silnym minusem jest inwazyjność tej metody i konieczność napisania wyspecjalizowanej funkcji do obsługi błędów.
Jako przykład mogą posłużyć powiązane funkcje numpy.seterr i numpy.seterrcall. Pierwsza z nich może być użyta do ustawienia jako obsługę błędów wywołania funkcji zarejestrowanej przy pomocy drugiej z nich. Funkcja zarejestrowana jako obsługa błędów otrzymuje w momencie wywołania informację o błędzie i może wypisać komunikat i ewentualnie rzucić wyjątek. Jeśli wyjątek nie zostanie rzucony, obliczenia są kontynuowane.
Wyjątki kontra inne mechanizmy błędów
Python jest językiem w którym mechanizm wyjątków jest szczególnie szeroko wykorzystywany. Wynika to po części z tego, że konstrukcja try...except jest wydajna, tak że nie ma sensu (z punktu szybkości działania programu), prewencyjnie sprawdzać czy jakiś warunek jest spełniony, by uniknąć przechwytywania wyjątku. Jest to ujęte zwięźle w zaleceniu dla pythonistów
Easier to ask for forgiveness than permission
Rozpowszechnione użycie wyjątków ma też tę zaletę, że pominięcie obsługi błędów prowadzi, w przypadku niepoprawnego działania programu, do jego szybkiego zakończenia i wypisania ciągu wywołań funkcji w programie (ang. stack trace). Nie jest to może rozwiązanie najbardziej eleganckie, często też komunikat jest niezrozumiały dla nie-programistów, ale generalnie jest to znacznie lepsze wyjście niż dalsze wykonywanie programu.
Jak każdy ogólny mechanizm, wyjątki nie są panaceum, tylko jednym z mechanizmów do użycia w połączeniu z innymi w zależności od sytuacji. Program przeznaczony dla końcowego użytkownika powinien gdzieś na wysokim poziomie zawierać blok try...except przechwytujący większość wyjątków. Użytkownik powinien zostać poinformowany o zaistniałej sytuacji w możliwie łagodnych słowach. Następnie powinien zostać użyty inny z wymienionych mechanizmów, zapewne po prostu zakończenie programu.
Podobnie jeśli mamy program wykonujący długotrwałe obliczenia numeryczne, i w jednym z 10000 punktów siatki obliczenia zakończą się rzuceniem wyjątku, to zakończenie działania programu zapewne nie jest rozwiązaniem preferowanym przez użytkownika. Należy zapewne zapisać wartość szczególną (NaN ?), wypisać komunikat, i kontynuować obliczenia.
Przykłady
Przechowywanie wyniku długotrwałych obliczeń
Załóżmy, że chcemy w celu optymalizacji wykonywać funkcję obliczeniową tylko raz dla każdego zestawu argumentów.
def _funkcja(argument):
"Ta funkcja symuluje długotrwałe, trudne symuluacje."
import time, random
time.sleep(3)
wynik = random.random()
return wynik
_cache = {}
def funkcja(argument):
try:
return _cache[argument]
except KeyError:
wynik = _cache[argument] = _funkcja(argument)
return wynik
Tutaj przedrostek _, zgodnie z powszechnie przyjętą w Pythonie konwencją, oznacza funkcję lub zmienną prywatną — element implementacji, do którego nie należy odwoływać się spoza tego modułu. Interfejs publiczny to funkcja funkcja.
Zaglądanie do słownika
Załóżmy, że chcemy zwrócić wartość ze słownika, jeśli tam jest, a w przeciwnym wypadku obliczyć domyślną wartość dla danego klucza.
Tak nie należy (wersja LOOK BEFORE YOU LEAP — spójrz zanim skoczysz):
if key in dictionary:
return dictionary[key]
else:
return compute_default_value_for(key)
To, że tak nie należy, wynika z faktu, że jeśli słownik jest wystarczająco duży, to najbardziej pracochłonna operacja zostanie wykonana dwa razy. Tą operacją jest przeszukanie słownika, by znaleźć odpowiedni klucz — niezależnie czy się uda go znaleźć, czy nie, zajmuje to tyle samo czasu.
Należy po prostu spróbować (wersja EASIER TO ASK FOR FORGIVENESS THAN PERMISSION — łatwiej prosić o wybaczenie niż o pozwolenie):
try:
return dictionary[key]
except KeyError:
return compute_default_value_for(key)
Dostęp do pliku
Załóżmy, że napisaliśmy prosty programik liczący linijki komentarza (zaczynające się od znaku #) w pliku.
# komentarze.py
import sys
file = open(sys.argv[1])
count = 0
for line in file:
count += line.startswith('#')
print 'liczba linijek z komentarzem =', count
Czy ten program ma jakąkolwiek obsługę błędów? Explicite nie, ale dzięki temu, że funkcje wbudowane w Pythona (funkcje biblioteczne) rzucają wyjątki w przypadku niepowodzenia, program zachowa się poprawnie w przypadku wystąpienia różnego rodzaju sytuacji nieprzewidzianych. Nawet jeśli komunikat nie będzie doszlifowany, to będzie zawierał informacje pozwalające (programiście) na rozwiązanie problemu. Zobaczmy:
# wywołanie poprawne
$ python komentarze.py uni1.py
liczba linijek z komentarzem = 1
# wywołanie na pliku którego nie ma
$ python komentarze.py xxx.py
Traceback (most recent call last):
File "komentarze.py", line 3, in <module>
file = open(sys.argv[1])
IOError: [Errno 2] No such file or directory: 'xxx.py'
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# wywołanie na pliku którego nie wolno odczytać
$ python komentarze.py /etc/shadow
Traceback (most recent call last):
File "komentarze.py", line 3, in <module>
file = open(sys.argv[1])
IOError: [Errno 13] Permission denied: '/etc/shadow'
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# wywołanie bez wymaganego argumentu
$ python komentarze.py
Traceback (most recent call last):
File "komentarze.py", line 3, in <module>
file = open(sys.argv[1])
IndexError: list index out of range
Widać, że program wypisuje wynik tylko wtedy, gdy ten wynik jest poprawny.
Nawet w ostatnim przypadku, gdy komunikat jest mniej jasny, można łatwo się domyślić (jeśli się wie że sys.argv to lista argumentów programu), że lista argumentów programu jest za krótka.
Oczywiście znacznie lepiej, by programista sprawdził, czy użytkownik podał właściwą listę argumentów do programu. W szczególności w obecnej wersji, nadmiarowe argumenty są ignorowane, co stoi w sprzeczności z zasadą, że errors should never pass silently, czyli że żadne błędy nie powinny uchodzić uwadze. Niemniej, wydaje mi się, że ten przykład ładnie pokazuje elegancję wyjątków.
Sprzątanie po sobie
W części o chwytaniu wyjątków został pokazany blok finally. Do czego on służy? Kiedy należy go użyć?
Komendy zawarte w bloku finally są wykonywane zawsze, niezależnie od tego, czy wyjątek został rzucony, czy nie. Pozwala to na zawarcie w tym bloku operacji typu „sprzątanie po sobie”, które muszą zawsze zostać wykonane. Typowy przykład to sytuacja, gdzie procesy działające równolegle muszą się ze sobą synchronizować i wykorzystują do tego celu blokadę. Może to być np. pusty plik na dysku, który zostaje stworzony przed rozpoczęciem działań i skasowany po ich zakończeniu, tzw. lock-file.
import sys, time
def create(lockfile):
while True:
try:
os.open(lockfile, os.O_CREAT|os.O_EXCL|os.O_RDONLY)
break
except OSError as e:
if e.errno != os.errno.EEXIST:
raise
print 'file is locked!, waiting'
time.sleep(1)
def remove(lockfile):
os.unlink(lockfile)
def do_work(datafile):
lockfile = datafile + '.lock'
create(lockfile)
try:
do_some_work_on(datafile)
finally:
remove(lockfile)
if __name__ == '__main__'
do_work(sys.argv[1])
Jak widać, ten program jest napisany tak, że jeśli w momencie wywołania będzie istniał plik lockfile, to program w nieskończoność będzie czekał na jego zniknięcie. Dlatego kluczowe jest, by uruchomienie programu zawsze zostało zakończone skasowaniem pliku lockfile. Programista nie przywidywał wystąpienia żadnego konkretnego błędu w wywołaniu do_some_work_on, wyjątek rzucony w trakcie wykonywania spowoduje zakończeniu programu i wypisanie ciągu wywołań w programie. Niemniej, nawet przed takim smutnym zakończeniem, plik lockfile zostanie usunięty.
Jako nieco łatwiejszy przykład możemy rozważyć sytuację, gdy chcemy zamknąć otwarty plik zaraz po zakończeniu pewnego ciągu instrukcji. W przykładzie powyżej (zob. #Pliki) zamykaliśmy plik explicite, używając jego metody close. Jest to jak najbardziej wystarczające, bo chodzi nam o jego zamknięcie tylko dlatego, by przy okazji wypchnąć zmiany w nim dokonane i móc go znowu za chwilę otworzyć i odczytać nową zawartość. Normalnie plik i tak zostałby zamknięty w momencie zakończenia programu. Wobec tego nie ma sensu przejmować się wystąpieniem wyjątku w operacjach wykonywanych na otwartym pliku (czyli pojedynczej operacji os.write(wierszyk)), bo on i tak uniemożliwiłby wykonanie dalszej części programu. Ale załóżmy, że chcemy, by metoda close została wykonana niezależnie od tego, co zdarzy się w trakcie os.write. Możemy zawrzeć wywołanie close w bloku finally:
f1 = open('wierszyk.txt', 'w')
try:
f1.write(wierszyk)
finally:
f1.close()
Sprzątanie zestandaryzowane
Nowsze wersje Pythona (≥ 2.5) zawierają konstrukcję with pozwalającą na wygodne skorzystanie z predefiniowanych operacji do wywołania przed i po jakimś bloku kodu. Istotne jest to, że tak jak w przypadku bloku finally, operacje kończące są wywoływane nawet w wypadku wystąpienia wyjątku.
Operacja close na pliku wywoływana w bloku finally jest bardzo powszechna, chociaż może nie tak jak iteracja po linijkach w pliku. Dlatego klasa file definiuje ją jako standardową operację „sprzątającą”.
Przykład powyżej przepisany z wykorzystaniem konstrukcji with:
with open('wierszyk.txt', 'w') as f1:
f1.write(wierszyk)
# po wyjściu z tego bloku plik zostanie samoczynnie zamknięty
W nowych programach należy postępować w ten właśnie sposób.
W jaki sposób klasa file definiuje, co ma zostać wywołane na końcu bloku with? Wykorzystuje context manager protocol opisany w PEP 0343.