When doing some first coding with Julia, I run into an issue that a function argument was changed unexpectedly. I could boil the problem down to the following lines of code:
function myfunc(f,n)
q = f
for k in 1:n
q[k] += + 1.1
end
return q
end
n = 1
f = ones(1)
println(“f =”,f)
my_q = myfunc(f,n)
println(“After calling of myfunc(f,n) f is modified:”)
println(“f =”,f)
=========== output ========
f =[1.0]
After calling of myfunc(f,n) f is modified:
f =[2.1]
My question is why f in myfunc(f,n) is changed from 1.0 to 2.1? This is completely unexpected for me, because the function myfunc() should calculate q but should not change the argument f.
I’m not sure if this is a bug or a feature of Julia. I’ve used Julia version 1.7.1.
When you type q = f you are making q point to the same array as f. So whatever changes you make to q are also visible in f. This is extremely useful, but you must be aware of it.
Whenever f is a mutable data structure, q = f followed by mutation of q will be visible in f.
If you want to avoid this, write q = copy(f) instead. (In some cases you even need q = deepcopy(f).)
help?> deepcopy
search: deepcopy
deepcopy(x)
Create a deep copy of x: everything is copied recursively, resulting in
a fully independent object. For example, deep-copying an array produces
a new array whose elements are deep copies of the original elements.
Calling deepcopy on an object should generally have the same effect as
serializing and then deserializing it.
While it isn't normally necessary, user-defined types can override the
default deepcopy behavior by defining a specialized version of the
function deepcopy_internal(x::T, dict::IdDict) (which shouldn't
otherwise be used), where T is the type to be specialized for, and dict
keeps track of objects copied so far within the recursion. Within the
definition, deepcopy_internal should be used in place of deepcopy, and
the dict variable should be updated as appropriate before returning.
Maybe a follow up question is then; for which cases would one want to use “copy” only?
Seems to me that copying the outer shell but keeping the inner references, would not have any practical use case? (I.e. I am asking; when would I prefer to use “copy” and not “deepcopy”?)
It’s a good question. Someone recently suggested that they should be called copy and shallowcopy. I have noticed that copy is (ever so slightly) faster and allocates less than deepcopy for Float64 array. Which is strange. I would have thought that deepcopy(::Array{Float64}) dispatched to copy.
There is an old Julia issue somewhere where Jeff and Tim Holy agree that deepcopy is sort of a lower level function that should rarely be used. I didn’t understand the arguments, but if they agreed, I agree . (couldn’t find the issue right now).
For my use cases I always want to cut the connections, so I think I will stick to deepcopy always - copy makes me a bit uneasy, since I cannot figure out when it is useful to have the same underlying object with two names.
In “flat code” it is not very useful, probably. But that is consistent with what happens on function calls, in which mutable structure are not copied (which would imply possibly huge allocations in every call):
julia> f(x) = x .= .+ 1 # mutates x
f (generic function with 1 method)
julia> a = zeros(3);
julia> f(a);
julia> a
3-element Vector{Float64}:
1.0
1.0
1.0
Thus, for a mutable structure, the label assigned to it is just a label. And some care has to be taken with that.
The same happens if you share a big array within two structures:
julia> struct A
x::Vector{Float64}
end
julia> struct B
x::Vector{Float64}
end
julia> a = A(rand(3)); # could be very large
julia> b = B(a.x);
julia> a.x
3-element Vector{Float64}:
0.008402191874595566
0.027938927073541286
0.8813538319613927
julia> a.x[1] = 0.
0.0
julia> b.x
3-element Vector{Float64}:
0.0
0.027938927073541286
0.8813538319613927
This is about being efficient in memory management.