Программирование и научные вычисления на языке Python/§6

Даже, если ваша программа работает правильно, неверные данные и ошибки ввода могут привести к непредсказуемым результатам. Поэтому мы познакомимся с перехватом ошибок и тем, что мы с ними после перехвата можем сделать. Представим, что в нашей программе из прошлого урока мы забыли передать аргумент:


```
c2f.py
Traceback (most recent call last):
```


File"c2f.py", line 2, in

C = float(sys.argv[1])

IndexError: list index out of range



Python прервал выполнение программы, показал что ошибка находится во второй строке и указал на тип ошибки — `IndexError` и краткое объяснение что не так. Из этой информации, просмотрев код программы, можно сделать вывод, что индекс 1 выходит за пределы списка (index out of range). А это и правильно, ведь в списке `sys.argv` только нулевой элемент, название программы. Значит есть всего один возможный индекс 0.



## Решение громоздкое

С тем, в чем состоит ошибка мы разобрались. Но это снаружи, а что сделать внутри нее, как эту ошибку обработать? Ведь нам не хотелось бы, чтобы программа завершалась, когда мы того не хотим. Первое, что приходит на ум — идти от самой проблемы и предварительно проверять длину списка:



```
if len(sys.argv) < 2:
print 'You failed to provide Celsius degrees as input '\
'on the command line!'
sys.exit(1) # прекращаем ввиду ошибки
F = 9.0*C/5 + 32
print '%gC is %.1fF' % (C, F)
```


Для преднамеренного прекращения программы используется функция exit модуля sys. В случае прекращения программы без ошибок функции передается 0, в случае наличия ошибки любое отличное от нуля значение (например, 1). Но этот решение для такого элегантного языка как Python весьма неповоротливо и громоздко, в нем существует гораздо более приятная конструкция.



## Решение элегантное: try-except

Более гибким способом перехвата ошибок заключается в возможности *попробовать* (try) выполнить записанные инструкции и если что-то идет не так, то есть возникает ошибка, то уже выполняются инструкции, соответствующие *исключению* (exception). В общих чертах это выглядит так:



```
try:
<statements>
except:
<statements>
```


Если что-то вдруг неправильно идет в блоке *try*, то выполнение программы тут же перескакивает в блок *except*, в котором находится обезболивающее средство против полученной ошибки.



## Перехват исключений

Для того, чтобы лучше прояснить технику перехвата исключений мы, конечно, применим его к нашему примеру:



```
import sys
try:
C = float(sys.argv[1])
except:
print 'You failed to provide Celsius degrees as input '\
'on the command line!'
sys.exit(1)
F = 9.0*C/5 + 32
print '%gC is %.1fF' % (C, F)
```


Теперь, если мы не передадим аргументов, то найти sys.argv[1] невозможно, значит наша кампания потерпела неудачу, возникло исключение и мы отправляемся в блок *except*. Если же передать аргумент, то выполняется только блок *try*. Проверим:


```
c2f.py
```


You failed to provide Celsius degrees as input on the command line!


`c2f.py 21`


21C is 69.8F



## Особые исключения

Возвращаясь к выражению `C = float(sys.argv[1])` можно сказать, что вообще здесь видны две чаще всего встречающиеся ошибки: первая, которую мы рассмотрели — отсутствие значения аргумента, вторая — в качестве аргумента вводится строка, которая не может быть конвертирована в `float`. Python определяет обе эти ошибки и называет их определенными именами: в первом случае это `IndexError`, во втором случае `ValueError`. В программе выше мы переходим в общий блок `except` независимо от того, что происходит неправильно в блоке `try`. Например, мы не пропустили значение переменной при вызове, но записали его в неверной форме (`21С`), результат тот же, что и раньше:


`c2f.py 21C`


You failed to provide Celsius degrees as input on the command line!


В Python есть удобная возможность разделять инструкции для ошибок различного рода:



```
import sys
try:
C = float(sys.argv[1])
except IndexError:
print 'Celsius degrees must be supplied on the command line'
sys.exit(1)
except ValueError:
print 'Celsius degrees must be a pure number, '\
'not "%s"' % sys.argv[1]
sys.exit(1)
F = 9.0*C/5 + 32
print '%gC is %.1fF' % (C, F)
```


Теперь в зависимости от ошибки совершенной пользователем, мы и сами можем сказать пользователю, что он сделал не так и как это исправить.



## Типы исключений

Использование индекса, выходящего за пределы списка, приводит к ошибке `IndexError`:

```
>>> data = [1.0/i for i in range(1,10)]
>>> data[9]
...
IndexError: list index out of range
```


Некоторые языки программирования (например, Fortran, C, C++ и Perl) позволяют индексацию вне интервала, что может быть иногда удобным, но делает сложным поиск скрытых ошибок. Python всегда останавливает программу, когда встречает неправильный индекс.

В случае, если содержимое строки не представляет собой только число, конвертирование заканчивается неудачей и ошибкой `ValueError`:

```
>>> C = float('21 C')
...
ValueError: invalid literal for float(): 21 C
```


В случае, если вызывается переменная, которой не задано значение, возникает ошибка `NameError`:

```
>>> print a
...
NameError: name 'a' is not defined
```


Деление на ноль:

```
>>> 3.0/0
...
ZeroDivisionError: float division
```


В случае, если вы ошибаетесь в написании ключевых слов языка, возникает `SyntaxError`:

```
>>> forr d in data:
...
forr d in data:
^
SyntaxError: invalid syntax
```


Если мы захотим перемножить число на строку:

```
>>> 'a string'*3.14
...
TypeError: can’t multiply sequence by non-int of type 'float'
```


Исключение `TypeError` возникает, поскольку объекты таких *типов* не могут быть перемножены (str и float). Но, вообще говоря, это не значит что число и строка не могут быть перемножены.



### Отступление об умножении

Перемножение возможно, если число целое (int). Под операцией умножения здесь понимается повторение строки указанное число раз. Это же правило действует и на списки:



```
>>> '--'*10 # десять двойных дефисов = 20 дефисов
'--------------------'
>>> n = 4
>>> [1, 2, 3]*n
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> [0]*n
[0, 0, 0, 0]
```



## Возбуждение исключений: raise

В случае возникновения ошибки, вы можете, как раньше, вывести сообщение и прекратить программу с помощью `sys.exit(1)`, а можете возбудить исключение. Для этого вы пишите `raise E(message)`, где `E` известный тип ошибки, а `message` — строка, объясняющая, что произошло. Чаще всего на месте `E` стоит `ValueError` или `TypeError`. Вы можете и сами определить тип ошибки. И, конечно, пример.



### Пример

Возбуждение исключений полезно, когда мы хотим уточнить какую-то ошибку, например понятную нам из физического смысла или каких-то других соображений. Пусть для ввода температуры и подойдет аргумент в виде числа, но мы помним про абсолютный ноль и предостерегаем пользователя:



```
def read_C():
try:
C = float(sys.argv[1])
except IndexError:
raise IndexError('Celsius degrees must be supplied on the command line')
except ValueError:
raise ValueError('Celsius degrees must be a pure number, '\
'not "%s"’ % sys.argv[1])
if C < -273.15:
raise ValueError('C=%g is a non-physical value!' % C)
return C
```


Далее имеются две возможности использовать функцию `read_C()`. Простой:



```
C = read_C()
```


Неправильный ввод приведет к:


`c2f.py `


Traceback (most recent call last):

File "c2f.py", line 5, in ?

raise IndexError

IndexError: Celsius degrees must be supplied on the command line.


Но стоит помнить, что вашей программой можете пользоваться не только вы и всегда надо стремиться скрывать лишнюю ненужную информацию. Для людей, незнакомых с Python, возникающие на экране слова `Traceback, raise, IndexError` могут вызвать недоумение. Самая важная информация для человека, работающего с вашей программой расположена в самом конце. Существует возможность выводить только эту строку. В этом случае функцию мы вызываем так:



```
try:
C = read_C()
except Exception, e:
print e
sys.exit(1)
```


`Exception` это имена всех возможных исключений, `e` — сообщение исключения. В нашем случае у нас в функции записаны два типа исключений, поэтому:



```
try:
C = read_C()
except (ValueError, IndexError), e:
print e
sys.exit(1)
```


После блока определения функции и блока *try-except* пишем блок вычислений и проверяем программу:



```
import sys
def read_C():
try:
C = float(sys.argv[1])
except IndexError:
raise IndexError\
('Celsius degrees must be supplied on the command line')
except ValueError:
raise ValueError\
('Celsius degrees must be a pure number, '\
'not "%s"' % sys.argv[1])
# C is read correctly as a number, but can have wrong value:
if C < -273.15:
raise ValueError('C=%g is a non-physical value!' % C)
return C
try:
C = read_C()
except (IndexError, ValueError), e:
print e
sys.exit(1)
F = 9.0*C/5 + 32
print '%gC is %.1fF' % (C, F)
```


`c2f.py`


Celsius degrees must be supplied on the command line


`c2f.py 21C`


Celsius degrees must be a pure number, not "21C"


`c2f.py -500`


C=-500 is a non-physical value!


`c2f.py 21 `


21C is 69.8F


Как видно, наша программа теперь не только обрабатывает получаемые данные и выдает ответ, но может работать и с неверными данными, определяя ошибку и выдавая информативное сообщение без лишней раздражающей информации.

## Мельком о графическом интерфейсе

Возможно, вас уже удивило, что программы, с которыми вы были знакомы до изучения Python так разительно отличаются. На Python можно писать и такие программы, но изначально мы не ставили себе целью писать программы с графическим интерфейсом, это темы многих книг и других курсов. Стоит сказать, что и вещь эта может оказаться довольно утомительной. Поскольку нашей целью, обозначенной и в названии, было решение научных проблем, то стоит сказать, что проще фокусироваться на них именно в такой форме, как мы это делаем, не вовлекая сюда вопросы интерфейса.

Но чтобы проиллюстрировать возможности Python и в таком ракурсе, приведем пример нашей программы, выполненной с использованием графического интерфейса (graphical user interface, GUI):



```
from Tkinter import *
root = Tk()
C_entry = Entry(root, width=4)
C_entry.pack(side='left')
Cunit_label = Label(root, text='Celsius')
Cunit_label.pack(side='left')
def compute():
C = float(C_entry.get())
F = (9./5)*C + 32
F_label.configure(text='%g' % F)
compute = Button(root, text=' is ', command=compute)
compute.pack(side='left', padx=4)
F_label = Label(root, width=4)
F_label.pack(side='left')
Funit_label = Label(root, text='Fahrenheit')
Funit_label.pack(side='left')
root.mainloop()
```


Цель дальнейшего рассмотрения — показать как в общих чертах устроено программирование GUI. Строится оно из маленьких графических элементов, называемых *виджетами* (widgets). Окно, что вы получили, если запустили программу выше, включает пять таких виджетов. Слева находится элемент для ввода значений (*entry widget*), справа — *label widget*, который показывает некоторый текст, здесь это «Celsius». Также здесь мы видим кнопку, *button widget*, при нажатии на которой осуществляется расчет и вывод результата. Этот результат показывается в еще одном label widget, справа от кнопки. Наконец, справа от результата мы видим label widget с текстом «Fahrenheit». В программе записано, какие виджеты надо создать и как правильно разместить в окне.

Первая инструкция нашей программы импортирует все инструменты из модуля `Tkinter`, предназначенного специального для создания GUI. Вначале нам требуется создать основной (root) виджет, который будет содержать окно со всеми остальными виджетами. Этот корневой виджет объект типа `Tk`. Далее мы создаем entry widget, на который будет ссылаться переменная `C_entry`. Этот виджет является объектом типа `Entry`, поддерживаемым модулем `Tkinter`. Описание виджета происходит так:



```
variable_name = Widget_type(parent_widget, option1, option2, ...)
variable_name.pack(side='left')
```


Когда создается новый виджет, мы должны связать его с *родительским* (parent widget), в котором этот новый элемент будет упакован. В настоящей программе все элементы размещаются в виджете `root`. Различные виджеты имеют и соответствующие им по сану полномочия. Например, `Entry widget` может изменять ширину текстовой области, в нашем случае `width=4`, то есть поле имеет ширину в 4 символа. Важно не забывать «упаковывать» (pack) элементы, иначе они будут невидимы.

Другие виджеты создаются похожим способом. Другая важная особенность заключается в том, как связано нажатие кнопки, на которой написано "is" и расчет внутри программы. `Button widget` привязан к функции `compute` через опцию `command=compute`. Это означает, что когда пользователь нажимает "is", вызывается функция `compute`. Внутри функции `compute` сначала с помощью функции `get` забирается значение из виджета `С_Entry`, затем в этой же строчке оно преобразуется из строки в `float` и отправляется вычисляться. Наконец, обновляется ("configure") текст в label widget с именем `F_label`.

В конце текста мы видим `root.mainloop()`. Здесь мы видим знакомое слово *loop*, цикл. И то, что происходит и правда можно считать бесконечным циклом, который ждет пока пользователь не совершит какое-либо понятное ему действие. В нашем примере действие только одно — нажатие на кнопку. Как только мы на нее нажимаем, вызывается функция `compute` и программа выполняет свою математическую работу.

На этом мы пока завершим наше знакомство с программированием GUI. О библиотеке Tkinter на Викиверситете создан специальный курс. Кроме того, существует и большие сторонние библиотеки: wxPython, PyQt, PyGTK.