2025, Oct 15 16:16
Хеширование кортежей из массивов CuPy: как избежать TypeError
TypeError unhashable type в CuPy: почему кортежи из массивов не хешируются и как исправить это через tolist() или приведение к float в структурах данных Python
Переход с NumPy на CuPy обычно проходит без труда, но порой небольшие различия в типах всплывают в самых неожиданных местах. Одна из частых ловушек — попытка хешировать данные, полученные из массивов CuPy. Если вы передаёте во множество вложенные кортежи, собранные из CuPy‑массивов, можно столкнуться с TypeError о «нехешируемом типе».
Обзор проблемы
Задача — переупорядочить столбцы матрицы CuPy и сохранить результат как вложенный кортеж во множестве Python. Прямолинейный приём, который с NumPy работает без нареканий, в случае CuPy приводит к ошибке:
import cupy as cp
def reorder_cols(arr: cp.ndarray):
    col_idx = cp.lexsort(arr[:, 1:])
    arr[:, 1:] = arr[:, 1:][:, col_idx]
    return arr
gpu_mat = cp.array([
    [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.],
    [ 0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0., -1.]
])
seen_patterns = set()
seen_patterns.add(tuple(tuple(row) for row in reorder_cols(gpu_mat)))
Это приводит к:
TypeError: unhashable type: 'ndarray'
Что происходит на самом деле
Вложенный кортеж остаётся хешируемым только тогда, когда хешируемы все его элементы. В NumPy при итерации по строке в таком паттерне обычно получаются питоновские float, а они хешируемы. В CuPy же эти элементы внутри генератора кортежа выходят объектами CuPy, а не Python‑скалярами, и в итоге внутри кортежа оказываются нехешируемые значения. Этого несоответствия достаточно, чтобы вставка во множество проваливалась.
Исправление и рабочий пример
Минимальное решение — перед сборкой кортежей преобразовывать скаляры CuPy в Python‑float или заранее материализовать каждую строку как список Python. Оба способа дают хешируемые элементы. Ниже — компактный вариант без внутреннего генератора: используется tolist(), что немного быстрее и проще:
import cupy as cp
def reorder_cols(arr: cp.ndarray):
    col_idx = cp.lexsort(arr[:, 1:])
    arr[:, 1:] = arr[:, 1:][:, col_idx]
    return arr
gpu_mat = cp.array([
    [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.],
    [ 0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0., -1.]
])
seen_patterns = set()
seen_patterns.add(tuple(tuple(r.tolist()) for r in reorder_cols(gpu_mat)))
Эквивалентный вариант — явно приводить каждый элемент через float(x) при построении вложенного кортежа.
Почему это важно
Миграция с NumPy на CuPy — это не всегда механическая замена импортов. Код, опирающийся на неявное преобразование к питоновским скалярам, может вести себя иначе, когда библиотека возвращает объекты, которые не являются хешируемыми. Это особенно критично, когда вы кладёте производные значения во множества или используете их в качестве ключей словаря. Явное преобразование скаляров делает вычисление хеша предсказуемым и избавляет от неприятных сюрпризов во время выполнения.
Итоги
Если вы формируете хешируемые структуры из массивов CuPy, перед вставкой во множество убедитесь, что элементы — это питоновские скаляры или другие хешируемые типы. Преобразование строк через tolist() либо приведение элементов к float устраняет TypeError без изменения алгоритма. А согласованные с CuPy подсказки типов, вроде def reorder_cols(arr: cp.ndarray), проясняют намерение и снижают риск путаницы при переносе операций с массивами на GPU.
Статья основана на вопросе с StackOverflow от EzTheBoss 2 и ответе от EzTheBoss 2.