2025, Nov 16 21:02

Как сравнить списки массивов NumPy без учёта порядка

Почему in и set не работают с numpy.ndarray, и как безопасно сравнить списки массивов NumPy без учёта порядка, преобразуя их в кортежи с flatten надёжно.

Проверить, содержат ли два списка Python одни и те же массивы NumPy независимо от порядка, кажется простым, пока не попробуешь очевидные инструменты. Множества не подходят, потому что numpy.ndarray не хешируется, а даже простая проверка принадлежности через in рушится из‑за поэлементной семантики NumPy. Давайте разберёмся, почему так происходит, и как сравнивать такие коллекции безопасно и предсказуемо.

Постановка задачи

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

import numpy as np
arrs_a = [np.array([1, 2]), np.array([3])]
arrs_b = [np.array([3]), np.array([1, 2])]
arrs_c = [np.array([2, 1]), np.array([3])]

На первый взгляд, прямой подход через проверку вхождения выглядит разумно, но он ломается:

def compare_unsafely(lst_a, lst_b):
    for block in lst_a:
        if block not in lst_b:
            return False
    for block in lst_b:
        if block not in lst_a:
            return False
    return True
# compare_unsafely(arrs_a, arrs_b)  # вызывает ошибку во время выполнения

Запуск такой проверки вхождения приводит к ошибке:

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

А попытка применить множества, чтобы игнорировать внешний порядок, ломается ещё раньше:

TypeError: unhashable type: numpy.ndarray

Почему это не работает

Операторы NumPy, такие как ==, векторизованы: сравнение массивов поэлементно возвращает другой массив булевых значений, а не единичное True/False. В основе оператора in в Python лежит проверка равенства. Когда он пытается интерпретировать результат поэлементного сравнения в булевом контексте, NumPy отказывается, и вы получаете ошибку об неоднозначном булевом значении.

Подход с множествами не срабатывает, потому что numpy.ndarray не является хешируемым. Контейнеры на основе хеша, такие как set и dict, требуют стабильного хеша; у ndarray его нет, поэтому возникает ошибка unhashable type.

Решение

Надёжный способ сравнивать такие коллекции без учёта порядка — преобразовать каждый массив в хешируемое, неизменяемое представление его содержимого, собрать эти представления в множества и сравнить множества. Для этого хорошо подходят кортежи. Предварительное «выпрямление» (flatten) каждого массива гарантирует, что мы сравниваем фактическую последовательность значений в единой линейной форме.

import numpy as np
def same_payloads(seq_a, seq_b):
    pool_a = {tuple(item.flatten()) for item in seq_a}
    pool_b = {tuple(obj.flatten()) for obj in seq_b}
    return pool_a == pool_b
bags_1 = [np.array([1, 2]), np.array([3])]
bags_2 = [np.array([3]), np.array([1, 2])]
result = same_payloads(bags_1, bags_2)
print(result)
# Истина

Такое сравнение игнорирует порядок массивов во внешнем списке, но сохраняет порядок внутри каждого массива, потому что кортежи удерживают порядок элементов. Например, массив со значениями [2, 1] не будет совпадать с [1, 2].

Почему это важно

Легко предположить, что проверки вхождения и операции с множествами в Python будут работать с массивами NumPy «как обычно», но семантика NumPy намеренно другая. Опираться на поведение по умолчанию — значит получать непонятные ошибки вместо ясных ответов True/False. Преобразование массивов в кортежи делает намерение явным, а результат — детерминированным.

Итоги

Если нужно сравнить два списка массивов NumPy как неупорядоченные коллекции, сохраняя порядок внутри каждого массива, сначала сопоставьте каждый массив с кортежем его «выпрямленных» значений, а затем сравните получившиеся множества. Это обходит и проблему неоднозначного булева значения, и отсутствие хеша у ndarray — при этом исходные данные не меняются.