събота, 17 януари 2009 г.

Изключения в Python


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

Стефан Кънев & Николай Бачийски

21.03.2007г.

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

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

"""Модул за зимнината на Митьо Питона"""
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:
raise MityoWinterError

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

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
bad()
File "bad.py", line 2, in badfunc
def badfunc(): print l[3]
IndexError: list index out of range

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

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

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

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

>>> 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 = "Mityo is lonely!"
def make_lonely(): raise LonelyError
try:
make_lonely()
except LonelyError:
print LonelyError

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

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

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

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

LonelyError0 = "Mityo is lonely!"
LonelyError1 = "Mityo is lonely!"

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

води до:

Traceback (most recent call last):
File "", line 2, in
File "", line 1, in make_lonely
Mityo is lonely!

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

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

class LonelyMityoError(MityoError):
def __init__(self):
MityoError.__init__(self, "Mityo is Lonely!")

class StupidMityoError(MityoError): pass

def make_lonely(): raise LonelyMityoError()

def eat_a_cockroach(): raise StupidMityoError

try:
make_lonely()
except StupidMityoError:
print "Mityo has been somehow stupid!"
except MityoError, data:
print data

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

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

  • raise клас, инстанция # инстанцията е от точно този клас
  • raise инстанция # примерът със самотния Митьо, същото като
    raise инстанция.__class__, инстанция

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

  • raise клас # примерът с тъпия Митью,
    същото като raise клас()
  • raise клас, arg # същото като raise клас(arg)
  • raise клас, (arg0, arg1) # същото като raise клас(arg0, arg1)

Пример:

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

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

Initialized (99)

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

  • Прихващат се всички наследници — така лесно можем да си структурираме типовете грешки.
    class EmotionalMityoError(MityoError): pass
    class LonelyMityoError(EmotionalMityoError): pass
    class ConfusedMityoError(EmotionalMityoError): pass
    class MoneyMityoError(MityoError): pass

    mityo = Mityo()
    try:
    mityo.live_a_day()
    except MoneyMityoError:
    mityo.rob_a_bank()

    # прихващаме по-общия проблем, а не по-частните Lonely и Confused
    # от инстанцията на проблема психоаналитика може да извлече ценна информация
    except EmotionalMityoError, problem:
    mityo.go_to_shrink(problem)

    except MityoError:
    mityo.call_911()
    else:
    mityo.set_happy_bit()
    finally:
    mityo.play_with(Girl(beauty=99, hair='blonde')*3)

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

EmotionalMityoError = "Mityo has an emotional problem!"
LonelyMityoError = "Mityo is lonely!"
ConfusedMityoError = "Mityo is confused!"
MoneyMityoError = "Mityo is out of money!"

mityo = Mityo()
try:
mityo.live_a_day()
except MoneyMityoError:
mityo.rob_a_bank()

# налага се да опишем всички наследници на EmotionalError на ръка
# при всяко добавяне на грешка, трябва да я добавяме и тук
except (EmotionalMityoError, LonelyMityoError, ConfusedMityoError), problem:
mityo.go_to_shrink(problem)

# тук пък трябва да опишем всички други грешки...
# което си е ад за поддръжка, почти като москвич осмак
except MityoError:
mityo.call_911()
else:
mityo.set_happy_bit()
finally:
mityo.play_with(Girl(beauty=99, hair='blonde')*3)

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

  • Когато Python се натъкне на изключение в даден блок и в него то не се обработи, изключението се праща към горния блок, после към по-горния и така докато или изключението не бъде прехванато или не стигнем най-отгоре и интерпретаторът не спре програма по познатия ни вече начин (в червеничко).
  • Можем да се намесим в следната схема или като прихванем изключението (вече знаем как), или като пратим изключението нагоре по трасето. Последното става с голо извикване на raise:
    try:
    mityo.live_a_day()
    except GirlfriendMityoError:
    mityo.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
  • Какво може да направи Exception за нас?
    • >>> class MyError(Exception): pass
      >>> raise MyError("Failed, you have!", -1)
      Traceback (most recent call last):
      File "", 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:
    mityo.live_a_day()
    except (MityoWantsACracker, MityoWantsADrink,
    MityoWantsAnIsland, MityoWantsA333YearOldWhiskey,
    MityoNeedsPanties, MityoNeedsNewSlippers), thing:
    you.buy(whom=mityo, what=thing)
  • какво ще се случи ако Митьо може да поиска и мармотче? Ами само 111 годишно?
  • винаги е по-добре изключенията да се структурират, за да се избегне дългото, кошмарно за поддръжка и податливо на много грешки изброяване:
    except (MityoWants, MityoNeeds), thing:

Нека обобщим

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

Няма коментари:

Публикуване на коментар