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.