Search code examples
pythonpython-dataclasses

In Python, how do I modify one object from a list without modifying others?


First of all, I am aware of this question.

I have the same problem. I am modifying an object "Cell" one at a time from a list of said cells. But the problem is modifying one maps all the modifications to every other element of the list too. Deepcopy is not a viable solution for me, as it increases the computation time by way too much.

The problem is simple, and I understand it, all the cells in the list point to same object. But I really don't understand how. For example list is a object in Python, but when i modify a list, not all lists are modified right? Sorry if I sound dumb, but I think I don't really understand how objects in Python are dealt with. Please provide any insight that might be helpful. I will post my code below. You can find a TL;DR after all the code snippets.

First of all the classes that are used:

@dataclass(slots=True)
class Leaves:
   nlen: int = 0
   leaf_cells: List['Cell'] = None # containing cell objects


@dataclass(slots=True)
class Neighbor:
   n: int = 0
   idx: np.ndarray = None



@dataclass(slots=True)
class Cell:
   xmin: float = 0.0
   xmax: float = 0.0
   ymin: float = 0.0
   ymax: float = 0.0
   val: np.ndarray = np.zeros(NumOfBasicParam)
   using: bool = False
   converged: bool = False
   id: int = -1
   order: int = 0
   nChildren: int = 0
   nOffspring: int = 0
   nleaves: int = 0
   parent: Optional["Cell"] = None
   children: List["Cell"] = field(default_factory=list)
   inner: Neighbor = field(default_factory=Neighbor)
   outer: Neighbor = field(default_factory=Neighbor)
   below: Neighbor = field(default_factory=Neighbor)
   above: Neighbor = field(default_factory=Neighbor)
   around: Neighbor = field(default_factory=Neighbor)

Now the functions to make leaves

def grid_make_leaves(croot: Cell
                     ) -> Tuple[Cell, Leaves, int]:
    
    leaves = Leaves(croot.nleaves)
    # for i in range(leaves.nlen):
    #     leaves.list[i].id = - 1

    # leaves.nlen = croot.nleaves

    leaves.list = [Cell() for i in range(leaves.nlen)]

    idx = -1

    croot, leaves, idx = grid_add_leaves(croot, leaves, idx)

    return croot, leaves, idx

def grid_add_leaves(c: Cell,
                    leaves: Leaves,
                    idx: int,
                    ) -> Tuple[Cell, Leaves, int]:
    
    if c.using:
        idx = idx + 1
        leaves.list[idx] = c
        leaves.list[idx].id = idx

        return c, leaves, idx
    else:
        for i in range(c.nChildren):
            c.children[i], leaves, idx = grid_add_leaves(c.children[i], leaves, idx)

    return c, leaves, idx

Finally function which makes neighbors.

def grid_make_neighbors(grid_config: GridConfig,
                        leaves: Leaves
                        ) -> Leaves:
    
    max_neigh = 0
    idx = 0
    leaves_dict = asdict(leaves)
    # print(leaves_dict.keys())
    # instead of using leaves objects all the time
    # use something like for leaf_cell in leaves.list:
    # for i in tqdm(range(leaves.nlen)):
    for i, c in tqdm(enumerate(leaves.list)):
        
        nnei_max = 512

        idx_inner = np.zeros(nnei_max)
        idx_outer = np.zeros(nnei_max)
        idx_below = np.zeros(nnei_max)
        idx_above = np.zeros(nnei_max)

        c.inner.n = 0
        c.outer.n = 0
        c.above.n = 0
        c.below.n = 0
        c.around.n = 0

        for j, c2 in enumerate(leaves.list):

            if i == j:
                continue
                
            tol = min(1e-3 * min(c.xmax - c.xmin, c.ymax - c.ymin, 
                                 c2.xmax - c2.xmin, c2.ymax - c2.ymin),
                                 grid_config.very_small_len)

            neighbor_cell, pos, frac = is_neighbor(c, c2, tol)

            if neighbor_cell:

                match pos:
                    case 1:
                        c.inner.n = c.inner.n + 1
                        if c.inner.n > nnei_max:
                            print("In make_neighbors: ")
                            print("c.inner.n too large", c.inner.n, nnei_max)
                            exit()
                        idx_inner[c.inner.n-1] = j + 1
                    
                    case 2:
                        c.outer.n = c.outer.n + 1
                        if c.outer.n > nnei_max:
                            print("In make_neighbors: ")
                            print("c.outer.n too large", c.inner.n, nnei_max)
                            exit()
                        idx_outer[c.outer.n-1] = j + 1

                    case 3:
                        c.below.n = c.below.n + 1
                        if c.below.n > nnei_max:
                            print("In make_neighbors: ")
                            print("c.below.n too large", c.inner.n, nnei_max)
                            exit()
                        idx_below[c.below.n-1] = j + 1

                    case 4:
                        c.above.n = c.above.n + 1
                        if c.above.n > nnei_max:
                            print("In make_neighbors: ")
                            print("c.below.n too large", c.inner.n, nnei_max)
                            exit()
                        idx_above[c.above.n-1] = j + 1

        c.around.n = c.inner.n + c.outer.n + c.above.n + c.below.n
        if c.around.n > 0:
            c.around.idx = np.zeros(c.around.n)

        k = 0

        if c.above.n > 0:
            c.above.idx = idx_above[0:c.above.n]
            c.around.idx[k:k+c.above.n] = c.above.idx
            k = k + c.above.n
        
        if c.inner.n > 0:
            c.inner.idx = idx_inner[0:c.inner.n]
            c.around.idx[k:k+c.inner.n] = c.inner.idx
            k = k + c.inner.n
        
        if c.outer.n > 0:
            c.outer.idx = idx_outer[0:c.outer.n]
            c.around.idx[k:k+c.outer.n] = c.outer.idx
            k = k + c.outer.n

        if c.below.n > 0:
            c.below.idx = idx_below[0:c.below.n]
            c.around.idx[k:k+c.below.n] = c.below.idx
            k = k + c.below.n

        if max_neigh < c.around.n:
            max_neigh = c.around.n
            idx = i
            print(idx, c.above.n, c.below.n, c.inner.n, c.outer.n, c.around.n)
        

        # leaves.list[i] = copy.deepcopy(c)

        # if i == 30:
        #     break
        # print(leaves.list[8559].above.n, leaves.list[8559].below.n, leaves.list[8559].inner.n, leaves.list[8559].outer.n, leaves.list[8559].around.n)
            
    print("Max number of neighbors:", max_neigh)
    print("owned by this one: xmin,xmax,ymin,ymax", c.xmin, c.xmax, c.ymin, c.ymax)
    # print(idx, leaves_new.list[idx].above.n, leaves_new.list[idx].below.n, leaves_new.list[idx].inner.n, leaves_new.list[idx].outer.n, leaves_new.list[idx].around.n) # Prints the correct number of neighbors
    

    return leaves

It is fine if you did not go through the code: leaves is an instance of Leaves object with an attribute list. leaves.list is a list of Cell type objects. When I modify each Cell object of leaves.list in a loop, all members of leaves.list are modified. I would like only the member which I'm using at any given moment is modified. Deepcopy is too slow, so not a viable solution. In the process, I would really like if someone can properly explain why and how Python objects work like this.

Thank you.


Solution

  • Instead of using default_factory or initialising attributes as objects when defining the dataclass was causing the issue. On the advice of Tim, I changed everything to None by default, and initialised them as Neighbor objects only when I needed them.

    So only following changes were made:

    @dataclass(slots=True)
    class Cell:
       xmin: float = 0.0
       xmax: float = 0.0
       ymin: float = 0.0
       ymax: float = 0.0
       val: np.ndarray = np.zeros(NumOfBasicParam)
       using: bool = False
       converged: bool = False
       id: int = -1
       order: int = 0
       nChildren: int = 0
       nOffspring: int = 0
       nleaves: int = 0
       parent: Optional["Cell"] = None
       children: List["Cell"] = None
       inner: Neighbor = None
       outer: Neighbor = None
       below: Neighbor = None
       above: Neighbor = None
       around: Neighbor = None