贡献者: xzllxls
本文授权转载自郝林的 《Julia 编程基础》。原文链接:第 10 章 容器:数组(下)。
我们在上一章讨论了数组的表示法、构造方法,以及存取其中元素值的各种方式。对于一般的应用场景来说,我觉得这些内容应该是足够的。但是,我们还应该了解更多,尤其是那些可以提高我们的编码效率的知识。
下面,我们就来介绍几个比较重要的专题。我会先接续上一章的内容,从修改数组的方式讲起。
视图为我们改动数组中的元素值提供了一个很好的途径。不过,在真正改动的时候,我们仍然需要通过索引去定位元素值,并且需要分别修改每一个元素值或者告知每一个元素的新值。当改动量较大的时候,这种方式就显得很繁琐了。
为了减少我们的代码量,Julia 提供了一种名为广播(broadcast)的操作方式。这一操作的首要代表就是 broadcast
函数。该函数可以批量地操作某个数组复本中的所有元素值。示例如下:
julia> operand1 = copy(array2d)
5×6 Array{Int64,2}:
1 6 11 16 21 26
2 7 12 17 22 27
3 8 13 18 23 28
4 9 14 19 24 29
5 10 15 20 25 30
julia> broadcast(*, operand1, 10)
5×6 Array{Int64,2}:
10 60 110 160 210 260
20 70 120 170 220 270
30 80 130 180 230 280
40 90 140 190 240 290
50 100 150 200 250 300
julia>
在这里,broadcast
函数调用的含义是,让数组 operand1
的复本中的每一个元素值都与 10
相乘,并返回该复本。
这个函数的第一个参数值(或称操作值)的含义,不但取决于它本身,还取决于后续的参数值(或称被操作值)。比如,在下面的调用中,操作符 -
会让数组中的所有元素值都变成相应的负数:
julia> broadcast(-, operand1)
5×6 Array{Int64,2}:
-1 -6 -11 -16 -21 -26
-2 -7 -12 -17 -22 -27
-3 -8 -13 -18 -23 -28
-4 -9 -14 -19 -24 -29
-5 -10 -15 -20 -25 -30
julia>
而像下面这样做则可以让一个数组中的元素值都被减去 10
:
julia> broadcast(-, operand1, 10)
5×6 Array{Int64,2}:
-9 -4 1 6 11 16
-8 -3 2 7 12 17
-7 -2 3 8 13 18
-6 -1 4 9 14 19
-5 0 5 10 15 20
julia>
另外,操作值不仅可以是一个操作符,还可以是一个普通的函数或者构造函数。例如:
julia> broadcast(isodd, operand1)
5×6 BitArray{2}:
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
julia> broadcast(Int, ans)
5×6 Array{Int64,2}:
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
julia>
上面的第一个调用表达式会分别判断数组 operand1
中的每一个元素值是否为奇数,并用一个具有相同尺寸的数组承载这些判断的结果。更确切地说,它用来承载判断结果的是一个位数组。而第二个调用表达式则会基于这个位数组生成一个元素类型为 Int
的新数组,并返回后者。
你应该已经看出来了,broadcast
函数中的被操作值可以是数组这样的容器,也可以是像整数这样的标量。或者说,被该函数的第一个参数值所操作的值可以是容器或标量。虽然没有什么优势,但是像下面这样做也是可以的:
julia> broadcast(+, 5, -10)
-5
julia>
显然,鉴于 broadcast
函数的功能特点,我们应该让被操作值中至少有一个是数组或元组。当然了,所有的被操作值都是数组也是可以的。就像这样:
julia> operand2 = [2, 4, 6, 8, 10];
julia> broadcast(+, operand1, operand2)
5×6 Array{Int64,2}:
3 8 13 18 23 28
6 11 16 21 26 31
9 14 19 24 29 34
12 17 22 27 32 37
15 20 25 30 35 40
julia>
让我来解释一下这个广播操作。先看两个被操作值,第一个被操作值 operand1
是一个 5 行 6 列的二维数组,而第二个被操作值 operand2
则是一个列向量。它们的维数是不同的,但它们在第一个维度上的长度是相同的,都是 5
。此操作的含义是,把两个数组中的所有对应位置上的元素值分别相加,并以此生成一个新的数组。可是,这两个数组的维数都不同,又怎么相加呢?
在这种情况下,broadcast
函数会先对 operand2
进行扩展,使它的维数和尺寸都都与 operand1
一致。更确切地说,由于 operand2
比 operand1
少了一个维度,因此需要进行扩展。
在这里,扩展的具体方式是,把 operand2
再复制出 5 份,并将它们横向地拼接在一起,共同组成一个 5 行 6 列的二维数组。然后,让这个拼接而成的数组成为新的第二个被操作值。下面是示意代码:
julia> operand2_ext = [operand2 operand2 operand2 operand2 operand2 operand2]
5×6 Array{Int64,2}:
2 2 2 2 2 2
4 4 4 4 4 4
6 6 6 6 6 6
8 8 8 8 8 8
10 10 10 10 10 10
julia> broadcast(+, operand1, operand2_ext)
5×6 Array{Int64,2}:
3 8 13 18 23 28
6 11 16 21 26 31
9 14 19 24 29 34
12 17 22 27 32 37
15 20 25 30 35 40
julia>
一定要注意,虽然这些数组的维数可以不同,但是它们在对应维度上的长度都必须相同。因为只要有一个对应的长度不同,broadcast
函数就无法确定数组扩展的具体方式,从而导致广播操作无法进行,并抛出一个 DimensionMismatch
类型的错误。例如:
julia> broadcast(+, zeros(Int, 5, 3), ones(Int, 5, 2, 3))
ERROR: DimensionMismatch("arrays could not be broadcast to a common size")
# 省略了一些回显的内容。
julia>
下面,我们来讲一个可以表达广播操作的语法——点语法(dot syntax)。
所谓的点语法,就是把英文点号 .
放在我们要使用的操作符之前(或要调用的函数之后),使得此操作(或此函数)可以逐个地施加在被操作值中的每一个元素值之上,并以此达到广播操作的目的。例如:
julia> operand1 .* 10
5×6 Array{Int64,2}:
10 60 110 160 210 260
20 70 120 170 220 270
30 80 130 180 230 280
40 90 140 190 240 290
50 100 150 200 250 300
julia> .- operand1
5×6 Array{Int64,2}:
-1 -6 -11 -16 -21 -26
-2 -7 -12 -17 -22 -27
-3 -8 -13 -18 -23 -28
-4 -9 -14 -19 -24 -29
-5 -10 -15 -20 -25 -30
julia> isodd.(operand1)
5×6 BitArray{2}:
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
julia> Int.(ans)
5×6 Array{Int64,2}:
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
0 1 0 1 0 1
1 0 1 0 1 0
julia>
注意,当点语法作用于操作符时,英文点号要与操作符紧挨在一起。而当点语法作用于函数调用时,英文点号则要写在函数名称和包裹参数值列表的圆括号之间。另外,与 broadcast
函数一样,点语法改动的也只是被操作值的复本,而不是其本身。
broadcast
还有一个孪生函数,名为 broadcast!
。与前者不同,后者中的第三个参数值才是第一个被操作值。它的第二个参数值专用于存储广播操作的结果。我们可以称之为目的(destination)值。不过,broadcast!
函数仍然会返回操作的结果值。特别提示一下,我们一定不要搞混这两种参数值。当一个值确实需要既充当目的值又充当被操作值的时候,我们一定要多一份谨慎。
最后,顺便说一下,虽然点语法看上去更加方便,但当被操作值的数量多于两个的时候,我们就不得不重复写入多个操作符了,如:
julia> operand1 .+ operand2 .+ 10 .+ 100 .+ 1000
5×6 Array{Int64,2}:
1113 1118 1123 1128 1133 1138
1116 1121 1126 1131 1136 1141
1119 1124 1129 1134 1139 1144
1122 1127 1132 1137 1142 1147
1125 1130 1135 1140 1145 1150
julia>
这个时候,broadcast
函数的优势就得以显现了:
julia> broadcast(+, operand1, operand2, 10, 100, 1000)
5×6 Array{Int64,2}:
1113 1118 1123 1128 1133 1138
1116 1121 1126 1131 1136 1141
1119 1124 1129 1134 1139 1144
1122 1127 1132 1137 1142 1147
1125 1130 1135 1140 1145 1150
julia>
这种优势并不在于更少的代码量,而在于更少的重复代码。重复的代码越少,我们犯错的概率也就越小。