Kodomo

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

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

Форматирование выдачи. Исключения.

Форматирование выдачи

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

Работает он так: вы пишете строку с вашим текстом и описанием того, куда и как вставить в неё ваши данные (эта строка называется шаблоном), и вызываете для этой строки метод format, которой даёте вставляемые данные в качестве аргументов.

Место, куда нужно вставить ваши данные, обозначается в шаблоне знаком {0}.

Пример:

   1 >>> who = "world"
   2 >>> print "Hello, {0}!".format(who)
   3 "Hello, world!"

Описания бывают вида:

Примеры:

   1 >>> print 'Name: {0:5}, Surname: {1:5}'.format('w', 'orld')
   2 Name: w    , Surname: orld 
   3 >>> print "4 + 5 = {0:05.3f}!".format(9)
   4 4 + 5 = 9.000!

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

   1 >>> message = '{filename}:{line_no}: {error_text}'
   2 >>> print message.format(line_no=10, filename="hello.txt", error="No error")
   3 hello.txt:10: No error

Возможности указания форматов в этом месте очень велики и мы не будем всё охватывать в наших лекциях, но отошлём вас к документации.

Исключения

Исключение (англ. 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         except Exception:
   4             print "WARNING: Can not open " + filename
   5             continue
   6         print filename, len(file.read().split("\n"))

Теперь, если внутри блока 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")

Приведём ещё один пример работы с исключениями, который демонстрирует идею о том, что исключения нужны для того, чтобы переносить обработку ошибки из того места, где мы можем обнаружить ошибку в то место, где у нас достаточно данных, чтобы её обработать. Предположим, у нас есть функции parse_line, которая разбирает очередную строку в каком-то сложном формате и функция work_with, которая получает разобранный кусочек данных и делает с ним умную вещь©. И мы хотим написать программу, которая делает то же самое, но при этом, если в файле где-то произошла ошибка, пишет, в каком именно файле и на какой строке (чтобы хоть как-то было проще эту ошибку отыскать).

   1 for n, line in enumerate(open(filename)):
   2     try:
   3         data = parse_line(line)
   4         work_with(data)
   5     except Exception, error:
   6         sys.stderr.write("{0}:{1}: {2}".format(filename, n+1, error))
   7         sys.exit(1)

В этом примере ни функции parse_line, ни функции work_with вообще не нужно заботиться о том, чтобы выводить сообщение об ошибке. Их задача – разобрать очередную строку из файла и что-то с ней сделать. И это достаточно сложная задача, чтобы не отвлекаться в ней на то, чтобы делать красивые сообщения об ошибке.

(Здесь мы несколько оставляем за скобками очень удобную функцию enumerate, которой очень настоятельно рекомендуем вам пользоваться. К нынешнему моменту все из вас, кто выполняли все домашние задания, уже вполне могут считаться начинающими программистами. И в таком случае, вам можно открыть великую тайну, что одна из бед программирования – это дичайше богатый арсенал средств, среди которых нужно постоянно искать новые, которые будут подходить именно для вашей задачи. С этой проблемой вам придётся много сталкиваться и в будущем. Конечно, мы постараемся хотя бы упомянуть о каких-то из наиболее неочевидных полезных вещей, однако, даже для того, чтобы упомянуть обо всём полезном, не хватит всего времени этого курса, а ведь кроме упоминания, нужно ещё научить вас пользоваться наиболее сложными вещами... Так что вам придётся очень много чего обнаруживать самостоятельно, благо для питона есть очень удобный справочник по его стандартной библиотеке)

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

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

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

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

Например, если мы разбираем файл в формате Stockholm и обнаружили, что у него есть строки, которые не начинаются с правильного символа (либо #, либо !, либо пробел, либо /) – это наверняка файл с ошибкой, вероятнее всего, нам просто дали файл не в том формате.

   1 def parse_stockholm(fd):
   2     for line in fd:
   3         if line.startswith("!"):
   4             ...
   5         elif line.startswith("#"):
   6             ...
   7         elif line.startswith(" "):
   8             ...
   9         else:
  10             raise Exception("Line not in Stockholm format")

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

Заметим, что в этом случае у нас как раз есть информация о номере строки, а снаружи её нет. Мы можем её туда передать, и, более того, нам полезно такую информацию передавать туда для ВСЕХ исключений, которые вывалились в этом месте. Мы не будем приводить здесь наиболее тактичный способ решать эту проблему, однако такой подход тоже неплох:

   1 def parse_stockholm(fd):
   2     for line_no, line in enumerate(fd):
   3         try:
   4             if line.startswith("!"):
   5                 ...
   6             elif line.startswith("#"):
   7                 ...
   8             elif.line.startswith(" "):
   9                 ...
  10             else:
  11                 raise Exception("Line not in Stockholm format")
  12         except Exception, error:
  13             raise Exception("line {0}: {1}".format(line_no, error))

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

Примеры работы с разными форматами файлов

Дабы у вас начало появляться представление о том, как всё то, что мы рассказали, применять к реальной жизни, попробуем разобрать пару примеров про чтение довольно типичных форматов файлов.

Пример 1. Получение словаря из FASTA-файла

Замечание: это пример того, как можно решить эту задачу. Мы не рекомендуем решать её всегда так же, так как для большинства целей проще обходиться без словаря, либо пользоваться специализированными библиотеками. В частности, вам не будет никакой пользы от того, что вы просто скопируете этот пример в домашнее задание.

   1 sequences = {}
   2 for line in open(filename):
   3     line = line.strip()
   4     if line == '':
   5         continue
   6     if line.startswith(">"):
   7         header = line[1:]
   8         sequences[header] = ""
   9     else:
  10         sequences[header] += line

Здесь два полезных приёма.

Во-первых, мы перед телом цикла выкинули из строки крайние проблеы (и, главное, переносы строк). После этого мы можем, например, спокойно склеивать две части последовательности через +, и можем проверять на пустоту строку сравнением её с пустой строкой. (До этого нам бы пришлось в каждом из этих мест отдельно заботиться о том, чтобы лишние переносы строк не мешали нам).

Во-вторых, мы перед телом цикла добавили проверку, не пустая ли строка, и пустые строки просто пропускаем.

Эти два приёма имеет смысл всегда держать на вооружении при построчной работе с файлами.

Пример 2. Чтение матрицы BLOSUM62 в словарь

Матрица BLOSUM обычно задаётся в обычном текстовом файле с комментариями. Нам же хочется иметь её в словаре с ключом в виде пары аминокислот.

   1 blosum = {}
   2 columns = None
   3 for line in open(filename):
   4     line = line.strip()
   5     if line == '' or line.startswith("#"):
   6         continue
   7     if not columns:
   8         columns = line.split()
   9         continue
  10     row = line.split()
  11     rowname = row.pop(0)
  12     for column, value in zip(columns, row):
  13         blosum[rowname, column] = int(value)

Новые приёмы.

Во-первых, мы модифицировали пропуск пустых строк так, чтобы пропускать ещё и строки с комментариями.

Во-вторых, см. документацию по list.pop. Иногда это очень полезная функция.

В-третьих, см. документацию по zip. Здесь мы совместили функцию zip (которая из двух списков делает поэлементно список пар), цикл и распаковку кортежа, взятого из очередного элемента того, что вернула функция zip, в одну строку. (То, чего мы не успели рассказать про распаковку кортежей раньше: её можно делать также в определениях функций и в заголовках циклов).

Замечание. Далеко не всем сразу становится понятно, как работает zip и почему можно так писать эту строку. На контрольной в аналогичных условиях мы будем сразу спрашивать, а почему оно так и как оно работает. Так что если вы не понимаете, как работает эта строка, сочините способ обойтись без неё (с помощью range(len(...))).