Julia 广播式的修改

                     

贡献者: xzllxls

   本文授权转载自郝林的 《Julia 编程基础》。原文链接:第 10 章 容器:数组(下)

1. 第 10 章 容器:数组(下)

   我们在上一章讨论了数组的表示法、构造方法,以及存取其中元素值的各种方式。对于一般的应用场景来说,我觉得这些内容应该是足够的。但是,我们还应该了解更多,尤其是那些可以提高我们的编码效率的知识。

   下面,我们就来介绍几个比较重要的专题。我会先接续上一章的内容,从修改数组的方式讲起。

10.1 广播式的修改

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

   为了减少我们的代码量,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 一致。更确切地说,由于 operand2operand1 少了一个维度,因此需要进行扩展。

   在这里,扩展的具体方式是,把 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>

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

                     

© 小时科技 保留一切权利