10.1 广播式的修改

视图为我们改动数组中的元素值提供了一个很好的途径。不过,在真正改动的时候,我们仍然需要通过索引去定位元素值,并且需要分别修改每一个元素值或者告知每一个元素的新值。当改动量较大的时候,这种方式就显得很繁琐了。

为了减少我们的代码量,Julia 提供了一种名为广播(broadcast)的操作方式。这一操作的首要代表就是broadcast函数。该函数可以批量地操作某个数组复本中的所有元素值。示例如下:

  1. julia> operand1 = copy(array2d)
  2. 5×6 Array{Int64,2}:
  3. 1 6 11 16 21 26
  4. 2 7 12 17 22 27
  5. 3 8 13 18 23 28
  6. 4 9 14 19 24 29
  7. 5 10 15 20 25 30
  8. julia> broadcast(*, operand1, 10)
  9. 5×6 Array{Int64,2}:
  10. 10 60 110 160 210 260
  11. 20 70 120 170 220 270
  12. 30 80 130 180 230 280
  13. 40 90 140 190 240 290
  14. 50 100 150 200 250 300
  15. julia>

在这里,broadcast函数调用的含义是,让数组operand1的复本中的每一个元素值都与10相乘,并返回该复本。

这个函数的第一个参数值(或称操作值)的含义,不但取决于它本身,还取决于后续的参数值(或称被操作值)。比如,在下面的调用中,操作符-会让数组中的所有元素值都变成相应的负数:

  1. julia> broadcast(-, operand1)
  2. 5×6 Array{Int64,2}:
  3. -1 -6 -11 -16 -21 -26
  4. -2 -7 -12 -17 -22 -27
  5. -3 -8 -13 -18 -23 -28
  6. -4 -9 -14 -19 -24 -29
  7. -5 -10 -15 -20 -25 -30
  8. julia>

而像下面这样做则可以让一个数组中的元素值都被减去10

  1. julia> broadcast(-, operand1, 10)
  2. 5×6 Array{Int64,2}:
  3. -9 -4 1 6 11 16
  4. -8 -3 2 7 12 17
  5. -7 -2 3 8 13 18
  6. -6 -1 4 9 14 19
  7. -5 0 5 10 15 20
  8. julia>

另外,操作值不仅可以是一个操作符,还可以是一个普通的函数或者构造函数。例如:

  1. julia> broadcast(isodd, operand1)
  2. 5×6 BitArray{2}:
  3. 1 0 1 0 1 0
  4. 0 1 0 1 0 1
  5. 1 0 1 0 1 0
  6. 0 1 0 1 0 1
  7. 1 0 1 0 1 0
  8. julia> broadcast(Int, ans)
  9. 5×6 Array{Int64,2}:
  10. 1 0 1 0 1 0
  11. 0 1 0 1 0 1
  12. 1 0 1 0 1 0
  13. 0 1 0 1 0 1
  14. 1 0 1 0 1 0
  15. julia>

上面的第一个调用表达式会分别判断数组operand1中的每一个元素值是否为奇数,并用一个具有相同尺寸的数组承载这些判断的结果。更确切地说,它用来承载判断结果的是一个位数组。而第二个调用表达式则会基于这个位数组生成一个元素类型为Int的新数组,并返回后者。

你应该已经看出来了,broadcast函数中的被操作值可以是数组这样的容器,也可以是像整数这样的标量。或者说,被该函数的第一个参数值所操作的值可以是容器或标量。虽然没有什么优势,但是像下面这样做也是可以的:

  1. julia> broadcast(+, 5, -10)
  2. -5
  3. julia>

显然,鉴于broadcast函数的功能特点,我们应该让被操作值中至少有一个是数组或元组。当然了,所有的被操作值都是数组也是可以的。就像这样:

  1. julia> operand2 = [2, 4, 6, 8, 10];
  2. julia> broadcast(+, operand1, operand2)
  3. 5×6 Array{Int64,2}:
  4. 3 8 13 18 23 28
  5. 6 11 16 21 26 31
  6. 9 14 19 24 29 34
  7. 12 17 22 27 32 37
  8. 15 20 25 30 35 40
  9. julia>

让我来解释一下这个广播操作。先看两个被操作值,第一个被操作值operand1是一个 5 行 6 列的二维数组,而第二个被操作值operand2则是一个列向量。它们的维数是不同的,但它们在第一个维度上的长度是相同的,都是5。此操作的含义是,把两个数组中的所有对应位置上的元素值分别相加,并以此生成一个新的数组。可是,这两个数组的维数都不同,又怎么相加呢?

在这种情况下,broadcast函数会先对operand2进行扩展,使它的维数和尺寸都都与operand1一致。更确切地说,由于operand2operand1少了一个维度,因此需要进行扩展。

在这里,扩展的具体方式是,把operand2再复制出 5 份,并将它们横向地拼接在一起,共同组成一个 5 行 6 列的二维数组。然后,让这个拼接而成的数组成为新的第二个被操作值。下面是示意代码:

  1. julia> operand2_ext = [operand2 operand2 operand2 operand2 operand2 operand2]
  2. 5×6 Array{Int64,2}:
  3. 2 2 2 2 2 2
  4. 4 4 4 4 4 4
  5. 6 6 6 6 6 6
  6. 8 8 8 8 8 8
  7. 10 10 10 10 10 10
  8. julia> broadcast(+, operand1, operand2_ext)
  9. 5×6 Array{Int64,2}:
  10. 3 8 13 18 23 28
  11. 6 11 16 21 26 31
  12. 9 14 19 24 29 34
  13. 12 17 22 27 32 37
  14. 15 20 25 30 35 40
  15. julia>

一定要注意,虽然这些数组的维数可以不同,但是它们在对应维度上的长度都必须相同。因为只要有一个对应的长度不同,broadcast函数就无法确定数组扩展的具体方式,从而导致广播操作无法进行,并抛出一个DimensionMismatch类型的错误。例如:

  1. julia> broadcast(+, zeros(Int, 5, 3), ones(Int, 5, 2, 3))
  2. ERROR: DimensionMismatch("arrays could not be broadcast to a common size")
  3. # 省略了一些回显的内容。
  4. julia>

下面,我们来讲一个可以表达广播操作的语法——点语法(dot syntax)。

所谓的点语法,就是把英文点号.放在我们要使用的操作符之前(或要调用的函数之后),使得此操作(或此函数)可以逐个地施加在被操作值中的每一个元素值之上,并以此达到广播操作的目的。例如:

  1. julia> operand1 .* 10
  2. 5×6 Array{Int64,2}:
  3. 10 60 110 160 210 260
  4. 20 70 120 170 220 270
  5. 30 80 130 180 230 280
  6. 40 90 140 190 240 290
  7. 50 100 150 200 250 300
  8. julia> .- operand1
  9. 5×6 Array{Int64,2}:
  10. -1 -6 -11 -16 -21 -26
  11. -2 -7 -12 -17 -22 -27
  12. -3 -8 -13 -18 -23 -28
  13. -4 -9 -14 -19 -24 -29
  14. -5 -10 -15 -20 -25 -30
  15. julia> isodd.(operand1)
  16. 5×6 BitArray{2}:
  17. 1 0 1 0 1 0
  18. 0 1 0 1 0 1
  19. 1 0 1 0 1 0
  20. 0 1 0 1 0 1
  21. 1 0 1 0 1 0
  22. julia> Int.(ans)
  23. 5×6 Array{Int64,2}:
  24. 1 0 1 0 1 0
  25. 0 1 0 1 0 1
  26. 1 0 1 0 1 0
  27. 0 1 0 1 0 1
  28. 1 0 1 0 1 0
  29. julia>

注意,当点语法作用于操作符时,英文点号要与操作符紧挨在一起。而当点语法作用于函数调用时,英文点号则要写在函数名称和包裹参数值列表的圆括号之间。另外,与broadcast函数一样,点语法改动的也只是被操作值的复本,而不是其本身。

broadcast还有一个孪生函数,名为broadcast!。与前者不同,后者中的第三个参数值才是第一个被操作值。它的第二个参数值专用于存储广播操作的结果。我们可以称之为目的(destination)值。不过,broadcast!函数仍然会返回操作的结果值。特别提示一下,我们一定不要搞混这两种参数值。当一个值确实需要既充当目的值又充当被操作值的时候,我们一定要多一份谨慎。

最后,顺便说一下,虽然点语法看上去更加方便,但当被操作值的数量多于两个的时候,我们就不得不重复写入多个操作符了,如:

  1. julia> operand1 .+ operand2 .+ 10 .+ 100 .+ 1000
  2. 5×6 Array{Int64,2}:
  3. 1113 1118 1123 1128 1133 1138
  4. 1116 1121 1126 1131 1136 1141
  5. 1119 1124 1129 1134 1139 1144
  6. 1122 1127 1132 1137 1142 1147
  7. 1125 1130 1135 1140 1145 1150
  8. julia>

这个时候,broadcast函数的优势就得以显现了:

  1. julia> broadcast(+, operand1, operand2, 10, 100, 1000)
  2. 5×6 Array{Int64,2}:
  3. 1113 1118 1123 1128 1133 1138
  4. 1116 1121 1126 1131 1136 1141
  5. 1119 1124 1129 1134 1139 1144
  6. 1122 1127 1132 1137 1142 1147
  7. 1125 1130 1135 1140 1145 1150
  8. julia>

这种优势并不在于更少的代码量,而在于更少的重复代码。重复的代码越少,我们犯错的概率也就越小。