Повышение типа данных в NumPy#
При смешивании двух разных типов данных NumPy должен определить подходящий dtype для результата операции. Этот шаг называется повышение или нахождение общего типа данных.
В типичных случаях пользователю не нужно беспокоиться о деталях повышения типа, поскольку шаг повышения обычно гарантирует, что результат будет либо соответствовать, либо превышать точность входных данных.
Например, когда входные данные имеют одинаковый dtype, dtype результата совпадает с dtype входных данных:
>>> np.int8(1) + np.int8(1)
np.int8(2)
Смешивание двух разных dtypes обычно даёт результат с dtype входных данных с более высокой точностью:
>>> np.int8(4) + np.int64(8) # 64 > 8
np.int64(12)
>>> np.float32(3) + np.float16(3) # 32 > 16
np.float32(6.0)
В типичных случаях это не приводит к неожиданностям. Однако если вы работаете с нестандартными типами данных, такими как беззнаковые целые числа и числа с плавающей точкой низкой точности, или если вы смешиваете целые числа NumPy, числа с плавающей точкой NumPy и скаляры Python, некоторые детали правил продвижения типов NumPy могут быть важны. Обратите внимание, что эти подробные правила не всегда совпадают с правилами других языков [1].
Числовые типы данных делятся на четыре "вида" с естественной иерархией.
беззнаковые целые числа (
uint)знаковые целые числа (
int)float (
float)комплексный (
complex)
В дополнение к типу, числовые типы данных NumPy также имеют связанную точность, указанную в битах. Вместе тип и точность определяют dtype. Например,
uint8 является беззнаковым целым числом, хранящимся с использованием 8 бит.
Результат операции всегда будет иметь тип, равный или более высокий, чем любой из входных данных. Кроме того, результат всегда будет иметь точность, большую или равную точности входных данных. Это уже может привести к некоторым примерам, которые могут быть неожиданными:
При смешивании чисел с плавающей точкой и целых чисел точность целого числа может вынудить результат к более высокой точности с плавающей точкой. Например, результат операции, включающей
int64иfloat16являетсяfloat64.При смешивании беззнаковых и знаковых целых чисел с одинаковой точностью результат будет иметь выше точность выше, чем у любого из входов. Кроме того, если один из них уже имеет 64-битную точность, более высокоточное целое число недоступно, и, например, операция с участием
int64иuint64даетfloat64.
Пожалуйста, ознакомьтесь с Числовое повышение раздел и изображение ниже для подробностей о обоих.
Подробное поведение скаляров Python#
Начиная с NumPy 2.0 [2], важный момент в наших правилах продвижения заключается в том, что хотя операции с двумя типами данных NumPy никогда не теряют точность, операции с типом данных NumPy и скаляром Python (int, float,
или complex) может потерять точность. Например, интуитивно понятно, что результат операции между целым числом Python и целым числом NumPy должен быть целым числом NumPy. Однако целые числа Python имеют произвольную точность, тогда как все типы данных NumPy имеют фиксированную точность, поэтому произвольная точность целых чисел Python не может быть сохранена.
В более общем случае NumPy учитывает «тип» скаляров Python, но игнорирует их точность при определении результирующего типа данных. Это часто удобно. Например, при работе с массивами типа данных низкой точности обычно желательно, чтобы простые операции со скалярами Python сохраняли тип данных.
>>> arr_float32 = np.array([1, 2.5, 2.1], dtype="float32")
>>> arr_float32 + 10.0 # undesirable to promote to float64
array([11. , 12.5, 12.1], dtype=float32)
>>> arr_int16 = np.array([3, 5, 7], dtype="int16")
>>> arr_int16 + 10 # undesirable to promote to int64
array([13, 15, 17], dtype=int16)
В обоих случаях точность результата определяется типом данных NumPy. Из-за этого arr_float32 + 3.0 ведет себя так же, как
arr_float32 + np.float32(3.0), и arr_int16 + 10 ведет себя как
arr_int16 + np.int16(10.).
В качестве другого примера, при смешивании целых чисел NumPy с Python float
или complex, результат всегда имеет тип float64 или complex128:
>> np.int16(1) + 1.0 np.float64(2.0)
Однако эти правила также могут приводить к неожиданному поведению при работе с типами данных низкой точности.
Во-первых, поскольку значение Python преобразуется в NumPy перед выполнением операции,
операции могут завершиться ошибкой, когда результат кажется
очевидным. Например, np.int8(1) + 1000 не может продолжить, потому что 1000
превышает максимальное значение int8. Когда скаляр Python
не может быть преобразован в NumPy dtype, возникает ошибка:
>>> np.int8(1) + 1000
Traceback (most recent call last):
...
OverflowError: Python integer 1000 out of bounds for int8
>>> np.int64(1) * 10**100
Traceback (most recent call last):
...
OverflowError: Python int too large to convert to C long
>>> np.float32(1) + 1e300
np.float32(inf)
... RuntimeWarning: overflow encountered in cast
Во-вторых, поскольку точность Python float или integer всегда игнорируется, скаляр NumPy с низкой точностью будет продолжать использовать свою более низкую точность, если явно не преобразован в тип данных NumPy с более высокой точностью или скаляр Python (например, через int(),
float(), или scalar.item()). Эта более низкая точность может быть вредна для
некоторых вычислений или привести к неверным результатам, особенно в случае целочисленного
переполнения:
>>> np.int8(100) + 100 # the result exceeds the capacity of int8
np.int8(-56)
... RuntimeWarning: overflow encountered in scalar add
Обратите внимание, что NumPy предупреждает о переполнениях для скаляров, но не для массивов; например, np.array(100, dtype="uint8") + 100 будет не warn.
Числовое повышение#
На следующем изображении показаны правила числового повышения с видами на вертикальной оси и точностью на горизонтальной оси.
Входной dtype с более высоким видом определяет вид результирующего dtype. Результирующий dtype имеет минимально возможную точность, не появляясь слева от любого входного dtype на диаграмме.
Обратите внимание на следующие конкретные правила и наблюдения:
Когда Python
floatилиcomplexвзаимодействует с целым числом NumPy, результат будетfloat64илиcomplex128(желтая граница). Логические значения NumPy также будут приведены к целочисленному типу по умолчанию [3]. Это не имеет значения, когда дополнительно задействованы значения с плавающей точкой NumPy.Точность выбирается так, что
float16 < int16 < uint16потому что большиеuint16не подходятint16и большиеint16потеряет точность при сохранении вfloat16. Однако этот шаблон нарушен, поскольку NumPy всегда считаетfloat64иcomplex128должны быть приемлемыми результатами повышения для любого целочисленного значения.Особый случай заключается в том, что NumPy преобразует многие комбинации знаковых и беззнаковых целых чисел в
float64. Здесь используется более высокий тип, потому что ни один знаковый целочисленный тип данных не является достаточно точным для храненияuint64.
Исключения из общих правил повышения#
В NumPy повышение относится к тому, что конкретные функции делают с результатом, и в некоторых случаях это означает, что NumPy может отклоняться от того, что np.result_type даст.
Поведение sum и prod#
np.sum и np.prod всегда будет возвращать тип целого числа по умолчанию
при суммировании целых значений (или булевых). Обычно это int64.
Причина этого в том, что суммирование целых чисел в противном случае очень вероятно приведет к переполнению и даст запутанные результаты.
Это правило также применяется к базовому np.add.reduce и
np.multiply.reduce.
Примечательное поведение со скалярами NumPy или Python integer#
Продвижение в NumPy относится к результирующему dtype и точности операции, но операция иногда диктует этот результат. Деление всегда возвращает значения с плавающей точкой, а сравнение — булевы значения.
Это приводит к тому, что могут возникать «исключения» из правил:
Сравнения NumPy с целыми числами Python или целыми числами смешанной точности всегда возвращают правильный результат. Входные данные никогда не будут приведены таким образом, чтобы потерять точность.
Сравнения равенства между типами, которые нельзя преобразовать, будут считаться все
False(равенство) или всеTrue(не равно).Унарные математические функции, такие как
np.sinкоторые всегда возвращают значения с плавающей точкой, принимают любой целочисленный ввод Python, преобразуя его вfloat64.Деление всегда возвращает значения с плавающей точкой и, таким образом, также позволяет деление между любым целым числом NumPy и любым целым числом Python путем приведения обоих к
float64.
В принципе, некоторые из этих исключений могут иметь смысл для других функций. Пожалуйста, создайте issue, если вы считаете это уместным.
Примечательное поведение с встроенными классами типов Python#
При комбинировании встроенных скалярных типов Python типы (т.е., float, int,
или complex, не скаляр values), правила продвижения могут показаться
неожиданными:
>>> np.result_type(7, np.array([1], np.float32))
dtype('float32') # The scalar value '7' does not impact type promotion
>>> np.result_type(type(7), np.array([1], np.float32))
dtype('float64') # The *type* of the scalar value '7' does impact promotion
# Similar situations happen with Python's float and complex types
Причина такого поведения заключается в том, что NumPy преобразует int к его стандартному целочисленному типу и использует этот тип для повышения:
>>> np.result_type(int)
dtype('int64')
Смотрите также Встроенные типы Python для получения дополнительной информации.
Поддержка нечисловых типов данных#
NumPy расширяет приведение к нечисловым типам, хотя во многих случаях приведение не определено чётко и просто отклоняется.
Применяются следующие правила:
Строки байтов NumPy (
np.bytes_) могут быть преобразованы в строки Unicode (np.str_). Однако, приведение байтов к юникоду не удастся для не-ASCII символов.Для некоторых целей NumPy будет приводить почти любой другой тип данных к строкам. Это применяется при создании или конкатенации массивов.
Конструкторы массивов, такие как
np.array()будет использоватьobjectdtype, когда нет подходящего повышения типа.Структурированные dtypes могут повышаться, когда их имена полей и порядок совпадают. В этом случае все поля повышаются индивидуально.
NumPy
timedeltaв некоторых случаях может повышать с целыми числами.
Примечание
Некоторые из этих правил несколько неожиданны и рассматриваются для изменения в будущем. Однако любые обратно несовместимые изменения должны быть взвешены против рисков нарушения существующего кода. Пожалуйста, создайте issue, если у вас есть конкретные идеи о том, как должно работать приведение типов.
Детали продвижения dtype экземпляры#
Вышеприведённое обсуждение в основном касалось поведения при смешивании различных классов DType.
A dtype экземпляр, прикреплённый к массиву, может содержать дополнительную информацию,
такую как порядок байтов, метаданные, длина строки или точная структура dtype.
Хотя длина строки или имена полей структурированного типа данных важны, NumPy считает порядок байтов, метаданные и точную структуру структурированного типа данных деталями хранения.
При повышении типа NumPy делает не учитывайте эти детали хранения:
Порядок байтов преобразуется в собственный порядок байтов.
Метаданные, прикрепленные к dtype, могут сохраняться или не сохраняться.
Результирующие структурированные dtypes будут упакованы (но выровнены, если входные данные были).
Такое поведение является оптимальным для большинства программ, где детали хранения не имеют значения для конечных результатов и где использование неправильного порядка байтов может значительно замедлить вычисления.