Изключения

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

class LectorsException(Exception): pass
raise LectorsException()

08.04.2010г.

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

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

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

ERROR = -1
SUCCESS = 0

def prepare_for_winter():
    jar = jars.Jar()
    if jar.clean() == jars.ERROR:
        print("Shit happens")
        return ERROR
    if jar.fill('python juice') == jars.ERROR:
        print("Shit happens")
        return ERROR
    if jar.close() == jars.ERROR:
        print("Shit happens")
        return ERROR
    return SUCCESS

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

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

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

class MityoWinterError(Exception): pass

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

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

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

…

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

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

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

bad.py:

l = [1, 2, 3]
def bad(): print(l[3])
bad()
След изпълнение получаваме:
[~]$ python3.0 bad.py
Traceback (most recent call last):
  File "bad.py", line 3, in 
    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

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

>>> ZeroDivisionError
<class 'ZeroDivisionError'>

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

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

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

>>> issubclass(ZeroDivisionError, ArithmeticError)
True

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

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

finally

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

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

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

class XmasError(Exception):
    def __init__(self):
        self.issuer, self.message = 'Robosanta', 'watches you'

class NaughtyError(XmasError):
    def __init__(self):
        super().__init__()
        self.message = 'You were very naughty this year!'

class AreYouDeadYetError(XmasError):
    def __init__(self):
        super().__init__()
        self.message = 'Are you dead yet?'

def confess_sins(): raise NaughtyError

def celebrate_xmas(): raise AreYouDeadYetError

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

try:
    celebrate_xmas()
except AreYouDeadYetError as e:
    print(e.issuer, '--', e.message)
except XmasError:
    print('Climbing to the sky. Never wonder why. Tailgunner...')

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

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

Пораждане на изключения (4)

Пример:

class MyError(Exception):
    def __init__(self, who, why):
        self.who, self.why = who, why
    def __str__(self):
        return '{0} did it because {1}'.format(self.who, self.why)

>>> try:
    raise MyError('I', 'I want to rule the world!')
except MyError as e:
    print(e)

I did it because I want to rule the world!
>>>

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

  • Прихващат се инстанции на всички наследници — така лесно можем да си структурираме типовете грешки.
    class EmotionalError(BenderError): pass
    class FryDeadError(EmotionalError): pass
    class NotFamousError(EmotionalError): pass
    class MoneyError(BenderError): pass
    
    bender = Bender()
    try:
        bender.live_a_day()
    except MoneyError:
        bender.rob_a_friend()
    # прихващаме по-общия проблем, а не по-частните FryDead и NotFamous
    # от инстанцията на проблема психоаналитика може да извлече ценна информация
    except EmotionalError as problem:
        bender.drink_and_run_away_from(problem)
    except BenderError:
        bender.activate_self_destruct_sequence()
    else:
        bender.watch_tv()
    finally:
        bender.build_own_ship_with_blackjack_and_hookers()
    

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

  • Когато Python се натъкне на изключение в даден блок и в него то не се обработи, изключението се праща към горния блок, после към по-горния и така докато или изключението не бъде прехванато или не стигнем най-отгоре и интерпретаторът не спре програма по познатия ни вече начин (в червеничко).
  • Можем да се намесим в следната схема или като прихванем изключението (вече знаем как), или като пратим изключението нагоре по трасето. Последното става с голо извикване на raise:
    try:
        bender.live_a_day()
    except BenderError:
        bender.boned = True
        # Бендър не може да се оправя с това, нека тези отгоре да се грижат
        raise

Подходи

  • Look Before You Leap (LBYL)
  • Easier to Ask for Forgiveness than Permission (EAFP)

Design by Contract

За всеки метод определяме:

def factorial(n):
    ...

x = factorial(7)

assert

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

class Exception

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

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

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

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

Нека обобщим

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

Още въпроси?