6. Файловый ввод и вывод массивов

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

6.1. Двоичные файлы NumPy (.npy, .npz)

NumPy имеет два собственных формата файлов .npy - для хранения массивов без сжатия и .npz - для предварительного сжатия массивов. Если массивы, которые необходимо сохранить являются небольшими, то можно воспользоваться функцией numpy.save(). В самом простом случае, данная функция принимает всего два аргумента - имя файла в который будет сохранен массив и имя самого сохраняемого массива. Однако следует помнить, что файл будет сохранен, в той директории в которой происходит выполнение скрипта Python или в указанном месте:

>>> import numpy as np
>>> 
>>> a = np.arange(12).reshape(3, 4)
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>>
>>> b = np.arange(16).reshape(4, 4)
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])
>>>
>>> #  Файл сохранится в той же папке что и исполняемый скрипт
>>> np.save('example_1', a)
>>>
>>> #  Файл будет сохранен в папке example
>>> np.save('example/example_1', b)

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

>>> import numpy as np
>>> 
>>> a = np.load('example_1.npy')
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>>
>>> b = np.load('example/example_1.npy')
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])

Файлы .npy удобны для хранения одного массива, если в одном файле нужно сохранить несколько массивов, то необходимо воспользоваться функцией numpy.savez(), которая сохранит их в несжатом виде в файле NumPy с форматом .npz.

>>> import numpy as np
>>> 
>>> a = np.array([1, 2, 3])
>>> b = np.array([[1, 1], [0, 0]])
>>> c = np.array([[1], [2], [3]])
>>> 
>>> #  Сохраняет в той же директории что и исполняемый скрипт
>>> np.savez('example_2', a, b, c)
>>>
>>> #  Сохраняет в в папке example
>>> np.savez('example/example_2', a, b, c)

После сохранения массивов в файл .npz они могут быть загружены с помощью, уже знакомой нам, функции numpy.load(). Однако, имена массивов теперь изменились с a, b и c на arr_0, arr_1 и arr_2 соответственно:

>>> >>> ex_2 = np.load('example_2.npz')
>>> ex_2.files
['arr_1', 'arr_0', 'arr_2']
>>> 
>>> ex_2['arr_0']
array([1, 2, 3])
>>> 
>>> ex_2['arr_1']
array([[1, 1],
       [0, 0]])
>>> 
>>> ex_2['arr_2']
array([[1],
       [2],
       [3]])

Что бы вместе с массивами сохранялись их оригинальные имена, необходимо в функции numpy.savez() указывать их как ключи словарей Python:

>>> np.savez('example_2', a=a, b=b, c=c)
>>> 
>>> ex_2 = np.load('example_2.npz')
>>> 
>>> ex_2.files
['b', 'c', 'a']
>>> 
>>> ex_2['a']
array([1, 2, 3])
>>> 
>>> ex_2['b']
array([[1, 1],
       [0, 0]])
>>> 
>>> ex_2['c']
array([[1],
       [2],
       [3]])

В случае очень больших массивов можно воспользоваться функцией numpy.savez_compressed().

>>> a = np.arange(100000)
>>> a
array([    0,     1,     2, ..., 99997, 99998, 99999])
>>> 
>>> #  Файл example_3.npy занимает 400 кБ на диске:
...
>>> np.save('example_3', a)
>>> 
>>>
>>> #  файл example_3.npynpz занимает всего 139 кБ на диске:
...
>>> np.savez_compressed('example_3', a)

На самом деле, файлы .npz это просто zip-архив который содержит отдельные файлы .npy для каждого массива.

После того как файл был загружен с помощью функции numpy.savez_compressed() его так же легко загрузить с помощью функции numpy.load():

>>> ex_3 = np.load('example_3.npz')
>>> 
>>> ex_3.files
['arr_0']
>>> 
>>> ex_3['arr_0']    #  исходный массив 'a'
array([    0,     1,     2, ..., 99997, 99998, 99999])

6.2. Текстовые файлы (.txt)

Очень часто приходится иметь дело с данными, которые хранятся в обычных текстовых файлах (.txt). Как правило, данные представлены в виде строк, в каждой строке может быть несколько значений, разделенных некоторым символом-разделителем, как правило '' - пробелом, ',' или любым другим символом (или группой символов). Такие файлы, по своей сути, являются текстовыми представлениями таблиц, данные которых необходимо как-то загружать и выгружать.

Для работы с данными в текстовом формате NumPy предоставляет очень гибкие функции, но пока мы ограничимся рассмотрением лишь двух из них numpy.savetxt() и numpy.loadtxt() в самом простом случае их использования.

>>> a = np.arange(7)
>>> a
array([0, 1, 2, 3, 4, 5, 6])
>>> 
>>> np.savetxt('test_1.txt', a)

Если заглянуть в получившийся файл, то мы увидим следующее:

0.000000000000000000e+00
1.000000000000000000e+00
2.000000000000000000e+00
3.000000000000000000e+00
4.000000000000000000e+00
5.000000000000000000e+00
6.000000000000000000e+00

Как видим, одномерный массив записывается в текстовый файл простой последовательностью строк, одно значение - одна строка. Давайте сохраним двумерный массив:

>>> b = np.arange(9).reshape(3, 3)
>>> b
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> 
>>> np.savetxt('test_2.txt', b)

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

0.000000000000000000e+00 1.000000000000000000e+00 2.000000000000000000e+00
3.000000000000000000e+00 4.000000000000000000e+00 5.000000000000000000e+00
6.000000000000000000e+00 7.000000000000000000e+00 8.000000000000000000e+00

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

Если попробовать сохранить в текстовый файл трехмерный массив или же массив с большей размерностью, то это приведет к ошибке:

>>> c = np.arange(8).reshape(2, 2, 2)
>>> c
array([[[0, 1],
        [2, 3]],

       [[4, 5],
        [6, 7]]])
>>> 
>>> np.savetxt('test_3.txt', c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/n1/anaconda3/lib/python3.5/site-packages/numpy/lib/npyio.py", line 1320, in savetxt
    "Expected 1D or 2D array, got %dD array instead" % X.ndim)
ValueError: Expected 1D or 2D array, got 3D array instead

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

Итак, у нас есть два файла test_1.txt и test_2.txt, теперь загрузим массивы которые в них хранятся с помощью функции numpy.loadtxt():

>>> a = np.loadtxt('test_1.txt')
>>> a
array([0., 1., 2., 3., 4., 5., 6.])
>>> 
>>> b = np.loadtxt('test_2.txt')
>>> b
array([[0., 1., 2.],
       [3., 4., 5.],
       [6., 7., 8.]])

Как видим, все довольно просто. Но, мы не рассматривали загрузку данных из реальных таблиц, вид которых может быть очень сложным. Например, у нас есть текстовый файл loadtxt_example.txt со следующими данными:

#  Некоторые данные о 15 химических элементах.
#  Здесь представлены: "Атомный номер", 
#  "Обозначение элемента", "Группа", "Период", 
#  "Атомный масса", "Радиус атома (pm)"


1 H 7 1 1.00797 25
2 He 8 1 4.0026 30
3 Li 1 2 6.939 145
4 Be 2 2 9.0122 105
5 B 3 2 10.811 85
6 C 4 2 12.01115 70
7 N 5 2 14.0067 65
8 O 6 2 - 60
9 F 7 2 18.9984 50
10 Ne 8 2 20.179 40
11 Na 1 3 22.9898 180
12 Mg 2 3 24.305 150
13 Al 3 3 26.9815 125
14 Si 4 3 28.086 110
15 P 5 3 - 100

Функция numpy.loadtxt() позволяет загрузить эти данные в так называемый структурированный массив NumPy:

>>> fname = 'loadtxt_example.txt'
>>> dt_1 = np.dtype([('№','i2'),('symbol','|S2'),('radius','i2'),])
>>> a = np.loadtxt(fname, dtype=dt_1, skiprows=6, usecols=(0,1,5))
>>> 
>>> a
array([( 1, b'H',  25), ( 2, b'He',  30), ( 3, b'Li', 145), ...,
       (13, b'Al', 125), (14, b'Si', 110), (15, b'P', 100)],
      dtype=[('№', '<i2'), ('symbol', 'S2'), ('radius', '<i2')])

Обрабатывать отсутствующие значения (строка 8 и 15):

>>> def parse_mas(s):
...     try:
...         return float(s)
...     except ValueError:
...         return np.nan
... 
>>> dt_2 = np.dtype([('№','i2'),('symbol','|S2'),('mass','f4'),])
>>> 
>>> b = np.loadtxt(fname, dtype=dt_2, skiprows=6, usecols=(0,1,4), converters={4: parse_mas})
>>> 
>>> b
array([(1, b'H', 1.0079699754714966), (2, b'He', 4.002600193023682),
       (3, b'Li', 6.939000129699707), (4, b'Be', 9.012200355529785),
       (5, b'B', 10.810999870300293), (6, b'C', 12.011150360107422),
       (7, b'N', 14.006699562072754), (8, b'O', nan),
       (9, b'F', 18.99839973449707), (10, b'Ne', 20.179000854492188),
       (11, b'Na', 22.98979949951172), (12, b'Mg', 24.30500030517578),
       (13, b'Al', 26.98150062561035), (14, b'Si', 28.086000442504883),
       (15, b'P', nan)], 
      dtype=[('№', '<i2'), ('symbol', 'S2'), ('mass', '<f4')])

Или выгружать только необходимые столбцы таблицы:

>>> dt_3 = np.dtype([('symbol','|S2'),('radius','i2'),])
>>> 
>>> a, b = np.loadtxt(fname, dtype=dt_3, skiprows=6, usecols=(1,5), unpack = True)
>>> 
>>> a
array([b'H', b'He', b'Li', b'Be', b'B', b'C', b'N', b'O', b'F', b'Ne',
       b'Na', b'Mg', b'Al', b'Si', b'P'], 
      dtype='|S2')
>>> 
>>> b
array([ 25,  30, 145, 105,  85,  70,  65,  60,  50,  40, 180, 150, 125,
       110, 100], dtype=int16)

И это только малая часть всех возможностей, которые предоставляет NumPy для работы с данными в текстовом виде. Функция numpy.fromregex() позволяет создавать массивы из текстовых файлов с использованием регулярных выражений, а функция numpy.genfromtxt() имеет более 20 параметров настройки и предоставляет самые гибкие решения создания массивов из текстовых файлов.


6.3. Бинарные файлы

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

Рассмотрим простой пример:

>>> a = np.array([1/7, 2/7, 3/7])
>>> a
array([0.14285714, 0.28571429, 0.42857143])
>>> 
>>> a.tofile('test_bin_file_1')

Как видим, двоичный файл создан не с помощью функции NumPy, а спомощью метода базового класса ndarray объектами которого являются все массивы. Что бы загрузить массив из такого файла необходимо воспользоваться функцией numpy.fromfile():

>>> b = np.fromfile('test_bin_file_1')
>>> b
array([0.14285714, 0.28571429, 0.42857143])

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