Kodomo

Пользователь

Учебная страница курса биоинформатики,
год поступления 2012

Ещё раз про объекты и классы; исключения

О пользе объектно-ориентированного подхода

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

Задание: подумайте пять минут и предложите, как такую задачу можно делать без классов.

Напомним, что в прошлый раз мы решали задачу так:

Теперь, чтобы добавить новый тип мордочек, нам нужно сделать совсем немного. Мы опишем класс новых мордочек – главное, чтобы у него были те же методы, которые вызывает on_timer и on_mouse, и которые нужны для создания мордочек: __init__, contains, delete, wink. Мы можем легко описать такой класс, так как требования у нас к нему минимальные:

   1 class BrickFace(object):
   2     w = 30
   3     h = 20
   4 
   5     def __init__(self, canvas, x, y):
   6         # игнорируем x и y -- они были нужны только в случае,
   7         # если объекты нашего класса будут создаваться по нажатию мыши,
   8         # а мы этого делать не будем
   9         x = random.randint(self.w, canvas_size - self.w)
  10         y = random.randint(self.h, canvas_size - self.h)
  11         self.canvas = canvas
  12         self.face = canvas.create_rectangle(x-self.w/2, y-self.h/2, x+self.w/2, y+self.h/2, fill="red")
  13         self.x = x
  14         self.y = y
  15     def wink(self):
  16         pass
  17     def contains(self, x, y):
  18         return self.x - self.w/2 < x < self.x + self.w/2 \
  19             and self.y - self.h/2 < y < self.y + self.h/2
  20     def delete(self):
  21         self.canvas.delete(self.face)

Единственное неочевидное место в этом коде – новое слово pass. Мы хотели описать функцию wink как функция, которая ничего не делает. При этом питон требует в обязательном порядке, чтобы если в предыдущей строчке было def (или if/else/for/...), то следующая строка идёт с отступом и в ней описывается, что делать. Поэтому в питоне есть специальная команда pass, которая значит: эта строчка ничего не делает.

Теперь для того, чтобы использовать новый тип мордочек, нам достаточно добавить сколько-то их в список faces там, где мы его создаём:

   1 ...
   2 
   3 faces=[]
   4 for i in range(20):
   5     faces.append(Face(canv, None, None))
   6 for i in range(20):
   7     faces.append(BrickFace(canv, None, None))
   8 
   9 ...

Теперь, когда on_mouse будет перебирать мордочки и проверять, попал ли пользователь курсором в какую-нибудь из них... (этот фрагмент кода с прошлого раза не изменился, мы его не трогали):

   1      ...
   2      for f in faces:
   3          if f.contains(x, y):
   4                 f.delete()
   5                 faces.remove(f)
   6                 return
   7      faces.append(Face(canv, x, y))

... то он будет точно так же перебирать все объекты в списке faces, однако на этот раз часть из них будут объектами класса Face, а часть будет объектами класса BrickFace. Однако, у обоих этих классов есть методы contains и delete. Поэтому когда питон будет натыкаться на объект класса BrickFace, он будет проверять, попал ли курсор в мордочку по новым правилам (попадания в прямоугольник), и удалять ее.

Аналогично, когда on_timer будет решать, что заданный объект класса BrickFace должен моргнуть, он будет вызывать у него метод wink – в этом случае он просто ничего не будет делать.

Проявленное в этом примере свойство объектного подхода называется полиморфизм – то есть когда метод с одним и тем же названием может вести себя по-разному в зависимости от того, к какому классу относится объект.

Исключения

Исключение (англ. exception; редк. исключительная ситуация) – это способ обозначить в программе ошибку (например "нам дали плохие данные") таким образом, чтобы потом эту ошибку можно было исправить (в некотором смысле).

Отлов исключений

Начнем с примера. Предположим, мы хотим написать программу, которая читает информацию из файлов (например, просто считает, сколько строк в каждом файле), имена которых подаются в командной строке.

$ python countLines.py a.txt b.txt c.txt

   1 import sys
   2 args = sys.argv[1:]
   3 if len(args)>0:
   4     for filename in args:
   5         file = open(filename)
   6         print filename, len(file.read().split("\n"))

Допустим, в директории, в которой мы находимся есть файлы a.txt и c.txt, но нет b.txt.

Тогда при попытке открыть несуществующий файл программа упадет, так и не посмотрев третий файл. При это питон напишет вам сообщение об ошибке, которое помимо прочего будет содержать строчку «IOError: [Errno 2] No such file or directory: 'b.txt'»

Из-за того, что мы написали один лишний файл в командной строке, мы не посчитали ответа для c.txt, что неприятно. Ещё неприятно, что сообщение об ошибке содержит всё, что нужно программисту, но почти ничего, что нужно пользователю.

Может оказаться, что вы хотите, чтобы в этом случае программа не падала, а просто напечатала вам какое-нибудь предупреждение и продолжила работу со следующим файлом. Оказывается, такого поведения программы можно легко добиться.

Когда мы сказали питону open('b.fasta'), питон выбросил исключение. (англоязычная терминология тут всегда либо to raise an exception, либо to throw an exception). Покуда мы с исключением ничего не делаем, выброс исключения для нас означает прекращение работы программы здесь и сейчас. Но мы можем его поймать, и тогда сделать то, что кажется нам местным в данном случае, например, напечатать предупреждение и продолжить работать:

   1         try:
   2             file = open(filename)
   3             print filename, len(file.read().split("\n"))
   4         except Exception:
   5             print "WARNING: Can not open " + filename

Теперь, если внутри блока try было выброшено исключение, то в этот момент питон прекратит исполнять содержимое блока и перейдёт к блоку except. Если исключения не случилось, то в блок except питон не попадёт.

Но не надо всегда окружать открытие файла конструкцией «try-except»! Это надо делать только если вы действительно хотите, чтобы программа не упала, а продолжила работать.

Еще момент: теперь сообщении, что файл не открывается смешивается с выходными данными, что не всегда удобно. Можно развести их в разные потоки — выходные данные в стандартный поток вывода (stdout), как и было о этого, а предупреждение — в стандартный поток ошибок (stderr):

вместо

   1 print "WARNING: Can not open " + filename

написать так:

   1 sys.stderr.write("WARNING: Can not open " + filename+'\n')

В этом случае, если мы запустим нашу программу, перенаправив stdout в файл:

python countLines.py a.txt b.txt c.txt > out.txt

Все «нужные» данные запишутся в этот файл, а предупреждение на консоль:

$ python countLines.py a.txt b.txt c.txt > out.txt
Could not open file b.txt, ignoring
$ cat out.txt
a.txt 22
c.txt 51

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

   1     except Exception, e:
   2         sys.stderr.write(str(e) + "\n")

Где еще можно встретить исключения?

Например, когда вы пытаетесь обратиться к элементу словаря по несуществующему ключу, или к несуществующему элементу списка. Иногда (но опять же не всегда — это зависит от задачи!) хочется, чтобы программа не падала и продолжала работать.

Выброс исключений

Исключения можно не только отлавливать, но и выбрасывать.

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

   1 def __init__(self, canvas, x = None, y = None):
   2         self.canvas = canvas
   3         self.r = r
   4         if x == None or y == None:
   5             self.x = random.randint(self.r, canvas_size - self.r)
   6             self.y = random.randint(self.r, canvas_size - self.r)
   7         else:
   8             self.x = x
   9             self.y = y
  10         for f in faces:
  11             if (f.x - self.x)**2 + (f.y - self.y)**2 <= 4*self.r**2:
  12                 raise Exception("Overlapped!")
  13         self.color = random.choice(self.colors)
  14         self.eye_color = "black"
  15         self.create_face()
  16         self.create_eyes()

В этом случае, код создания рожицы перестанет исполняться, то есть она не отрисуется.

Исключение выбросится наверх — в то место, где мы создаем рожицы и кладем их в список. То есть данная недосозданная рожица не положится в список. Более того, поскольку выбросится исключение, программа должна упасть.

Но так будет происходить не во всех случаях. Мы создаем и добавляем рожицы в двух местах: вначале, когда создаем первые 20 рожиц, и потом — по щелчку мыши.

Так вот, когда исключение выпадает внутри функции mousePressed, обработчик Tkinter'овских событий ловит его, пишет сообщение на экран и продолжают работать как ни в чём не бывало.

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

   1 for i in range(20):
   2     try:
   3         faces.append(Face(canv))
   4     except:
   5         pass

Ключевое слово pass показывает, что делать мы ничего не будем, просто продолжим работу.

Наследование

Зачастую бывает так, что в программе необходимо создать новые объекты, которые отличаются от уже имеющихся наличием каких-то дополнительных свойств или слегка другим поведением. Если бы, например, нам понадобилось сделать рожицы с квадратными глазами, нам бы пришлось просто скопировать весь класс Face и поменять лишь метод create_eyes(). Такое копирование может привести к нежелательным последствиям. Например, если мы найдем в классе ошибку, нам придется ее исправлять сразу в двух местах. Для того, чтобы избежать такое копирование, придумали механизм наследования. В качестве примера, рассмотрим систему классов для геометрических фигур. Каждая фигура задается своим положением на холсте, цветом и размерами. Все фигуры умеют вычислить свою площадь, и у каждого типа фигур площадь вычисляется по своим правилам. Помимо фигур, в нашей системе будут просто точки. Точка не имеет размера, но задается координатами и цветом.

   1 class Point():
   2         def __init__(self, x, y, color):
   3                 self.x=x
   4                 self.y=y
   5                 self.color = color
   6         def square(self):
   7                 return 0
   8         def getColor(self):
   9                 return self.color
  10         def __str__(self):
  11                 return "point (%d, %d), %s" % (self.x, self.y, self.color)

Метод str - это строковое представление объекта. Если определен этот метод, то когда мы печатаем с помощью print, объект преобразуется по таким правилам в строку для печати. Поскольку точка не имеет размера, ее площадь равна 0. Теперь зададим класс для прямоугольников. У прямоугольника, в отличие от точки, есть ширина и высота.

   1 class Rect(Point):           #в скобках указываем класс, от которого наследуем
   2         def __init__(self, x, y, color, w, h):
   3                 Point.__init__(self, x, y, color)   # вызываем конструктор родительского класса
   4                 self.w = w
   5                 self.h = h
   6         def square(self):
   7                 return self.w*self.h
   8         def __str__(self):
   9                 return "rect (%d, %d), %s, %d, %d" % (self.x, self.y, self.color, self.w, self.h)

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

   1 class Square(Rect):
   2         def __init__(self, x, y, color, w):
   3                 Rect.__init__(self, x, y, color, w, w)
   4         def __str__(self):
   5                 return "qsuare (%d, %d), %s, %d" % (self.x, self.y, self.color, self.w)

Ну и теперь создадим список из разных фигур и выведем на экран:

   1 figures = []
   2 
   3 figures.append(Point(0, 0, "red"))      
   4 figures.append(Rect(0, 0, "blue", 1, 2))                
   5 figures.append(Square(0, 0, "green", 3))        
   6 
   7 for fig in figures:
   8         print fig
   9         print fig.getColor()
  10         print fig.square()