Julia "strange" 使用 fill() 和 .+= 的行为
Julia "strange" behaviour using fill() and .+=
我在我的代码中观察到“.+=”的意外行为(可能只有我一个人,我对 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)
。这意味着一系列操作:
- 创建了一个新数组(我们称之为
tmp
)来保存 b[1]
和 ones(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]
都绑定到相同的底层数组。
因为 b
填充了 相同的 矩阵,而不是 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
我在我的代码中观察到“.+=”的意外行为(可能只有我一个人,我对 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)
。这意味着一系列操作:
- 创建了一个新数组(我们称之为
tmp
)来保存b[1]
和ones(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]
都绑定到相同的底层数组。
因为 b
填充了 相同的 矩阵,而不是 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