2025, Nov 07 06:02
NumPy и атрибуты класса в Python: причина общего состояния и решение
Разбираем, почему NumPy-массив в атрибуте класса приводит к общему состоянию всех экземпляров, и как это исправить: перенос в __init__, dtype=object, пример.
Когда вы создаёте контейнеры с пользовательскими объектами Python рядом с массивами NumPy, легко столкнуться с неожиданным «общим состоянием». Типичный признак — меняете один объект, а изменения проявляются во всех остальных. Причина не в механизме broadcasting NumPy, а в том, как атрибуты объявлены в самом классе.
Постановка задачи
Предположим, есть класс, который создаёт массив NumPy 3×2 и идентификатор, а также контейнер фиксированного размера на четыре экземпляра. Вы присваиваете значение в массив только одного экземпляра — и неожиданно видите это изменение у всех объектов.
import numpy as np
class Vertex():
matrix = np.zeros((3, 2))
tag = 0
verts = np.empty((4)).astype(Vertex)
for k in range(4):
v = Vertex()
v.tag = k
verts[k] = v
# Позже кажется, что одно обновление затрагивает все элементы:
verts[1].matrix[0] = [5, 5]
Для сравнения, «чистый» подход на NumPy работает нормально, потому что каждый подмассив занимает свою область памяти и не связан с объектами Python.
import numpy as np
arr = np.zeros((4, 3, 2))
def dump(x):
for a in range(4):
print("[", end="")
for b in range(3):
print(x[a][b], end="")
print("] ")
print()
dump(arr)
print("set arr[1][0] = [5, 5]...")
arr[1][0] = [5, 5]
dump(arr)
Почему так происходит
Массив и идентификатор заданы на уровне класса, а не конкретного экземпляра. Атрибуты, объявленные в классе, общие для всех экземпляров, поэтому каждый объект ссылается на один и тот же массив NumPy. Любое присваивание через любой экземпляр меняет этот общий массив — из‑за этого кажется, что изменились все объекты сразу.
Кроме того, если вы создаёте массив для хранения объектов Python, NumPy ожидает массив с типом object. Обычный числовой dtype или приведение через класс не превращают его в настоящий контейнер независимых Python‑объектов.
Исправление и рабочий пример
Перенесите массив и идентификатор в конструктор, чтобы у каждого экземпляра было своё состояние, а контейнер сделайте массивом объектов. Тогда изменение «на месте» в одном экземпляре останется локальным.
import numpy as np
class Vertex:
def __init__(self):
self.matrix = np.zeros((3, 2))
self.tag = 0
verts = np.empty((4,), dtype=object) # обратите внимание на запятую в форме и на dtype=object
for k in range(4):
v = Vertex()
v.tag = k
verts[k] = v
# Теперь одно обновление затрагивает только один экземпляр:
verts[1].matrix[0] = [5, 5]
Такой подход гарантирует, что у каждого объекта свой массив NumPy, и исключает «перекрёстное влияние» между экземплярами. Тип object у внешнего контейнера делает его корректным хранилищем произвольных Python‑объектов.
Зачем это важно
Случайное общее состояние может незаметно портить вычислительные процессы в численных задачах и симуляциях, где изменяемые массивы часто обновляются на месте. Понимание разницы между атрибутами класса и экземпляра, а также использование массивов object, когда действительно нужна коллекция Python‑объектов, помогает избежать тонких и трудноловимых багов.
Выводы
Данные каждого объекта размещайте в конструкторе — так у экземпляров будут собственные копии. Если нужен контейнер NumPy для Python‑объектов, явно создавайте его с dtype=object и корректной формой. Следуя этим двум правилам, вы избежите ситуации, когда изменение одного объекта непредсказуемо «разливается» на остальные.
Материал основан на вопросе на StackOverflow от пользователя user1069353 и ответе Uchenna Adubasim.