Програмиране с Python: Изключения
Author: Стефан Кънев и Николай Бачийски

# Изключения

### „ Програмиране с Python“, ФМИ

### class LectorsException(Exception): pass
raise LectorsException("Стефан Кънев, Николай Бачийски, Димитър Димитров")

#### 14.04.2008г.

# Традицията повелява

Ето как най-често се справяме с грешките в нашите програми:

    """Модул за зимнината на Митьо Питона"""
    import jars

    ERROR = -1
    SUCCESS = 0

    def prepare_for_winter():
        jar = jars.Jar()
        if jar.clean() == jars.ERROR:
            print "Couldn't clean Mityo's jar!"
            return ERROR
        if jar.fill('python juice') == jars.ERROR:
            print "Couldn't fill Mityo's jar!"
            return ERROR
        if jar.close() == jars.ERROR:
            print "Couldn't close Mityo's jar!"
            return ERROR
        return SUCCESS

# Традициите не са това…

Сега да опитаме с изключения:

    """Модул за зимнината на Митьо Питона"""
    import jars

    class MityoWinterError: pass

    def prepare_for_winter():
        try:
            jar = jars.Jar()
            jar.clean()
            jar.fill('python juice')
            jar.close()
        except jars.Error, error:
            print "Shit happens"

# Синтаксис и семантика

    try:
        блок
    except изключения:
        блок ако се случи някое от описаните изключения

    …

    except още изключения:
        блок ако се случи някое от описаните изключения
    except:
        блок ако изключението не е хванато по-горе
    finally:
        блок изпълнява се винаги
    else:
        блок ако не е възникнала изключителна ситуация

# Аз не (при|с)хващам

По подразбиране, при неприхванато изключение, Python спира изпълнението на програмата и отпечатва на стандартния изход описание на грешката и реда на извикване на функциите до момента на грешката.

bad.py:

    l = [1, 2, 3]
    def bad(): print l[3]
    bad()

След изпълнение получаваме:

    $ python bad.py
    Traceback (most recent call last):
      File "bad.py", line 3, in <module>
        bad()
      File "bad.py", line 2, in bad
        def bad(): print l[3]
    IndexError: list index out of range

Изключенията се използват активно от вградените средства в езика.

# Започвам да (при|с)хващам

    def distribute_over(beers):
        try:
            return 333/beers
        except ZeroDivisionError:
            return 0

Изключенията са класове, инстанции или низове, като последните не е препоръчително да се ползват.

`>>> ZeroDivisionError
<type 'exceptions.ZeroDivisionError'>`

Можем да прихванем и по-общ тип изключение (родителски клас):

    def distribute_over2(beers):
        try:
            return 333/beers
        except ArithmeticError:
            return 0

Ето и доказателство:

`>>> issubclass(ZeroDivisionError, ArithmeticError)
True`

Тази практика е много логична, тъй като делението на нула е и аритметична грешка и когато прихващаме аритметичните грешки, би трябвало да хванем и делението на нула.

# По-гъвкаво прихващане

- Можем да вземем и повече информация за изключението:

      try: x = [] / 4
      except TypeError, data: print data

  Какво ще има в `data`, зависи от самото изключение, но е прието всички да връщат годна за отпечатване стойност, ако се дадат като аргументи на `str` или `repr`.
- Ако за няколко изключения имаме една и съща реакция, можем да ги прихванем накуп:

      try:
          doomed()
      except (NameError, TypeError), data:
          print data
      except (MyError, YourError):
          print "Opps! This shouldn't've hapenned..."
      except:
          print "Unknown exception."
      else:
          print "It's my happy day!"

- С празен `except` прихващаме изключения, които не са били хванати до момента. Трябва да бъде поставен след всички други `except`-и.

# finally

    file = open('data.txt')
    try:
        mymodule.load_info(file)
    except IOError, data:
        print "Couldn't read from file: %s" % data
    except (mymodule.BadDataError, mymodule.InternalError), data:
        print "Loading failed: %s" % data
    else:
        print "Data loaded successfully from file."
    finally:
        file.close()

Ако присъства, `finally` стои винаги най-отдолу.

# Пораждане на изключителни ситуации — низове (1)

    LonelyError = "Stefan is lonely!"
    def make_lonely(): raise LonelyError
    try:
        make_lonely()
    except LonelyError:
        print LonelyError

Към можем низа да дадем и допълнителна информация:

    LonelyError = "Stefan is lonely!"
    def make_lonely(level): raise MyError, level
    try:
        make_lonely(666)
    except LonelyError, level:
        print "Stefan's loneliness is at level %d" % level

# Пораждане на изключителни ситуации — низове (2)

За да се определи дали едно изключение съвпада с конкретно `except` твърдение двете изключение се сравняват с `is`, а не с `==`:

    LonelyError0 = "Stefan is lonely!"
    LonelyError1 = "Stefan is lonely!"

    def make_lonely(): raise LonelyError0
    try:
        make_lonely()
    except LonelyError1:
        print LonelyError1

води до:

    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
      File "<stdin>", line 1, in make_lonely
    Stefan is lonely!

# Пораждане на изключения — класове (1)

    class StefanError:
        def __init__(self, msg = "There is something wrong with Stefan!"):
            self.msg = msg
        def __repr__(self): return self.msg
        def __str__(self): return self.__repr__()

    class LonelyStefanError(StefanError):
        def __init__(self):
          StefanError.__init__(self, "Stefan is Lonely!")

    class StupidStefanError(StefanError): pass

    def make_lonely(): raise LonelyStefanError()
    def eat_a_cockroach(): raise StupidStefanError

    try:
        make_lonely()
    except StupidStefanError:
    	print "Stefan has been somehow stupid!"
    except StefanError, data:
        print data

# Пораждане на изключения — класове (2)

2 начина за пораждане:

- `raise клас, инстанция # инстанцията е от точно този клас`
- `raise инстанция # примерът със самотния Стефан, същото като
  raise инстанция.__class__, инстанция`

За обратна съвместимост с низовите изключения могат да се използват и следните начини:

- `raise клас # примерът с тъпия Стефан,
  същото като raise клас()`
- `raise клас, arg # същото като raise клас(arg)`
- `raise клас, (arg0, arg1) # същото като raise клас(arg0, arg1)`

# Пораждане на изключения — класове (3)

Пример:

    class MyError():
        def __init__(self, msg, num):
            print "%s (%d)" % (msg, num)

    try:
    	raise MyError, ("Initialized", 99)
    except MyError:
    	pass

    Initialized (99)

# Далаверата от изключенията с класове

- Прихващат се всички наследници — така лесно можем да си структурираме типовете грешки.

      class EmotionalStefanError(StefanError): pass
      class LonelyStefanError(EmotionalStefanError): pass
      class ConfusedStefanError(EmotionalStefanError): pass
      class MoneyStefanError(StefanError): pass

      stefan = Stefan()
      try:
      	stefan.live_a_day()
      except MoneyStefanError:
      	stefan.rob_a_bank()
      # прихващаме по-общия проблем, а не по-частните Lonely и Confused
      # от инстанцията на проблема психоаналитика може да извлече ценна информация
      except EmotionalStefanError, problem:
          stefan.go_to_shrink(problem)
      except StefanError:
          stefan.call_911()
      else:
          stefan.set_happy_bit()
      finally:
          mityo = Mitio()
          mityo.play_with(Girl(beauty=99, hair='blonde')*3)

# Същото, ама с низове

    EmotionalStefanError = "Stefan has an emotional problem!"
    LonelyStefanError = "Stefan is lonely!"
    ConfusedStefanError = "Stefan is confused!"
    MoneyStefanError = "Stefan is out of money!"

    stefan = Stefan()
    try:
        stefan.live_a_day()
    except MoneyStefanError:
        stefan.rob_a_bank()
    # налага се да опишем всички наследници на EmotionalError на ръка
    # при всяко добавяне на грешка, трябва да я добавяме и тук
    except (EmotionalStefanError, LonelyStefanError, ConfusedStefanError), problem:
        stefan.go_to_shrink(problem)
    # тук пък трябва да опишем всички други грешки...
    # което си е ад за поддръжка, почти като москвич осмак
    except StefanError:
        stefan.call_911()
    else:
        stefan.set_happy_bit()
    finally:
        stefan.play_with(Girl(beauty=99, hair='blonde')*3)

# Ескалиране на грешката

- Когато Python се натъкне на изключение в даден блок и в него то не се обработи, изключението се праща към горния блок, после към по-горния и така докато или изключението не бъде прехванато или не стигнем най-отгоре и интерпретаторът не спре програма по познатия ни вече начин (в червеничко).
- Можем да се намесим в следната схема или като прихванем изключението (вече знаем как), или като пратим изключението нагоре по трасето. Последното става с голо извикване на `raise`:

      try:
          stefan.live_a_day()
      except GirlfriendStefanError:
          stefan.lonely = True
          # Стефан не може да се оправя с това, нека тези отгоре да се грижат
          raise

# assert

- `assert <проверка>, [<данни>]`
- Целта на твърдението `assert` е да се подсигурите, че важно за вашата програма условие е изпълнено
- `assert test, data` е еквивалентно на:

      if __debug__:
          if not test:
              raise AssertionError, data

  `data` никак не е задължително
- Както си личи от примера, `assert` рядко се ползва в крайния продукт, а най-вече по време на разработка за да си спестим главоболия и да сме сигурни в целостта на данните си
- По подразбиране глобалният атрибут `__debug__` има стойност `1` като може да бъде променена от вас или от опцията на командния ред `-O` (оптимизация), която го установява на `False`

-     def fib(n):
          assert n >= 0
          if n <= 1: return n
          else: return fib(n-2) + fib(n-1)

# Вградени класове за изключения

- Основният, който всички наследяват е `Exception`
- Главни категории:
  `StandardError`
    родител на всички вградени изключения; директен наследник на `Exception`
  `ArithmeticError`
    родител на `OverflowError`, `ZeroDivisionError`, `FloatingPointError`
  `LookupError`
    родител на `IndexError`, `KeyError`
  `EnvironmentError`
    родител на изключенията, които се случват извън интерпретора: `IOError`, `OSError`

# `class Exception`

Какво може да направи `Exception` за нас?

-     >>> class MyError(Exception): pass
      >>> raise MyError("Failed, you have!", -1)

      Traceback (most recent call last):
        File "<stdin>", line 1, in ?
      __main__.MyError: ('Failed, you have!', -1)

      >>> error = MyError("Failed, you have!", -1)
      >>> error.args
      ('Failed, you have!', -1)

- пази аргументите, които сме дали при създаване в `args`
- дефинира `__str__`, така че да връща нещо като: `map(str, self.args)`

# Използване на изключение не само за грешки

- за прихващане на събития през няколко блока дълбочина

      try:
          for box  in boxes:
              for jar in box.jars():
                  jar.has(throw=True, colour='velvet', eyes='cheese')
      except JarFound, good_jar:
          print "We found the jar! Its name is %s" % good_jar.name
      else:
          print "I couldn't find it :("

- за всякакви подобни странности на потока на програмата ни
- за обработка на частни случаи

# Аз хващам прекалено много

-     def func():
          try:
              … # някъде тук възниква IndexError
          except:
              … # тук хващаме всичко и отпечатваме съобщение за грешка

      try:
          func()
      except IndexError: # нъцки, няма да го хванем тук
          …

- прихващайте не повече отколкото ви трябва
- използвайте гол `except` главно в най-високото ниво на програмата си

# Ти пък прекалено малко

-     try:
          stefan.live_a_day()
      except (StefanWantsACracker, StefanWantsADrink,
              StefanWantsAnIsland, StefanWantsA333YearOldWhiskey,
      		StefanNeedsPanties, StefanNeedsNewSlippers), thing:
          you.buy(whom=stefan, what=thing)

- какво ще се случи ако Стефан може да поиска и мармотче? Ами само 111 годишно?
- винаги е по-добре изключенията да се структурират, за да се избегне дългото, кошмарно за поддръжка и податливо на много грешки изброяване:

      except (StefanWants, StefanNeeds), thing:
          …

# Нека обобщим

Няколко неща, за които може да ползваме изключения:

- обработка на грeшки:
  - структурирани, прихващаеми, позволяващи предаване на допълнителна информация
  - вградените функции и твърдения широко ги използват
  - пораждане и прихващане на собствени изключения
- безусловно извършване на заключителни действия — `finally`
- предаване на събития между отдалечени структурно части от кода

# Още въпроси?

- Страница на курса: http://fmi.py-bg.net/
- Форуми на курса: http://fmi.py-bg.net/forums/