When will changing an object also change the copy of the object?

Viewed 100

I am confused by the copy() function. As I understood, = is pointer style assignment and deepcopy() is creating a new independent copy. However, I found copy() is not very "stable". Please see the following two examples:

b = [[1,2,3], [4,5,6]];
a = copy(b);
b[1][1] = 10;
a
b

In the example above, a also changed after the assignment of b[1][1]

While in the second example:

b = [[1,2,3], [4,5,6]];

a = copy(b);

b[1] = [10,2,3];
a
b

The assignment of b[1] does not really change a. This is really confusing. Can anyone explain briefly what is happening? Thank you!

2 Answers

copy craetes a shallow copy and hence in your case references to the object are copied rather than real data. This happens because your b is a Vector of Vectors so this is storied as:

b = [<reference to the first vector>, <reference to the second vector>]

When you create a shallow-copy only those references are being copied but no the underlying data. Hence the copied references still point to the same memory address.

In your second example you are replacing the actual reference. Since the object a holds a copy of the reference, replacing the entire reference in b is not seen in a.

This behavior will be seen everywhere where you have "objects inside objects" data structure. On the other hand if you have arrays of primitives (on references) you will get an actual copy such as:

julia> a = [1 3; 3 4]
2×2 Matrix{Int64}:
 1  3
 3  4

julia> b = copy(a); b[1,1] = 100
100

julia> a
2×2 Matrix{Int64}:
 1  3
 3  4

This is a more detailed explanation of the differences between the equal sign, copy and deepcopy functions, extracted from the chapter 2 of my "Julia Quick Syntax Reference: A Pocket Guide for Data Science Programming" (Apress 2019) book:

Memory and copy issues

In order to avoid copying large amount of data, Julia by default copies only the memory address of objects, unless the programmer explicitly request a so-called "deep" copy or the compiler "judges" an actual copy more efficient.

Use copy() or deepcopy() when you don't want that subsequent modifications to the copied object would apply to the original object.

In details:

Equal sign (a=b)

  • performs a name binding, i.e. binds (assigns) the entity (object) referenced by b also to the a identifier (the variable name)
  • it results that:
    • if b then rebinds to some other object, a remains referenced to the original object
    • if the object referenced by b mutates (i.e. it internally changes), so does (being the same object) those referenced by a
  • if b is immutable and small in memory, under some circumstances, the compiler would instead create a new object and bind it to a, but being immutable for the user this difference would not be noticeable
  • as for many high level languages, we don't need to explicitly worry about memory leaks. A Garbage Collector exists such that objects that are no longer accessible are automatically destroyed.

a = copy(b)

  • creates a new, "independent" copy of the object and bind it to a. This new object may however reference in turn other objects trough their memory address. In this case it is their memory address that is copied and not the referenced objects themselves.
  • it results that:
    • if these referenced objects (e.g. the individual elements of a vector) are rebound to some other objects, the new object referenced by a maintains the reference to the original objects
    • if these referenced objects mutate, so do (being the same objects) those referenced by the new object referenced by a

a = deepcopy(b)

  • everything is deep copied recursively

The following code snippet highlights the differences between these three methods of "copying" an object:

julia> a = [[[1,2],3],4]
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
julia> b = a
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
julia> c = copy(a)
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
julia> d = deepcopy(a)
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
# rebinds a[2] to an other objects.
# At the same time mutates object a:
julia> a[2] = 40
40
julia> b
2-element Array{Any,1}:
   Any[[1, 2], 3]
 40
julia> c
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
julia> d
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
# rebinds a[1][2] and at the same
# time mutates both a and a[1]:
julia> a[1][2] = 30
30
julia> b
2-element Array{Any,1}:
   Any[[1, 2], 30]
 40
julia> c
2-element Array{Any,1}:
  Any[[1, 2], 30]
 4
julia> d
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
# rebinds a[1][1][2] and at the same
# time mutates a, a[1] and a[1][1]:
julia> a[1][1][2] = 20
20
julia> b
2-element Array{Any,1}:
   Any[[1, 20], 30]
 40
julia> c
2-element Array{Any,1}:
  Any[[1, 20], 30]
 4
julia> d
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4
# rebinds a:
julia> a = 5
5
julia> b
2-element Array{Any,1}:
   Any[[1, 20], 30]
 40
julia> c
2-element Array{Any,1}:
  Any[[1, 20], 30]
 4
julia> d
2-element Array{Any,1}:
  Any[[1, 2], 3]
 4

We can check if two objects have the same values with == and if two objects are actually the same with === (in the sense that immutable objects are checked at the bit level and mutable objects are checked for their memory address):

  • given a = [1, 2]; b = [1, 2]; a == b and a === a are true, but a === b is false;
  • given a = (1, 2); b = (1, 2); all a == b, a === a and a === b are true.
Related