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 — при этом исходные данные не меняются.