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.