Kodomo

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

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

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

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

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

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

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

Теперь, чтобы добавить новый тип мордочек, нам нужно сделать совсем немного. Мы опишем класс новых мордочек – главное, чтобы у него были те же методы, которые вызывает 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(1, canvas.winfo_width())
  10         y = random.randint(1, canvas.winfo_height())
  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.itemconfigure(self.face, fill="blue")

Единственное неочевидное место в этом коде – новое слово 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, он будет проверять, попал ли курсор в мордочку по новым правилам (попадания в прямоугольник), и вместо удаления мордочки будет перекрашивать её в синий цвет. Правда, у этого оказался неожиданный побочный эффект1.

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

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

Детали отношений между объектами и классами

Дабы вы представляли себе принципы, на которых устроено объектно-ориентированное программирование в питоне (и не путались в сложных ситуациях), полезно в первом приближении представлять себе, как соотносятся между собой объекты и классы.

На эту тему в питоне есть два правила:

  1. питон добавляет новые атрибуты ровно в тот объект, в который мы сказали их добавить (при этом класс тоже считается объектом – сказали добавить в класс, значит атрибуты оказались в классе)
  2. когда мы просим питон добыть из объекта атрибут, он сначала ищет его в самом объекте – а если не нашёл, в его родителе (в его классе)

(Если вы вспомните, то для отношений между локальными и глобальными переменными в питоне ровно те же самые правила: когда мы в переменную присваиваем, мы обязательно создаём локальную переменную; когда мы переменную читаем, мы сначала ищем её в локальных, а если локальной нет, то в глобальных)

Рассмотрим игрушечный пример:

   1 class A(object):
   2     x = 1
   3 p = A()
   4 p.x = 2
   5 q = A()
   6 print p.x, q.x
   7 A.x = 3
   8 print p.x, q.x

когда питон исполняет первые пять строк, у него в памяти возникает такая картинка:

digraph {
    rankdir = "tb";
    A [shape="record",label="{A|{x|1}}"];
    p [shape="record",label="{p|{x|2}}"];
    q [shape="record",label="{q|}"];
    p -> A;
    q -> A;
}

Т.е. питон выполнил ровно то, что мы ему сказали:

  1. в первой строке мы сказали ему создать класс A

  2. во второй строке мы сказали ему добавить атрибут x в класс A (именно в класс A)

  3. в третьей строке мы ему сказали создать объект p класса A

  4. в четвёртой строке мы сказали ему добавить атрибут x в объект p (и именно в объект p, а не в класс A – когда мы пишем, мы пишем всегда сразу в сам объект)

  5. в пятой строке мы ему сказали создать объект q класса A

Шестая строка нам демонстрирует, что в объекте p у нас атрибут x есть, и распечатается его значение, а в объекте q у нас атрибута x нет, но зато есть атрибут x в классе A.

На экране напечатается: 2 1

Седьмой строкой мы меняем значение атрибута x в классе A:

digraph {
    rankdir = "tb";
    A [shape="record",label="{A|{x|3}}"];
    p [shape="record",label="{p|{x|2}}"];
    q [shape="record",label="{q|}"];
    p -> A;
    q -> A;
}

И последней строкой мы демонстрируем, что все правила остались в действии: так как в объекте q нет атрибута x, то он полезет за ним в A, а объект p счастливо останется со своим значнием.

На экране напечатается: 2 3

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

Исключения

Исключение (англ. 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 показывает, что делать мы ничего не будем, просто продолжим работу.

  1. Морда кирпичом, в которую ткнули, будет удаляться из списка и в следующий раз мы не зарегистрируем попадания мышью в неё, а создадим поверх новую мордочку; если бы мы хотели избежать этого эффекта, нам нужно было бы, чтобы метод delete как-нибудь договаривался с on_mouse на тему того, нужно ли данный объект удалять из списка: например, возвращал в зависимости от своего пожелания True или False, и нам бы пришлось поменять on_mouse так, чтобы если он вернул True, то объект из списка не удалялся (1)