Вспоминаем Python!

Часть 4

Работа с файлами

Файлы

Работа с файлами -- неотделимая часть жизни биоинформатика. До 90% работы среднестатистического биоинформатика уходит на парсинг файлов и их конвертацию между форматами.

Что такое файл?

Файл - это данные, которые хранятся на некотором носителе, например, на жестком диске.

Чтение из файла

Как открыть файл для чтения?

Команда open(filename, "r") открывает ваш файл для чтения.

In [1]:
myfile = open('spring1.txt', "r")
In [1]:
myfile = open('spring1.txt', "r")

Объект myfile имеет интересный тип (название его мало о чем говорит, достаточно понимать, что такой тип объектов позволяет читать из себя данные).

In [2]:
type(myfile)
Out[2]:
_io.TextIOWrapper

Как прочитать весь файл?

Можно прочитать весь файл разом методом .read

In [3]:
print(myfile.read())
Бесконечно можно смотреть на три вещи:
на горящий огонь,
на бегущую воду
и на собственный код месячной давности.

Что необходимо сделать после работы с файлом?

Открытый файл после окончания работы НАДО закрыть командной close

In [4]:
myfile.close()

Как ещё можно читать из файла?

Из файла также можно читать построчно:

In [5]:
myfile2 = open("spring2.txt", "r")
print(myfile2.readline())
print(myfile2.readline())
myfile2.close()
- Мне сказали: умный человек!

- Ну мало ли, что про человека болтают.

Заметим, что прочитанная строка возвращается с \n на конце.

Как можно убрать \n в конце строки?

Метод .strip строки поможет нам!

In [6]:
myfile2 = open("spring2.txt", "r")
print(myfile2.readline().strip())
print(myfile2.readline().strip())
- Мне сказали: умный человек!
- Ну мало ли, что про человека болтают.

А что произойдёт, если прочитать строку после того, как мы дошли до конца файла?

millionaire.jpeg

Ответ:

Когда файл дочитан до конца, метод .readline возвращает пустую строку (без \n на конце)

In [7]:
print("Returning empty string : |", myfile2.readline(), "|", sep="")
myfile2.close()
Returning empty string : ||

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

In [8]:
myfile3 = open("spring3.txt", "r")
line = myfile3.readline()

while line != "":
    print(line.strip())
    line = myfile3.readline()  #!

myfile3.close()
Ученые подсчитали, что
шансы реального существования
столь откровенно абсурдного
мира равняются одному на миллион.
Однако волшебники подсчитали, что
шанс «один на миллион» выпадает
в девяти случаях из десяти.

Почему не используем .strip сразу в строке, помеченной #!?

Однако в Python есть механизм, который помогает упростить код, написанный выше.

Для чтения файла можно использовать цикл for.

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

In [9]:
myfile4 = open("spring4.txt", "r")

for line in myfile4:
    print(line.strip())

myfile4.close()
Если разобраться, — сказал Ливитт, —
так это проблема из проблем.
Как дезинфицировать человеческое тело,
грязней которого, наверно,
нет ничего во всей Вселенной,
не уморив при этом человека?

Как можно догадаться, итерация идет по строкам!

Важно

Пройдя по файлу один раз, вы попадаете в конец файла.

Это означает, что вы не сможете пройтись по нему еще раз!

In [10]:
tabular = open("tab.txt", "r")

for line in tabular:
    print(line.strip())
    
for line in tabular: #won't be executed
    print("WOW!", line.strip())

tabular.close()
#name   seqlen  class
mouse   100 +
human   120 -
cat 140 +

Аналогично, если из файла уже что-то читалось - второй раз оно прочитано не будет

In [11]:
infile = open("tab.txt", "r")

print("\t\tReading the first line")
print(infile.readline().strip())

print("\t\tReading by 'for' cycle")
for line in infile:
    print(line.strip())
    
infile.close()
		Reading the first line
#name   seqlen  class
		Reading by 'for' cycle
mouse   100 +
human   120 -
cat 140 +

Что с этим поделать?

Можно переместиться в начало файла с помощью метода .seek.

In [12]:
infile = open("tab.txt", "r")
for line in infile:
    print(line.strip())

print("\nReading file from the beginning\n")

infile.seek(0)
for line in infile: 
    print(line.strip())
infile.close()
#name   seqlen  class
mouse   100 +
human   120 -
cat 140 +

Reading file from the beginning

#name   seqlen  class
mouse   100 +
human   120 -
cat 140 +

Запись в файл

Как открыть файл на запись?

Открытие файла на запись - open(filename, "w") (эта команда создает новый пустой файл)

Запись в файл осуществляется методом .write

In [13]:
munch = open("Munchhausen.txt", "w")

phrase = """В свое время Сократ мне сказал: 
"Женись непременно.
Попадется хорошая жена — станешь счастливым. 
Плохая — станешь философом."
Не знаю, что лучше\n"""

munch.write(phrase)

munch.close()

Обратите внимание: метод .write не добавляет \n в конце строки!

Вам нужно добавлять перенос строки самостоятельно :^)

Проверим нашу запись:

In [14]:
munch = open("Munchhausen.txt", "r")
print(munch.read())
munch.close()
В свое время Сократ мне сказал: 
"Женись непременно.
Попадется хорошая жена — станешь счастливым. 
Плохая — станешь философом."
Не знаю, что лучше

Можно также записать информацию в конец уже существующего файла, открыв его в режиме "a" - от "append".

In [15]:
f = open("Munchhausen.txt", "a")

s = """После свадьбы мы сразу уехали в свадебное путешествие. 
Я в Турцию, жена в Швейцарию,
и прожили там три года в любви и согласии.\n"""
f.write("\n")
f.write(s)
f.write("@Тот самый Мюнхгаузен\n") 
f.close()

f = open("Munchhausen.txt", 'r')
print(f.read())
f.close()
В свое время Сократ мне сказал: 
"Женись непременно.
Попадется хорошая жена — станешь счастливым. 
Плохая — станешь философом."
Не знаю, что лучше

После свадьбы мы сразу уехали в свадебное путешествие. 
Я в Турцию, жена в Швейцарию,
и прожили там три года в любви и согласии.
@Тот самый Мюнхгаузен

Почему важно закрывать файлы?

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

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

In [16]:
cat = "cat.txt"
catfile = open(cat, "w")
catfile.write("Hello, ")
catfile.write("world\n")
catread = open(cat, "r")
print(catread.read())

Внезапно! Вы что-то записали в файл, не закрыв его, а этого там не оказалось => ваша программа не нашла то, что искала, и упала.

Почему так?

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

А вот если его все же закрыть, то:

In [17]:
catfile.close()
print(catread.read())
catread.close()
Hello, world

Более того, время от времени [даже] в биоинформатике вам приходится писать программы, работающие долгое время, возможно, отвечающие на тысячи задач.

Если вы забываете закрывать файлы, открытые разными запросами, то в какой-то момент вы просто не сможете открыть новый файл и ваша программа завершится аварийно.

По-умному такая ситуация называется "утечка файловых дескрипторов".

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

In [18]:
opened_files = []

for i in range(1, 10000):
    fl = open(str(i), "w")
    opened_files.append(fl)
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
<ipython-input-18-5f9900e3f484> in <module>
      2 
      3 for i in range(1, 10000):
----> 4     fl = open(str(i), "w")
      5     opened_files.append(fl)

OSError: [Errno 24] Too many open files: '962'

К счастью, разработчики Python не дураки :^)

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

Исключения

В работе с Python мы часто сталкивались с исключительными ситуациями, в которых мы получали ошибки - исключения. Необработанное исключение приходит к завершению Python-программы

Пример хорошей обработки исключений

Медведь убежал из зоопарка, не могут найти.

Наконец звонит бабуля:

— Ой, ко мне во двор медведь забрался и на дерево залез! Уберите его, мне страшно!

Приезжает мужик с маленькой собачкой, даёт бабуле ружье, показывает на собачку и говорит:

— Бабуля, это Кефирчик. Я сейчас залезу наверх, встряхну ветку, медведь свалится, Кефирчик схватит его за ляжку и потащит в зоопарк.

— Хорошо, сынок. А зачем же мне ружье?

— Если вместо медведя свалюсь я, стреляй в Кефирчика!

Супер. А как ловить исключения?

В Python существует специальный механизм для отлова исключений:

In [21]:
try:
    pass
except:
    pass
In [22]:
1 / 0 
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-22-b971f1464605> in <module>
----> 1 1 / 0

ZeroDivisionError: division by zero
In [23]:
try:
    1 / 0
except ZeroDivisionError:
    print("Surprise, ьщерукагслук")
Surprise, ьщерукагслук

Порой нам необходимо отловить несколько исключений. В этом случае просто пишем несколько блоков except

In [24]:
a = 10
b = 5
try:
    a = int(a)    # probable error
    b = int(b)    # probable error
    print(a / b)  # probable error
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
2.0

Порой нам необходимо отловить несколько исключений. В этом случае просто пишем несколько блоков except

In [25]:
a = "1"
b = "6"
try:
    a = int(a)    # probable error
    b = int(b)    # probable error
    print(a / b)  # probable error
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
0.16666666666666666

Порой нам необходимо отловить несколько исключений. В этом случае просто пишем несколько блоков except

In [26]:
a = "MCMXXCIV"
b = "MCDLXXXVIII"
try:
    a = int(a)    # probable error
    b = int(b)    # probable error
    print(a / b)  # probable error
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
a and b must be integers

Порой нам необходимо отловить несколько исключений. В этом случае просто пишем несколько блоков except

In [27]:
a = 2
b = 0
try:
    a = int(a)    # probable error
    b = int(b)    # probable error
    print(a / b)  # probable error
except ValueError:
    print ("a and b must be integers")
except ZeroDivisionError:
    print ("b must be different from 0")
b must be different from 0

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

подтипы исключений

Такой код не будет работать так, как мы хотим:

In [28]:
a = 12
b = 0
try:
    a = int(a)    # probable error
    b = int(b)    # probable error
    print(a / b)  # probable error
    print(a[0])   # probable error
except Exception:
    print("Other exception")
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
Other exception

В каких условиях и почему код не будет работать корректно?

Теперь код работает верно!

In [29]:
a = 12
b = 0
try:
    a = int(a)    # probable error
    b = int(b)    # probable error
    print(a / b)  # probable error
    print(a[0])   # probable error
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
except Exception:
    print("Other exception")
b must be different from 0

Нечасто используемые особенности

Как описать ситуацию, когда мы хотим что-то сделать только в случае, если исключения не произошло?

In [30]:
a = 23
b = 0
try:
    a = int(a)      # probable error
    b = int(b)      # probable error
    result = a / b  # probable error
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
except Exception:
    print("Other exception")

print(result)
b must be different from 0
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-30-900bc769499f> in <module>
     12     print("Other exception")
     13 
---> 14 print(result)

NameError: name 'result' is not defined

else

Существуют еще ветвь else в try/except, которая выполняется только в том случае, если исключения не произошло.

In [31]:
a = 14
b = 0
try:
    a = int(a)      # probable error
    b = int(b)      # probable error
    result = a / b  # probable error
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
except Exception:
    print("Other exception")
else:
    print(result)
b must be different from 0

finally

И последним оператором, который можно использовать в try/except, является finally.

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

In [32]:
a = "XVII"
b = "LXI"
try:
    a = int(a)        # probable error
    b = int(b)        # probable error
    result = a / b    # probable error
    print(result[0])  # probable error
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
else:
    print(result)
finally:
    print("...Well, at least we got there...")
a and b must be integers
...Well, at least we got there...

Обычно в finally освобождаются ресурсы, которые мы брали в блоке try и хотим несмотря ни на что освободить. Например, если бы мы не имели блока with для файла, то вынуждать его к закрытию пришлось бы следующим образом:

In [33]:
a = 10
b = 0
try:
    fl = open("outfile.txt", "w")
    a = int(a)        # probable error
    b = int(b)        # probable error
    result = a / b    # probable error
    fl.write(f"{result}\n")
except ValueError:
    print("a and b must be integers")
except ZeroDivisionError:
    print("b must be different from 0")
else:
    print(result)
finally:
    print("Closing file")
    fl.close()
b must be different from 0
Closing file

with (менеджер контекста)

Казалось бы, почему реже используется последняя конструкция?

Ведь нам удобно иметь возможность закрыть открытый файл, даже если в нашем коде работы с ним произошла ошибка.

На самом деле, в Python есть специальная конструкция для этого - with

Эта конструкция использует finally.

In [34]:
in_file = open("spring5.txt", "r")
try:
    print(in_file.read())
    a = 5 / 0
finally:
    in_file.close()
Когда делаешь шаг с обрыва, 
жизнь моментально принимает
очень чёткое направление.

---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-34-dc9d202118b4> in <module>
      2 try:
      3     print(in_file.read())
----> 4     a = 5 / 0
      5 finally:
      6     in_file.close()

ZeroDivisionError: division by zero
In [35]:
print(in_file.closed)
True
In [36]:
with open("spring5.txt", "r") as in_file2:
    print(in_file2.read())
    a = 5 / 0
Когда делаешь шаг с обрыва, 
жизнь моментально принимает
очень чёткое направление.

---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-36-a7d94ac30f7e> in <module>
      1 with open("spring5.txt", "r") as in_file2:
      2     print (in_file2.read())
----> 3     a = 5 / 0

ZeroDivisionError: division by zero
In [37]:
print(in_file2.closed)
True

assert

assert позволяет проверить условие и вызвать ошибку, если условие не выполняется:

In [38]:
assert 10 == 10
In [39]:
assert 10 == 2
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-39-ed8f5ab137f6> in <module>
----> 1 assert 10 == 2

AssertionError: 
In [40]:
v1 = 10
v2 = 0
assert v2 != 0, 'You passed wrong denominator'
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-40-b3e05dc76c09> in <module>
      1 v1 = 10
      2 v2 = 0
----> 3 assert v2 != 0, 'You passed wrong denominator'

AssertionError: You passed wrong denominator

Многие используют assert при поиске ошибок в своей программе, что (вообще-то говоря) неверно, но всех это не очень задевает

Некоторые часто используемые файловые форматы

В работе с данными удобно использовать информацию, записанную в таблицы.

Чаще всего используется .csv -- Comma-Separated Values и .tsv -- Tab Separated Values форматы.

https://docs.python.org/3.4/library/csv.html

example_csv.png

In [41]:
import csv
In [42]:
petal_widths = []

with open('example.csv') as csvfile:
    iris_dataset = csv.reader(csvfile)
    header = next(iris_dataset)
    for row in iris_dataset:
        petal_w = float(row[2])
        petal_widths.append(petal_w) 
        
print(sum(petal_widths) / len(petal_widths))
3.7586666666666693

JSON [read: jeyson]

(JavaScript Object Notation, see https://www.json.org/ )

Создадим словарь и запишем его в json-файл:

In [43]:
d = {"name": "John", "age": 20, "os": "linux"}
In [44]:
import json
with open('example.json', 'w') as f:
    json.dump(d, f)
In [45]:
%%bash
less example.json
{"name": "John", "age": 20, "os": "linux"}

Теперь прочтём данные из другого файла!

In [46]:
with open('complex.json', 'r') as f:
    d = json.load(f)
d
Out[46]:
{'maps': [{'id': 'blabla', 'iscategorical': '0'},
  {'id': 'blabla', 'iscategorical': '0'}],
 'masks': {'id': 'valore'},
 'om_points': 'value',
 'parameters': {'id': 'valore'}}
In [ ]:
d = json.load(open('complex.json', 'r'))
In [47]:
json.loads(open('example.json').read())
Out[47]:
{'name': 'John', 'age': 20, 'os': 'linux'}

Формат имеет достаточно жесткий синтаксис:

In [48]:
json.loads(open('example.json').read() + ']')
---------------------------------------------------------------------------
JSONDecodeError                           Traceback (most recent call last)
<ipython-input-48-5ff0619d9ae3> in <module>
----> 1 json.loads(open('example.json').read() + ']')

/usr/lib/python3.6/json/__init__.py in loads(s, encoding, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    352             parse_int is None and parse_float is None and
    353             parse_constant is None and object_pairs_hook is None and not kw):
--> 354         return _default_decoder.decode(s)
    355     if cls is None:
    356         cls = JSONDecoder

/usr/lib/python3.6/json/decoder.py in decode(self, s, _w)
    340         end = _w(s, end).end()
    341         if end != len(s):
--> 342             raise JSONDecodeError("Extra data", s, end)
    343         return obj
    344 

JSONDecodeError: Extra data: line 1 column 43 (char 42)

pickle

Бинарные файлы меньше текстовых, их быстрее считывать и проще хранить.

In [49]:
import pickle
In [50]:
d = {"name": "John", "age": 20, "os": "linux"}
with open("example.pickle", "wb") as pickle_out:
    pickle.dump(d, pickle_out)
In [51]:
with open("example.pickle", "rb") as pickle_in:
    d2 = pickle.load(pickle_in)
In [52]:
d2
Out[52]:
{'name': 'John', 'age': 20, 'os': 'linux'}

Чтение из сети.

Больше информации: https://docs.python.org/3/howto/urllib2.html

In [53]:
import urllib.request
In [54]:
with urllib.request.urlopen('http://python.org/') as response:
    html = response.read()
In [55]:
with urllib.request.urlopen('http://humanstxt.org/humans.txt') as response:
    txt = response.readlines()
txt[:5]
Out[55]:
[b'\xef\xbb\xbf/* TEAM */\n',
 b'\tChef:Juanjo Bernabeu\n',
 b'\tContact: hello [at] humanstxt.org\n',
 b'\tTwitter: @juanjobernabeu\n',
 b'\tFrom:Barcelona, Catalonia, Spain\n']
In [56]:
print(txt[2])
print(txt[2].decode())
b'\tContact: hello [at] humanstxt.org\n'
	Contact: hello [at] humanstxt.org

Что делать при работе с неизвестным типом файла?

  1. Есть расширение? Да (.bed, .fa, .pdb) -> Google: bed/fa/pdb/... format specification

  2. Расширения нет. Читается ли файл как текстовый? Да -> Ищем информацию о формате или намеки в заголовке.

  3. Файл не читается как текстовый. Despair.

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

Спецификации файлов, часто используемых для геномных данных: https://genome.ucsc.edu/FAQ/FAQformat.html Иногда спецификация хорошо описана на Википедии (https://en.wikipedia.org/wiki/Protein_Data_Bank_(file_format) ), но предпочтение стоит отдавать описанию формата от самих авторов.

standards.png

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

glob и os являются незаменимыми для работы с файловой системой через Python.

glob.glob позволяет осуществлять поиск файлов по маске:

In [57]:
import glob
glob.glob('*.txt')
Out[57]:
['spring4.txt',
 'cat.txt',
 'dummy_text.txt',
 'tab.txt',
 'outfile.txt',
 'spring1.txt',
 'example_seqs.txt',
 'infile.txt',
 'spring3.txt',
 'spring2.txt',
 'spring5.txt',
 'Munchhausen.txt',
 'new_file.txt']

Модуль os позволяет работать с файловой системой одинаково, вне зависимости от платформы (Windows, Linux и т.д.)

In [58]:
import os
In [59]:
os.path.isfile('dummy_text.txt')
Out[59]:
True
In [60]:
os.path.isdir('dummy_text.txt')
Out[60]:
False
In [61]:
os.mkdir('tmp/')

Полезный способ правильно собрать путь по частям.

Способ не зависит от того, как пользователь ввел имя директории (с / или без), например.

In [62]:
os.path.join("Vasya", "hw72", "exercise89")
Out[62]:
'Vasya/hw72/exercise89'
In [63]:
os.path.join("Vasya/", "hw72/", "exercise89")
Out[63]:
'Vasya/hw72/exercise89'
In [64]:
os.path.join("Vasya/", "hw72", "exercise89")
Out[64]:
'Vasya/hw72/exercise89'

Ну вот и всё!

folks.gif