12. Типы данных

Эффективность реализации алгоритмов, зависит от самых разных факторов в числе которых находится и используемый тип данных. В отличии от Python, который предоставляет лишь несколько числовых типов... на самом деле Python предоставляет большое количество не типов а видов чисел, например, в Python можно работать с обыкновенными дробями (модуль fractions) или числа с плавающей точкой произвольной точности (модуль decimal). В данном контексте понятие типа чисел связано с количеством памяти выделяемым под число определенного вида, поэтому, можно сказать, что набор числовых типов в NumPy гораздо больше, так как в Python мы вообще не заботимся о выделении памяти под определенное число. Но при работе с многомерными массивами, в которых могут храниться миллионы и миллиарды чисел, работа с памятью становится важным приоритетом.

В NumPy доступны следующие типы данных:

bool_
Логический тип (истина или ложь), хранящийся в виде байта.
int_
Целочисленный тип установленный по умолчанию (такой же, как C long, как правило это либо int64 либо int32).
intc
Идентичен C int (int32 или int64).
intp
Целочисленный тип, используемый для индексирования (такой же, как C ssize_t, как правило это либо int64 либо int32).
int8
Целые числа в диапазоне от -128 по 127 (числа размером 1 байт).
int16
Целые числа в диапазоне от -32768 по 32767, (числа размером 2 байта).
int32
Целые числа в диапазоне от -2147483648 по 2147483647, (числа размером 4 байта).
int64
Целые числа в диапазоне от -9223372036854775808 по 9223372036854775807, (числа размером 8 байт).
uint8
Целые числа в диапазоне от 0 по 255 (числа размером 1 байт).
uint16
Целые числа в диапазоне от 0 по 65535 (числа размером 2 байта).
uint32
Целые числа в диапазоне от 0 по 4294967295 (числа размером 4 байта).
uint64
Целые числа в диапазоне от 0 по 18446744073709551615 (числа размером 8 байт).
float_
То же самое что и float64.
float16
Вещественные числа половинной точности: 1 бит знака, 5 бит экспоненты, 10 бит мантисы (числа размером 2 байта).
float32
Вещественные числа одинарной точности: 1 бит знака, 8 бит экспоненты, 23 бита мантисы (числа размером 4 байта).
float64
Вещественные числа двойной точности: 1 бит знака, 11 бит экспоненты, 52 бита мантисы (числа размером 8 байт).
complex_
То же самое что и complex128.
complex64
Комплексные числа в которых действительная и мнимая части представлены двумя вещественными числами типа float32.
complex128
Комплексные числа в которых действительная и мнимая части представлены двумя вещественными числами типа float64.

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

>>> np.sctypeDict
{0: <class 'numpy.bool_'>, 1: <class 'numpy.int8'>, 2: <class 'numpy.uint8'>,
... ,
'Int16': <class 'numpy.int16'>, 'str': <class 'numpy.str_'>, 'str_': <class 'numpy.str_'>}

В NumPy существует 5 базовых числовых типов: булевы числа (тип bool: 0 - ложь и 1 - истина), целые числа (тип int), беззнаковые целые числа (тип uint), вещественные числа (числа с плавающей запятой, тип float) и комплексные числа (тип complex). У некоторых после указания типа следует количество бит необходимое для хранения такого числа в памяти (16, 32, 64 или 128), но некоторые, такие как int_ или intp зависят от используемой платформы (32 или 64-разрядные машины). При взаимодействии с низкоуровневым кодом на C или Fortran это нужно обязательно учитывать.

Объекты типа данных являются экземплярами класса numpy.dtype, каждый из которых имеет уникальные характеристики. Если импортировать NumPy как

>>> import numpy as np

то объекты типа данных становятся доступны как np.bool_, np.float64, np.int16 и т.д.

>>> np.bool
<class 'bool'>
>>> np.float64
<class 'numpy.float64'>

Для преобразования одного типа данных в другой можно так же воспользоваться объектами типа данных:

>>> np.float64(11)
11.0
>>> np.int64(11)
11
>>> np.bool(11)
True
>>> np.bool(0)
False
>>> np.float64(False)
0.0
>>> np.int64(True)
1
>>> np.float64([1, 2, 3])
array([ 1.,  2.,  3.])
>>> np.int16([1, 2, 3])
array([1, 2, 3], dtype=int16)

Если нужна информация о каком-то конкретном типе чисел с плавающей запятой, то ее можно запросить с помощью функции finfo():

>>> np.finfo(np.float64)
finfo(resolution=1e-15, min=-1.7976931348623157e+308, max=1.7976931348623157e+308, dtype=float64)
>>>
>>> np.finfo(np.floаt)
finfo(resolution=1e-15, min=-1.7976931348623157e+308, max=1.7976931348623157e+308, dtype=float64)
>>>
>>> np.finfo(np.float16)
finfo(resolution=0.0010004, min=-6.55040e+04, max=6.55040e+04, dtype=float16)

Многие функции в NumPy принимают тип данных в качестве необязательного аргумента:

>>> a = np.arange(5, dtype = np.int64)    #  Cоздали массив из чисел типа int64
>>> a
array([0, 1, 2, 3, 4], dtype=int64)
>>> 
>>> a = np.arange(5, dtype = np.int8)    #  Cоздали массив из чисел типа int8
>>> a
array([0, 1, 2, 3, 4], dtype=int8)

Иногда бывает очень полезно узнать размер массива в байтах:

>>> a = np.arange(5, dtype = np.float64)
>>> a
array([ 0.,  1.,  2.,  3.,  4.])
>>> a.size    #  Общее количество элементов в массиве
5
>>> a.itemsize     #  Размер одного элемента в байтах
8
>>> a.size*a.itemsize
40

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

>>> #  Если размер элемента в байтах больше
... #  выделяемой памяти, то это приведет к потере данных
... 
>>> a = np.array([2, 8, 1024, 2048], dtype = np.int8)
>>> a
array([2, 8, 0, 0], dtype=int8)

int8 может хранить целые числа в диапазоне от -128 по 127 включительно, мы же попытались сохранить в массив два числа 1024 и 2048, которые явно этот диапазон превышают. Причем, обратите внимание - ошибки не произошло, эти два числа просто заменились нулями т.е. понять, что что-то пошло не так можно только по завершении вычислений. Обычно, программисты на языке Python быстро привыкают не беспокоиться о типах данных и величине выделяемой памяти под каждое число. И это очень удобно, негативно сказывается на скорости вычислений, но, действительно, удобно. Здесь же на плечи программиста ложится дополнительная ответственность за возможные ошибки. В то же время появляется дополнительная возможность экономить определенное количество памяти, что в некоторых ситуациях, может быть чрезвычайно выгодно.

Так же следует помнить об обратной несовместимости некоторых типов данных:

>>> a = np.array([1, 2, 3])
>>> a                      #  Массив целых чисел
array([1, 2, 3])
>>> a = np.complex64(a)    #  может быть легко конвертирован в массив комплексных чисел
>>> a
array([ 1.+0.j,  2.+0.j,  3.+0.j], dtype=complex64)
>>> a = np.int16(a)        #  Но при обратном конвертировании мнимая часть будет отброшена
__main__:1: ComplexWarning: Casting complex values to real discards the imaginary part
>>> a
array([1, 2, 3], dtype=int16)

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

То же самое произойдет при преобразовании чисел с плавающей запятой в целые числа - дробные части будут просто отброшены:

>>> a = np. array([1.1, 2.2, 3.3])
>>> a
array([ 1.1,  2.2,  3.3])
>>> a = np.int8(a)
>>> a
array([1, 2, 3], dtype=int8)

12.1. Символьные коды типов данных

У числовых типов данных имеются свои символьные коды. Использовать данные коды не рекомендуется и здесь они представлены лишь потому, что они иногда появляются в других источниках. У NumPy имеется предшественник - Numeric и символьные коды существуют лишь для обратной совместимости с ним (и некоторыми другими старыми пакетами тоже). В своем коде лучше всего использовать объекты dtype.

Ниже представлены символьные коды различных типов данных:

Тип данных Символьный код
Целые числа i, i1, i2, i4, i8
Беззнаковые числа u1, u2, u4, u8
Вещественные числа одинарной точности f, f2, f4, f8
Вещественные числа двойной точности d
Булевы значения b
Комплексные числа D
Строки S
Символы юникода U
Пустое значение V

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

С одной стороны, с помощью таких символьных кодов определять типы данных вроде бы проще, но с другой стороны они являются менее информативными чем объекты dtype:

>>> a = np.arange(5, dtype = 'i')
>>> a
array([0, 1, 2, 3, 4], dtype=int32)
>>> 
>>> a = np.arange(5, dtype = 'i8')
>>> a
array([0, 1, 2, 3, 4], dtype=int64)
>>> 
>>> a = np.arange(5, dtype = 'f')
>>> a
array([ 0.,  1.,  2.,  3.,  4.], dtype=float32)
>>> 
>>> a = np.arange(5, dtype = 'u')    #  Может возникнуть ошибка
Traceback (most recent call last):
  File "", line 1, in 
TypeError: data type "u" not understood
>>>
>>> a = np.arange(5, dtype = 'u1')    #  Так ошибки нет
>>> a
array([0, 1, 2, 3, 4], dtype=uint8)
>>>
>>> a = np.arange(5, dtype = 'b')    
>>> a     #  Получается нечто не совсем логичное
array([0, 1, 2, 3, 4], dtype=int8)
>>> #  Хотя если использовать объект dtype
... #  то сразу появится ошибка с сообщением
... #  о том, что функции заполнения для такого типа данных нет:
... 
>>> a = np.arange(5, dtype = np.bool)
Traceback (most recent call last):
  File "", line 1, in 
ValueError: no fill-function for data-type.

Все же, в своем коде, лучше использовать объекты dtype - для NumPy они более естественны чем символьные коды. Да и путаницы с объектами dtype гораздо меньше.