Kodomo

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

Словари. Кортежи. Исключения.

Я вам всё ещё не рассказал про ещё два важных встроенных типа в питоне. Исправляю.

Словари

Синонимов названию масса: словари (dict, dictionary), отображения (map, mapping), хэш-таблицы (hash table), ассоциативные массивы (associative array) – это те названия, которые я вспомнил за пять минут, и которые всё-таки всегда понимаются более-менее одинаково.

Суть простая: питон хранит список пар ключ -> значение, и умеет по ключу добыть значение, записать новое значение, добавить в список этих пар новую или удалить существующую.1

Пишется это так:

   1 >>> x = dict() # так оно называется с т.з. питона
   2 >>> x = {} # лучше писать всегда так
   3 >>> x[1] = 2 # добавили элемент в словарь
   4 >>> print(x)
   5 { 1: 2 }
   6 >>> x["hello"] = "world" # добавили ещё элемент в словарь
   7 >>> print(x)
   8 { 'hello': 'world', 1: 2 }
   9 >>> print(x[1])
  10 2
  11 >>> x[1] = 3 # изменили элемент словаря
  12 { 1: 3, 'hello': 'world' }
  13 >>> # мы никогда не знаем, в каком порядке хранятся значения в словаре
  14 >>> y = { 1: 2, 'hello': "world" } # а можно создавать словарь сразу так
  15 >>> del x[1] # удалили элемент словаря

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

   1 >>> for key in y:
   2 ...     print("%s, %s" % (key, y[key]))
   3 hello, world
   4 1, 2
   5 >>> list(y)
   6 ['hello', 1]
   7 >>> 1 in y
   8 True
   9 >>> 2 in y
  10 False
  11 >>> "hello" in y
  12 True
  13 >>> "world" in y
  14 False

(!) Вопрос на засыпку: как распечатать содержимое словаря в порядке возрастания ключей?

Питон старается преследовать насколько это осмысленно (но не более) простоту и универсальность записи. Обратите внимание, что основной синтаксис работы со словарями такой же, как и со списками: хранилище[место в хранилище] = значение или значение = хранилище[место в хранилище]:

   1 >>> x = [1, 2, 3]
   2 >>> y = { 0: 1, 1: 2, 2: 3 }
   3 >>> print(x[0])
   4 1
   5 >>> print(y[0])
   6 1
   7 >>> x[1] = 5
   8 >>> y[1] = 5
   9 >>> print(x[1])
  10 5
  11 >>> print(y[1])
  12 5
  13 >>> print(x)
  14 [1, 5, 3]
  15 >>> print(y)
  16 { 1: 5, 0: 1, 2: 3 }

(!) Вопрос на засыпку: чем в данном случае отличается dict от list? (Два простых примера: как выглядят присваивание за границы существующего, цикл? Какие ещё отличия можете придумать?)

Метод get()

За дальнейшей информацией о том, как работать со словарями, я вас отправляю к хелпам (интересного там разве что update, get, setdefault, pop), но про один метод всё-таки расскажу:

Метод d.get(key,value) для словаря читает элемент по ключу key. (То же самое, что квадратные скобочки после имени словаря). Однако, если элемента в словаре нету, то он не выдаст ошибку, а вернёт свой второй аргумент value (или None, если он опущен).

   1 >>> d = { 1: 2, 3: 4 }
   2 >>> d.get(1, 5) # то же самое, что d[1], так как 1 в d есть
   3 2
   4 >>> d.get(2, 5) # ключа 2 в словаре нету
   5 5

Соответственно, довольно часто встречается такая идиома:

   1 def f(d, k):
   2   d[k] = d.get(k, []) + [1]

(!) Что делает функция f. Ответьте на примере:

>>> x = { 1: [1] }
>>> f(x, 1)
>>> f(x, 2)
>>> print(x)

Метод get никогда не меняет словарь. Аналогичная штука, которая записывает значение в словарь, зовётся setdefault.

Пример: слияние двух CSV-таблиц

Я надеюсь, все хорошо вели конспекты

Тупли

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

По сути это то же самое, что и список, только тупли нельзя менять.

   1 >>> x = (1, 2, 3) # чаще всего кортежи создают как список значений в скобках
   2 >>> x = 1, 2, 3 # но на самом деле, скобки здесь имеют тот же смысл, что и в арифметических выражениях
   3 >>> for i in x:
   4 ...     print(i)
   5 1
   6 2
   7 3
   8 >>> 1 in x
   9 True
  10 >>> x[0]
  11 1
  12 >>> x[0] = 2
  13 ...
  14 TypeError: 'tuple' object does not support item assignment

Такая вот ограниченная и простая штука.

Последняя запятая

В этой теме мне очень хочется рассказать вам об одной маленькой синтаксической фишке в питоне: в питоне, когда мы описываем какой-нибудь объект-хранилище (list, tuple, dict), мы можем после последнего элемента написать запятую:

   1 >>> x = [1, 2, 3,]
   2 >>> y = {1:2, 3:4,}
   3 >>> z = (1, 2, 3,)

Казалось бы, непонятно зачем и некрасиво.

Есть два случая, где это оказывается очень удобным:

   1 proteins = [
   2   "CYB5_HORSE",
   3   "CYB5_MOUSE",
   4   "CYB5_HUMAN",
   5 ]

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

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

   1 >>> x = (1,)

tuple unpacking для списка из одного элемента

Теперь, когда мы знаем, какой у питона синтаксис для создания тупля из одного элемента, мы можем догадаться, как пишется tuple unpacking для списка из одного элемента.

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

   1 ...
   2     options, args = parser.parse_args()
   3     filename, = args

Последняя строка отличается от filename = args[0] двумя свойствами:

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

Во-вторых, когда мы пишем tuple unpacking, мы не обращаемся к элементу по номеру. Мы всего лишь требуем, чтобы то, что находится в правой части присваивания было "подобно списку". Например, мы можем туда подсунуть файл. (Если мы требуем, чтобы во входном файле была ровно одна строка, то быстрый способ её получить будет такой: line, = open(filename)).

Хитрые примеры

Операция % для строк

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

А она на самом деле хорошая и простая: если мы пишем строка % тупль, то строка воспринимается как шаблон, а тупль как значения, которые в этот шаблон подставлять. (Ума не приложу, почему Гвидо запретил в этом месте использовать список в правой части. Это было бы гораздо более читаемо и объяснять проще).

   1 >>> template = "%s, %s!"
   2 >>> values = ("hello", "world")
   3 >>> template % values
   4 "hello, world!"

При этом у неё есть особый случай: строка % не-тупль ведёт себя так же как строка % (не-тупль,). Это как раз и есть тот случай, которым чаще всего приходилось пользоваться.

Более того, у этой операции есть ещё один очень приятный особый случай: если мы пишем строка % словарь, то тогда мы можем из шаблона получить любое значение из словаря по ключу (если ключ – строка). Пишется это так:

   1 >>> values = { "action": "hello", "whom": "world", "when": "today" }
   2 >>> template = "%(action)s, %(whom)s! %(action)s!"
   3 >>> print(values % template)
   4 hello, world! hello!

Тут куча приятностей: у нас остаются все те же возможности выбора форматов (этого я тут не демонстрировал), мы можем использовать одно и то же значение несколько раз, мы не обязаны использовать все значения, и подстановки становятся несколько более читаемыми, так как в них говорится, что в это место будет подставлено. Но и недостатков тут тоже есть: подстановка в шаблоне начинает выглядеть %(слово)формат – у меня глаз такое никогда не соглашается выцепить как единое целое, и буковку s после скобок я забываю; да и вообще, вся запись становится шибко длинной.

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

(Вероятно, в этом месте было бы лучше использовать str.format. Беда том, что эта операция появилась только в питоне 2.6 (и в питонах 3.*), в то время, как много где до сих пор стоит питон 2.4 (в классе – 2.5), а авторы особо крупных и уважающих себя проектов зачастую заботятся о совместимости аж с 2.3 или 2.2.)

Двойное присваивание

Помните ещё, что такое tuple unpacking?

   1 >>> x = [1, 2, 3]
   2 >>> a, b, c = x

Что получится, если его объединить с собственно, tuple packing (ну, забавы ради можно таким словом обозначить создание кортежа)?

   1 >>> a, b, c = 1, 2, 3

Тут полезно понимать следующее: сначала питон полностью исполняет то, что в правой части присваивания (собирает кортеж), а потом распаковывает получившийся котеж слева. Это значит, что мы можем в правой и в левой части использовать одни и те же переменные:

   1 >>> x = 1
   2 >>> y = 2
   3 >>> x, y = y, x
   4 >>> print(x)
   5 2
   6 >>> print(y)
   7 1

Возврат нескольких значений из функции

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

   1 def coords(time):
   2     return sin(time), cos(time)
   3 
   4 ...
   5 
   6 x, y = coords(10)

Одним примером этого вы уже давно пользуетесь: вспомните, как выглядит работа с optparse.

vars()

Помните ли вы определения объекта в питоне?

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

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

До этого словаря можно дотянуться, и его можно править. Делается это функцией vars:

   1 >>> class X(object):
   2 ...     x = 1
   3 >>> x = X()
   4 >>> x.y = 2
   5 >>> vars(x)
   6 { 'y': 2 }
   7 >>> vars(x)['z'] = 3
   8 >>> x.z
   9 3

Если вспомнить ещё про функцию dir, которая показывает список имён всех полей, доступных из объекта, то можно получить вот что:

   1 >>> dir(x) # dir найдёт ещё кучу служебных __имён__, я их спрятал в многоточие
   2 ['x', 'y', 'z', ...]
   3 >>> list(vars(x))
   4 ['y', 'z']

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

Исключения

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

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

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

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

   1 ...
   2 import sys
   3 
   4 def count_records(file):
   5     return len(file.read().split("\n>"))
   6 
   7 def main(options, args):
   8     if args != []:
   9         for filename in args:
  10             print("%s: %s" % (filename, count_records(open(filename))))
  11     else:
  12         print("<stdin>: %s" % count_records(sys.stdin))
  13 ...

(!) В этом месте стоит остановиться и задуматься о том, почему и как оно будет работать.

(!) Можно ли сделать этот фрагмент программы лучше с точки зрения тех придирок, которые так щедро я рассылал вам на почту? Как?

Предположим, у нас в директории, где лежит наш скрипт (назовём его count_fasta.py), лежит два FASTA-файла: a.fasta и c.fasta. Что случится, если мы вызовем наш скрипт ./count_fasta.py a.fasta b.fasta c.fasta? Питон откроет первый файл, вызовет для него count_records, посчитает ответ, напечатает, попытается открыть второй файл – а его нет. В этот момент питон грозно и неприлично выругается:

...$ python t.py a.fasta b.fasta c.fasta
a.fasta: 22
Traceback (most recent call last):
  File "count_fasta.py", line 16, in <module>
    main(options, args)
  File "count_fasta.py", line 9, in main
    print("%s: %s" % (filename, count_records(open(filename))))
IOError: [Errno 2] No such file or directory: 'b.fasta'

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

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

   1 try:
   2     print("%s: %s" % (filename, count_records(open(filename))))
   3 except Exception:
   4     print("Could not open %s, ignoring" % filename)

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

Теперь запуск нашей программы будет выглядеть так:

$ python count_fasta.py a.fasta b.fasta c.fasta
a.fasta: 22
Could not open file b.fasta, ignoring
c.fasta: 51

Уже намного лучше!

Только одна беда:

$ python count_fasta.py a.fasta b.fasta c.fasta > list.txt
$ cat list.txt 
a.fasta: 22
Could not open file b.fasta, ignoring
c.fasta: 51

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

   1 try:
   2     print("%s: %s" % (filename, count_records(open(filename))))
   3 except Exception:
   4     sys.stderr.write("Could not open %s, ignoring\n" % filename)

Теперь:

$ python count_fasta.py a.fasta b.fasta c.fasta > list.txt
Could not open file b.fasta, ignoring
$ cat list.txt 
a.fasta: 22
c.fasta: 51

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

   1 try:
   2     print("%s: %s" % (filename, count_records(open(filename))))
   3 except Exception, e:
   4     sys.stderr.write(str(e) + "\n")

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

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

Теперь:

$ python count_fasta.py a.fasta b.fasta c.fasta > list.txt
[Errno 2] No such file or directory: 'b.fasta'
$ cat list.txt 
a.fasta: 22
c.fasta: 51

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

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

Я надеюсь, у вас теперь появилось представление о том, что такое исключения и как их отлавливать.

Рассмотрим ещё один пример: функцию min, которую вы когда-то там давно делали в качестве домашнего задания. Эта функция не определена в случае, когда на вход ей дали пустой список. По сути – это ошибка во входных данных. Самый правильный способ сообщать о такой ошибке – выбросить исключение:

   1 def min(list):
   2     if list == []:
   3         raise Exception("List is empty")
   4     head = list[0]
   5     tail = min(list[1:])
   6     if head < tail:
   7         return head
   8     else:
   9         return tail

Теперь:

   1 >>> min([]) 
   2 Traceback (most recent call last):
   3   File "<stdin>", line 1, in <module>
   4   File "<stdin>", line 3, in min
   5 Exception: List is empty

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

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

Если бы мы, хуже того, как некоторые из вас делали, в случае неправильного аргумента min печатали бы сообщение об ошибке и прерывали выполнение программы, то нам бы приходилось проверять аргументы перед каждым вызовом min самостоятельно, – то есть мало того, что делать код менее читаемым, так ещё и дублировать проверки min, да ещё и самим знать, какие аргументы правильные, а какие – нет.

Bubbling

Уточню ещё несколько поведение питона с исключениями:

Если мы не отловили исключение в функции, то факт возникновения исключения можно воспринимать так, как будто в том месте, где возникло исключение, стоял return, с той разницей, что теперь исключение будет выброшено в той точке, где была вызвана наша функция. Этот процесс называют "всплытием по стеку" (bubbling).

В некоторых языках программирования исключения называют "нелокальным возвратом" (non-local return), т.е. возврат не в ту функцию, которая вызвала нашу функцию, а куда-нибудь выше по стеку.

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

Совсем искусственный пример:

   1 def a(x):
   2     if x == 0:
   3         raise Exception("x is zero")
   4     return x + 1
   5 
   6 def b(x):
   7     return a(x) + 1
   8 
   9 def c(x):
  10     return a(b(x))
  11 
  12 def main(options, args):
  13     try:
  14         x = int(args[0])
  15         y = c(x)
  16         print("Funny function of %d is %d" % (x, y))
  17     except Exception, e:
  18         print("Could not calculate funny function of %s: %s" % (args[0], e))

Здесь у нас есть две точки, в которых может быть выброшено исключение: в функции a, если в командной строке написали 0 или -2 или в функции main, если нам в командной строке дали не число. Какая бы из этих ситуаций ни случилась, программа отработает нормально и распечатает внятное сообщение.

Дабы прояснить или окончательно запутать, посмотрим ход выполнения ./funny.py -- -2:

Could not calculate funny function of -2: x is zero

  1. Все эти операции имеют сложность "константа" (т.е. скорость исполнения этой операции в общем-то не зависит от размера словаря); питон умеет сразу узнавать по ключу, где лежит пара ключ -> значение. (1)