Julia 使用 fill() 和 .+= "strange"行为



我观察到"的意外行为+="在我的代码中(可能只有我,我对Julia还很陌生(。考虑以下示例:

julia> b = fill(zeros(2,2),1,3)
1×3 Array{Array{Float64,2},2}:
[0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]
julia> b[1] += ones(2,2)
2×2 Array{Float64,2}:
1.0  1.0
1.0  1.0
julia> b
1×3 Array{Array{Float64,2},2}:
[1.0 1.0; 1.0 1.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]
julia> b[2] .+= ones(2,2)
2×2 Array{Float64,2}:
1.0  1.0
1.0  1.0
julia> b
1×3 Array{Array{Float64,2},2}:
[1.0 1.0; 1.0 1.0]  [1.0 1.0; 1.0 1.0]  [1.0 1.0; 1.0 1.0]

可以看出,最后一个命令不仅改变了b[2]的值,还改变了b[3]的值,而b[1]与之前(*(保持不变,正如我们可以确认的那样:

julia> b[2] .+= ones(2,2)
2×2 Array{Float64,2}:
2.0  2.0
2.0  2.0
julia> b
1×3 Array{Array{Float64,2},2}:
[1.0 1.0; 1.0 1.0]  [2.0 2.0; 2.0 2.0]  [2.0 2.0; 2.0 2.0]

现在,使用简单的"+="相反,我可以获得我所期望的行为+=&";,即:

julia> b = fill(zeros(2,2),1,3); b[2]+=ones(2,2); b
1×3 Array{Array{Float64,2},2}:
[0.0 0.0; 0.0 0.0]  [1.0 1.0; 1.0 1.0]  [0.0 0.0; 0.0 0.0]

有人能解释一下为什么会发生这种事吗?当然,我可以只使用+=,或者可能与数组不同的东西,但由于我追求速度(我有一个代码需要在更大的矩阵上执行数百万次这些操作(和是相当快的,如果我仍然可以利用这个功能的话,我想了解一下。提前感谢大家!

编辑:(*(显然只是因为b[1]不是零。如果我运行:

julia> b = fill(zeros(2,2),1,3); b[2]+=ones(2,2);
julia> b[1] .+= 10 .*ones(2,2); b
[10.0 10.0; 10.0 10.0]  [1.0 1.0; 1.0 1.0]  [10.0 10.0; 10.0 10.0]

您可以看到只有零值发生了更改。这打败了我。

发生这种情况是由于几个因素的结合。让我们试着把事情弄清楚。

首先,b = fill(zeros(2,2),1,3)不为b的每个元素创建新的zeros(2,2);相反,它创建一个2x2的零数组,并将b的所有元素设置为该唯一数组。简而言之,这条线的行为与相当

z = zeros(2,2)
b = Array{Array{Float64,2},2}(undef, 1, 3)
for i in eachindex(b)
b[i] = z
end

因此,修改z[1,1]b[i,j][1,1]中的任何一个也将修改其他值。举例说明:

julia> b = fill(zeros(2,2),1,3)
1×3 Array{Array{Float64,2},2}:
[0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]
# All three elements are THE SAME underlying array
julia> b[1] === b[2] === b[3]
true
# Mutating one of them mutates the others as well
julia> b[1,1][1,1] = 42
42
julia> b
1×3 Array{Array{Float64,2},2}:
[42.0 0.0; 0.0 0.0]  [42.0 0.0; 0.0 0.0]  [42.0 0.0; 0.0 0.0]

第二,b[1] += ones(2,2)等价于b[1] = b[1] + ones(2,2)。这意味着一系列操作:

  1. 创建一个新数组(我们称之为tmp(来保存b[1]ones(2,2)的总和
  2. b[1]反弹到该新阵列,从而失去其到z(或b的所有其他元件(的连接

这是经典主题的变体,尽管两者的符号中都包含=符号,但突变和赋值不是一回事。再次说明:

julia> b = fill(zeros(2,2),1,3)
1×3 Array{Array{Float64,2},2}:
[0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]  [0.0 0.0; 0.0 0.0]
# All elements are THE SAME underlying array
julia> b[1] === b[2] === b[3]
true
# But that connection is lost when `b[1]` is re-bound (not mutated) to a new array
julia> b[1] = ones(2,2)
2×2 Array{Float64,2}:
1.0  1.0
1.0  1.0
# Now b[1] is no more the same underlying array as b[2]
julia> b[1] === b[2]
false
# But b[2] and b[3] still share the same array (they haven't be re-bound to anything else)
julia> b[2] === b[3]
true

第三,b[2] .+= ones(2,2)完全不同。它并不意味着将任何东西重新绑定到新创建的数组;相反,它将阵列b[2]突变到位。它的有效行为类似于:

for i in eachindex(b[2])
b[2][i] += 1  # or b[2][i] = b[2][i] + 1
end

b本身甚至b[2]都不与任何东西重新绑定,只有其中的元素被修改到位。在您的示例中,这也会影响b[3],因为b[2]b[3]都绑定到相同的底层数组。

Becasueb填充有相同的矩阵,而不是3个相同的矩阵。.+=改变矩阵的内容,因此b中的所有内容都被改变。另一方面,+=创建一个新矩阵并将其分配回b[1]。要查看此信息,可以使用===运算符:

b = fill(zeros(2,2),1,3)
b[1] === b[2] # true
b[1] += zeros(2, 2) # a new matrix is created and assigned back to b[1]
b[1] == b[2] # true, they are all zeros
b[1] === b[2] # false, they are not the same matrix

fill函数的帮助消息中实际上有一个例子正是指出了这个问题。您可以通过在REPL中运行?fill来找到它。

...
If x is an object reference, all elements will refer to the same object:
julia> A = fill(zeros(2), 2);

julia> A[1][1] = 42; # modifies both A[1][1] and A[2][1]

julia> A
2-element Array{Array{Float64,1},1}:
[42.0, 0.0]
[42.0, 0.0]

有多种方法可以创建独立矩阵的数组。一种是使用列表理解:

c = [zeros(2,2) for _ in 1:1, _ in 1:3]
c[1] === c[2] # false

最新更新