8. Błędy i wyjątki
******************

Do tej pory wiadomości o błędach były tylko wspomniane, ale jeśli
próbowałaś(-łeś) przykładów to pewnie udało ci się na nie natknąć.
Występują (przynajmniej) dwa charakterystyczne typy błędów: *błędy
składni* (syntax errors) oraz *wyjątki* (exceptions).


8.1. Błędy składni
==================

Błędy składni, znane również jako błędy parsowania, są prawdopodobnie
najczęstszym rodzajem skarg, które pojawiają się podczas nauki
Pythona:

   >>> while True print('Hello world')
     File "<stdin>", line 1
       while True print('Hello world')
                  ^^^^^
   SyntaxError: invalid syntax

Parser powtarza błędną linię i wyświetla małe „strzałki” wskazujące
token w linii, w której wykryto błąd. Błąd może być spowodowany
brakiem tokenu *przed* wskazywanym tokenem. W przykładzie błąd jest
wykryty na funkcji "print()", ponieważ brakuje przed nią dwukropka
("':'").  Nazwa pliku i numer linii są drukowane, abyś wiedział(a),
gdzie szukać, w przypadku, gdy dane wejściowe pochodzą ze skryptu.


8.2. Wyjątki
============

Nawet jeśli instrukcja lub wyrażenie jest poprawne składniowo, może
ona wywołać błąd podczas próby jej wykonania. Błędy zauważone podczas
wykonania programu są nazywane *wyjątkami* (exceptions) i nie zawsze
są niedopuszczalne: już niedługo nauczysz w jaki sposób je obsługiwać.
Większość wyjątków nie jest jednak obsługiwana przez program przez co
wyświetlane są informacje o błędzie jak pokazano poniżej:

   >>> 10 * (1/0)
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   ZeroDivisionError: division by zero
   >>> 4 + spam*3
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   NameError: name 'spam' is not defined
   >>> '2' + 2
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   TypeError: can only concatenate str (not "int") to str

Ostatni wiersz komunikatu o błędzie wskazuje, co się stało. Wyjątki
występują w różnych typach, a typ jest drukowany jako część
komunikatu: typy w przykładzie to "ZeroDivisionError", "NameError" i
"TypeError". Ciąg wydrukowany jako typ wyjątku jest nazwą wbudowanego
wyjątku, który wystąpił.  Jest to prawdą dla wszystkich wbudowanych
wyjątków, ale nie musi być prawdą dla wyjątków zdefiniowanych przez
użytkownika (choć jest to przydatna konwencja). Standardowe nazwy
wyjątków są wbudowanymi identyfikatorami (nie zarezerwowanymi słowami
kluczowymi).

Pozostała część linii dostarcza szczegółów na temat typu wyjątku oraz
informacji, co go spowodowało.

Wcześniejsza część komunikatu o błędzie pokazuje kontekst, w którym
wystąpił wyjątek, w postaci śladu stosu. Ogólnie rzecz biorąc, zawiera
on ślad stosu z listą linii źródłowych; jednak nie wyświetli linii
odczytanych ze standardowego wejścia.

Built-in Exceptions wymienia wbudowane wyjątki i ich znaczenie.


8.3. Obsługa wyjątków
=====================

Możliwe jest pisanie programów, które obsługują wybrane wyjątki.
Spójrzmy na poniższy przykład, który prosi użytkownika o wprowadzenie
danych, dopóki nie zostanie wprowadzona poprawna liczba całkowita, ale
pozwala użytkownikowi na przerwanie programu (przy użyciu "Control-C"
lub czegokolwiek innego obsługiwanego przez system operacyjny);
zauważ, że przerwanie wygenerowane przez użytkownika jest
sygnalizowane przez podniesienie wyjątku "KeyboardInterrupt".

   >>> while True:
   ...     try:
   ...         x = int(input("Please enter a number: "))
   ...         break
   ...     except ValueError:
   ...         print("Oops!  That was no valid number.  Try again...")
   ...

Instrukcja "try" działa następująco.

* W pierwszej kolejności wykonywane są instrukcje pod klauzulą *try* -
  pomiędzy słowami kluczowymi "try" i "except".

* Jeżeli nie wystąpi żaden wyjątek, klauzula *except* jest pomijana i
  zostaje zakończone wykonywanie instrukcji "try".

* Jeśli wyjątek wystąpi podczas wykonywania klauzuli "try", reszta
  klauzuli jest pomijana. Następnie, jeśli jego typ pasuje do wyjątku
  nazwanego po słowie kluczowym "except", wykonywana jest *klauzula
  except*, a następnie wykonanie jest kontynuowane po bloku
  try/except.

* Jeśli wystąpi wyjątek, który nie pasuje do wyjątku nazwanego w
  *klauzuli except*, jest on przekazywany do zewnętrznych instrukcji
  "try"; jeśli nie zostanie znaleziona obsługa, jest to *nieobsłużony
  wyjątek* i wykonanie zatrzymuje się z komunikatem błędu.

Instrukcja "try" może mieć więcej niż jedną *klauzulę except*, aby
określić programy obsługi dla różnych wyjątków.  Wykonany zostanie co
najwyżej jeden handler. Obsługiwane są tylko wyjątki, które występują
w odpowiadających im *klauzulach try*, a nie w kodzie obsługi tej
samej instrukcji "try". *Klauzula except* może określać wiele wyjątków
krotką w nawiasach, na przykład:

   ... except (RuntimeError, TypeError, NameError):
   ...     pass

A class in an "except" clause is compatible with an exception if it is
the same class or a base class thereof (but not the other way around
--- an *except clause* listing a derived class is not compatible with
a base class). For example, the following code will print B, C, D in
that order:

   class B(Exception):
       pass

   class C(B):
       pass

   class D(C):
       pass

   for cls in [B, C, D]:
       try:
           raise cls()
       except D:
           print("D")
       except C:
           print("C")
       except B:
           print("B")

Zauważ, że jeśli *klauzule except* byłyby odwrócone (z "except B" na
pierwszym miejscu), wypisane zostałoby B, B, B --- uruchamiana jest
pierwsza pasująca *klauzula except*.

Gdy wystąpi wyjątek, może on mieć powiązane wartości, znane również
jako *argumenty* wyjątku. Obecność i typy argumentów zależą od typu
wyjątku.

*Klauzula except* może określać nazwę zmiennej po nazwie wyjątku.
Zmienna jest powiązana z instancją wyjątku, która zazwyczaj posiada
atrybut "args" przechowujący argumenty. Dla wygody, typy wyjątków
wbudowanych definiują "__str__()" drukującą wszystkie argumenty bez
odwoływania się do ".args".

   >>> try:
   ...     raise Exception('spam', 'eggs')
   ... except Exception as inst:
   ...     print(type(inst))    # the exception type
   ...     print(inst.args)     # arguments stored in .args
   ...     print(inst)          # __str__ allows args to be printed directly,
   ...                          # but may be overridden in exception subclasses
   ...     x, y = inst.args     # unpack args
   ...     print('x =', x)
   ...     print('y =', y)
   ...
   <class 'Exception'>
   ('spam', 'eggs')
   ('spam', 'eggs')
   x = spam
   y = eggs

Dane wyjściowe metody "__str__()" wyjątku są drukowane jako ostatnia
część ('szczegóły') komunikatu dla nieobsłużonych wyjątków.

"BaseException" jest wspólną klasą bazową wszystkich wyjątków. Jedna z
jej podklas, "Exception", jest klasą bazową wszystkich nie-fatalnych
wyjątków. Wyjątki, które nie są podklasami "Exception" nie są
zazwyczaj obsługiwane, ponieważ są używane do wskazania, że program
powinien się zakończyć. Obejmują one "SystemExit", który jest rzucany
przez "sys.exit()" i "KeyboardInterrupt", które są rzucane, gdy
użytkownik chce przerwać program.

"Exception" może być używany jako symbol wieloznaczny, który
przechwytuje (prawie) wszystko. Dobrą praktyką jest jednak jak
najdokładniejsze określenie typów wyjątków, które zamierzamy
obsługiwać, i umożliwienie propagacji wszelkim nieoczekiwanym
wyjątkom.

Najczęstszym wzorcem obsługi "Exception" jest drukowanie lub
umieszczenie wyjątku w logach, a następnie ponowne rzucenie go
(umożliwiając również obsługę wyjątku funkcji, w której znajduje się
wywołanie):

   import sys

   try:
       f = open('myfile.txt')
       s = f.readline()
       i = int(s.strip())
   except OSError as err:
       print("OS error:", err)
   except ValueError:
       print("Could not convert data to an integer.")
   except Exception as err:
       print(f"Unexpected {err=}, {type(err)=}")
       raise

Instrukcja "try" … "except" posiada opcjonalną *klauzulę else*, która,
gdy jest obecna, musi następować po wszystkich *klauzulach except*.
Jest to przydatne w przypadku kodu, który musi zostać wykonany, jeśli
*klauzula try* nie rzuci wyjątku. Na przykład:

   for arg in sys.argv[1:]:
       try:
           f = open(arg, 'r')
       except OSError:
           print('cannot open', arg)
       else:
           print(arg, 'has', len(f.readlines()), 'lines')
           f.close()

Użycie klauzuli "else" jest lepsze niż dodanie dodatkowego kodu do
klauzuli "try", ponieważ pozwala uniknąć przypadkowego wychwycenia
wyjątku, który nie został rzucony przez kod chroniony instrukcją
"try"… "except".

Instrukcja *try* … *catch* nie obsługuje tylko wyjątków, które
występują bezpośrednio w *klauzuli try*, ale także te, które występują
wewnątrz funkcji, które są wywoływane (nawet pośrednio) w *klauzuli
try*. Na przykład:

   >>> def this_fails():
   ...     x = 1/0
   ...
   >>> try:
   ...     this_fails()
   ... except ZeroDivisionError as err:
   ...     print('Handling run-time error:', err)
   ...
   Handling run-time error: division by zero


8.4. Rzucanie wyjątków
======================

Instrukcja "raise" pozwala programiście wymusić wystąpienie żądanego
wyjątku. Na przykład:

   >>> raise NameError('HiThere')
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   NameError: HiThere

Jedyny argument do "raise" wskazuje wyjątek, który ma być rzucony.
Musi to być albo instancja wyjątku, albo klasa wyjątku (klasa
dziedzicząca z "BaseException", taka jak "Exception" lub jedna z jej
podklas). Jeśli przekazana zostanie klasa wyjątku, zostanie ona
niejawnie zainicjowana przez wywołanie jej konstruktora bez
argumentów:

   raise ValueError  # shorthand for 'raise ValueError()'

Jeśli chcesz rozpoznać, czy wyjątek został rzucony, ale nie zamierzasz
go obsługiwać, prostsza forma instrukcji "raise" pozwala na ponowne
rzucenie wyjątku:

   >>> try:
   ...     raise NameError('HiThere')
   ... except NameError:
   ...     print('An exception flew by!')
   ...     raise
   ...
   An exception flew by!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   NameError: HiThere


8.5. Łańcuch wyjątków
=====================

Jeśli nieobsłużony wyjątek wystąpi wewnątrz sekcji "except", zostanie
do niego dołączony obsługiwany wyjątek i uwzględniony w komunikacie o
błędzie:

   >>> try:
   ...     open("database.sqlite")
   ... except OSError:
   ...     raise RuntimeError("unable to handle error")
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

   During handling of the above exception, another exception occurred:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
   RuntimeError: unable to handle error

Aby wskazać, że wyjątek jest bezpośrednią konsekwencją innego,
instrukcja "raise" dopuszcza opcjonalną klauzulę "from":

   # exc must be exception instance or None.
   raise RuntimeError from exc

Może to być przydatne podczas przekształcania wyjątków. Na przykład:

   >>> def func():
   ...     raise ConnectionError
   ...
   >>> try:
   ...     func()
   ... except ConnectionError as exc:
   ...     raise RuntimeError('Failed to open database') from exc
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
     File "<stdin>", line 2, in func
   ConnectionError

   The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
   RuntimeError: Failed to open database

Umożliwia również wyłączenie automatycznego łączenia wyjątków przy
użyciu idiomu "from None":

   >>> try:
   ...     open('database.sqlite')
   ... except OSError:
   ...     raise RuntimeError from None
   ...
   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
   RuntimeError

Więcej informacji na temat mechaniki łączenia w łańcuchy można znaleźć
w rozdziale Built-in Exceptions.


8.6. Wyjątki zdefiniowane przez użytkownika
===========================================

Programy mogą nazywać własne wyjątki, tworząc nową klasę wyjątków
(więcej informacji na temat klas Python można znaleźć w rozdziale
Klasy). Wyjątki powinny zazwyczaj dziedziczyć z klasy "Exception",
bezpośrednio lub pośrednio.

Można zdefiniować klasy wyjątków, które robią wszystko, co może zrobić
każda inna klasa, ale zwykle są one proste, często oferując tylko
kilka atrybutów, które pozwalają na wyodrębnienie informacji o błędzie
przez kod obsługi wyjątku.

Większość wyjątków ma nazwy kończące się na „Error”, podobnie jak w
przypadku standardowych wyjątków.

Wiele standardowych modułów definiuje własne wyjątki w celu zgłaszania
błędów, które mogą wystąpić w zdefiniowanych w nich funkcjach.


8.7. Definiowanie działań porządkujących
========================================

Instrukcja "try" ma inną opcjonalną klauzulę, która jest przeznaczona
do definiowania działań porządkujących, które muszą być wykonane w
każdych okolicznościach. Na przykład:

   >>> try:
   ...     raise KeyboardInterrupt
   ... finally:
   ...     print('Goodbye, world!')
   ...
   Goodbye, world!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   KeyboardInterrupt

Jeśli klauzula "finally" jest obecna, klauzula "finally" wykona się
jako ostatnie zadanie przed zakończeniem instrukcji "try". Klauzula
"finally" uruchomi się niezależnie od tego, czy instrukcja "try"
spowoduje wyjątek. Poniższe punkty omawiają bardziej złożone przypadki
wystąpienia wyjątku:

* Jeśli podczas wykonywania klauzuli "try" wystąpi wyjątek, może on
  zostać obsłużony przez klauzulę "except". Jeśli wyjątek nie zostanie
  obsłużony przez klauzulę "except", zostanie on ponownie rzucony po
  wykonaniu klauzuli "finally".

* Wyjątek może wystąpić podczas wykonywania klauzul "except" lub
  "else". Ponownie, wyjątek jest ponownie rzucany po wykonaniu
  klauzuli "finally".

* Jeśli klauzula "finally" wykonuje instrukcje "break", "continue" lub
  "return", wyjątki nie są ponownie rzucane.

* Jeśli instrukcja "try" osiągnie instrukcję "break", "continue" lub
  "return", klauzula "finally" wykona się tuż przed wykonaniem
  instrukcji "break", "continue" lub "return".

* Jeśli klauzula "finally" zawiera instrukcję "return", zwróconą
  wartością będzie ta z instrukcji "return" klauzuli "finally", a nie
  wartość z instrukcji "return" klauzuli "try".

Dla przykładu:

   >>> def bool_return():
   ...     try:
   ...         return True
   ...     finally:
   ...         return False
   ...
   >>> bool_return()
   False

Bardziej skomplikowany przykład:

   >>> def divide(x, y):
   ...     try:
   ...         result = x / y
   ...     except ZeroDivisionError:
   ...         print("division by zero!")
   ...     else:
   ...         print("result is", result)
   ...     finally:
   ...         print("executing finally clause")
   ...
   >>> divide(2, 1)
   result is 2.0
   executing finally clause
   >>> divide(2, 0)
   division by zero!
   executing finally clause
   >>> divide("2", "1")
   executing finally clause
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "<stdin>", line 3, in divide
   TypeError: unsupported operand type(s) for /: 'str' and 'str'

Jak widać, klauzula "finally" jest wykonywana w każdym przypadku.
"TypeError" rzucony przez dzielenie dwóch ciągów znaków nie jest
obsłużony przez klauzulę "except" i dlatego jest ponownie rzucony po
wykonaniu klauzuli "finally".

W prawdziwych aplikacjach, klauzula "finally" jest przydatna do
zwalniania zewnętrznych zasobów (takich jak pliki lub połączenia
sieciowe), niezależnie od tego, czy użycie zasobu zakończyło się
powodzeniem.


8.8. Predefiniowane akcje porządkujące
======================================

Niektóre obiekty definiują standardowe akcje porządkujące, które mają
zostać podjęte, gdy obiekt nie jest już potrzebny, niezależnie od
tego, czy operacja przy użyciu obiektu powiodła się, czy nie. Spójrz
na poniższy przykład, który próbuje otworzyć plik i wydrukować jego
zawartość na ekranie:

   for line in open("myfile.txt"):
       print(line, end="")

Problem z tym kodem polega na tym, że pozostawia on plik otwarty przez
nieokreślony czas po zakończeniu wykonywania tej części kodu. Nie jest
to problemem w prostych skryptach, ale może być problemem dla
większych aplikacjach. Instrukcja "with" pozwala na używanie obiektów
takich jak pliki w sposób, który zapewnia, że są one zawsze czyszczone
szybko i poprawnie:

   with open("myfile.txt") as f:
       for line in f:
           print(line, end="")

Po wykonaniu instrukcji, plik *f* jest zawsze zamykany, nawet jeśli
napotkano problem podczas przetwarzania linii. Obiekty, które,
podobnie jak pliki, zapewniają predefiniowane akcje czyszczenia,
wskażą to w swojej dokumentacji.


8.9. Rzucanie i obsługa wielu niepowiązanych wyjątków
=====================================================

Istnieją sytuacje, w których konieczne jest zgłoszenie kilku wyjątków,
które wystąpiły. Dzieje się tak często w przypadku frameworków
współbieżności, gdy kilka zadań może zakończyć się niepowodzeniem
równolegle, ale istnieją również inne przypadki użycia, w których
pożądane jest kontynuowanie wykonywania i zbieranie wielu błędów
zamiast rzucenia pierwszego wyjątku.

Wbudowana "ExceptionGroup" zawiera listę instancji wyjątków, dzięki
czemu mogą one być rzucone razem. Sama w sobie jest wyjątkiem, więc
może być przechwycona jak każdy inny wyjątek:

   >>> def f():
   ...     excs = [OSError('error 1'), SystemError('error 2')]
   ...     raise ExceptionGroup('there were problems', excs)
   ...
   >>> f()
     + Exception Group Traceback (most recent call last):
     |   File "<stdin>", line 1, in <module>
     |   File "<stdin>", line 3, in f
     | ExceptionGroup: there were problems
     +-+---------------- 1 ----------------
       | OSError: error 1
       +---------------- 2 ----------------
       | SystemError: error 2
       +------------------------------------
   >>> try:
   ...     f()
   ... except Exception as e:
   ...     print(f'caught {type(e)}: e')
   ...
   caught <class 'ExceptionGroup'>: e
   >>>

Używając "except*" zamiast "except", możemy selektywnie obsługiwać
tylko te wyjątki w grupie, które pasują do określonego typu. W
poniższym przykładzie, który pokazuje zagnieżdżoną grupę wyjątków,
każda klauzula "except*" wyodrębnia z grupy wyjątki określonego typu,
pozwalając wszystkim innym wyjątkom propagować się do innych klauzul i
ostatecznie być ponownie zgłaszanymi.

   >>> def f():
   ...     raise ExceptionGroup(
   ...         "group1",
   ...         [
   ...             OSError(1),
   ...             SystemError(2),
   ...             ExceptionGroup(
   ...                 "group2",
   ...                 [
   ...                     OSError(3),
   ...                     RecursionError(4)
   ...                 ]
   ...             )
   ...         ]
   ...     )
   ...
   >>> try:
   ...     f()
   ... except* OSError as e:
   ...     print("There were OSErrors")
   ... except* SystemError as e:
   ...     print("There were SystemErrors")
   ...
   There were OSErrors
   There were SystemErrors
     + Exception Group Traceback (most recent call last):
     |   File "<stdin>", line 2, in <module>
     |   File "<stdin>", line 2, in f
     | ExceptionGroup: group1
     +-+---------------- 1 ----------------
       | ExceptionGroup: group2
       +-+---------------- 1 ----------------
         | RecursionError: 4
         +------------------------------------
   >>>

Należy pamiętać, że wyjątki zagnieżdżone w grupie wyjątków muszą być
instancjami, a nie typami. Wynika to z faktu, że w praktyce wyjątki są
zazwyczaj tymi, które zostały już rzucone i przechwycone przez
program, zgodnie z następującym wzorcem:

   >>> excs = []
   ... for test in tests:
   ...     try:
   ...         test.run()
   ...     except Exception as e:
   ...         excs.append(e)
   ...
   >>> if excs:
   ...    raise ExceptionGroup("Test Failures", excs)
   ...


8.10. Wzbogacanie wyjątków o notatki
====================================

Gdy wyjątek jest tworzony w celu rzucenia, jest on zwykle inicjowany
informacjami opisującymi błąd, który wystąpił. Istnieją przypadki, w
których przydatne jest dodanie informacji po przechwyceniu wyjątku. W
tym celu wyjątki mają metodę "add_note(note)", która akceptuje ciąg
znaków i dodaje go do listy notatek wyjątku. Standardowe renderowanie
tracebacku zawiera wszystkie notatki, w kolejności, w jakiej zostały
dodane, po wyjątku.

   >>> try:
   ...     raise TypeError('bad type')
   ... except Exception as e:
   ...     e.add_note('Add some information')
   ...     e.add_note('Add some more information')
   ...     raise
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   TypeError: bad type
   Add some information
   Add some more information
   >>>

Na przykład zbierając wyjątki w grupę wyjątków, możemy chcieć dodać
informacje kontekstowe dla poszczególnych błędów. Poniżej każdy
wyjątek w grupie ma notatkę wskazującą, kiedy wystąpił.

   >>> def f():
   ...     raise OSError('operation failed')
   ...
   >>> excs = []
   >>> for i in range(3):
   ...     try:
   ...         f()
   ...     except Exception as e:
   ...         e.add_note(f'Happened in Iteration {i+1}')
   ...         excs.append(e)
   ...
   >>> raise ExceptionGroup('We have some problems', excs)
     + Exception Group Traceback (most recent call last):
     |   File "<stdin>", line 1, in <module>
     | ExceptionGroup: We have some problems (3 sub-exceptions)
     +-+---------------- 1 ----------------
       | Traceback (most recent call last):
       |   File "<stdin>", line 3, in <module>
       |   File "<stdin>", line 2, in f
       | OSError: operation failed
       | Happened in Iteration 1
       +---------------- 2 ----------------
       | Traceback (most recent call last):
       |   File "<stdin>", line 3, in <module>
       |   File "<stdin>", line 2, in f
       | OSError: operation failed
       | Happened in Iteration 2
       +---------------- 3 ----------------
       | Traceback (most recent call last):
       |   File "<stdin>", line 3, in <module>
       |   File "<stdin>", line 2, in f
       | OSError: operation failed
       | Happened in Iteration 3
       +------------------------------------
   >>>
