9.6 修改元素值

9.6.1 索引

修改一个数组最简单的方式就是使用索引表达式。无论是单点索引表达式,还是多点索引表达式,又或是范围索引表达式,都可以被用来修改数组。示例如下:

  1. julia> array2d_copy = 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> array2d_copy[5] = 50;
  9. julia> array2d_copy[[1,3]] = [10, 30];
  10. julia> array2d_copy[7:9] = [70, 80, 90];
  11. julia> array2d_copy
  12. 5×6 Array{Int64,2}:
  13. 10 6 11 16 21 26
  14. 2 70 12 17 22 27
  15. 30 80 13 18 23 28
  16. 4 90 14 19 24 29
  17. 50 10 15 20 25 30
  18. julia>

这里有两点需要注意。第一点,当我们使用多点索引表达式或范围索引表达式的时候,在赋值符号=右边的应该是一个一维的数组。并且,这个一维数组的长度应该与我们要替换的元素值的数量一致。第二点,不管使用哪一种索引表达式,等号右边的代表新元素的值都必须能被转换成其左边数组的元素类型的实例,否则 Julia 就会立即报错:

  1. julia> array2d_copy[[1,3]] = [10.1, 30.5]
  2. ERROR: InexactError: Int64(10.1)
  3. # 省略了一些回显的内容。
  4. julia>

浮点数10.1Float64类型的,它不能被转换成Int64类型的实例,所以 Julia 就报错了。

另外,我们也可以利用笛卡尔索引对数组进行修改。比如:

  1. julia> array3d_copy = copy(array3d)
  2. 3×5×2 Array{Int64,3}:
  3. [:, :, 1] =
  4. 1 4 7 10 13
  5. 2 5 8 11 14
  6. 3 6 9 12 15
  7. [:, :, 2] =
  8. 16 19 22 25 28
  9. 17 20 23 26 29
  10. 18 21 24 27 30
  11. julia> array3d_copy[:, :, 1] = zeros(Int64, 3, 5);
  12. julia> array3d_copy[:, 3:4, 2] = ones(Int64, 3, 2);
  13. julia> array3d_copy[:, [1,5], 2] = fill(2, 3, 2);
  14. julia> array3d_copy
  15. 3×5×2 Array{Int64,3}:
  16. [:, :, 1] =
  17. 0 0 0 0 0
  18. 0 0 0 0 0
  19. 0 0 0 0 0
  20. [:, :, 2] =
  21. 2 19 1 1 2
  22. 2 20 1 1 2
  23. 2 21 1 1 2
  24. julia>

简单地解释一下,函数copy用于浅拷贝一个值。在这里,我利用copy函数得到了数组array3d的复本,并把这个复本赋给了变量array3d_copy。关于copy函数和浅拷贝,我在下一章都会进行详细的说明。

9.6.2 视图

我们已经知道,索引表达式可以让我们获得一个数组中的某个或某些元素。如果索引表达式返回的是单个的元素值,那么这个值就是原数组中对应的那个元素值本身。如果索引表达式返回的是一个数组,那么它就相当于在一个新的数组结构中沿用了原数组中的相应元素值。这其实与copy函数有着异曲同工之妙。然而,不论索引表达式的求值结果是什么,我们都不能通过这个结果值去替换原有数组中的元素。但是,我们通过视图(view)是可以做到这一点的。

函数view用于创建一个数组的视图。它的第一个参数就是视图基于的那个数组(或称父数组)。除了父数组以外,我们还可以为它传入一个或多个索引号。为了演示,我们先定义一个新的多维数组:

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

解释一下,Vector(1:36)会构造出一个向量。这个向量的元素类型是Int(具体到这里是Int64),长度是36,并且其中会依次地包含从136的整数值。函数reshape会先创建一个此向量的复本,然后把该复本变成一个3×3×2×2的四维数组。这个四维数组的元素类型和长度都与原数组保持一致,只是在维数和尺寸上有所变化。

现在,我们基于四维数组array4d创建视图:

  1. julia> array4d_view1 = view(array4d, 26)
  2. 0-dimensional view(::Array{Int64,1}, 26) with eltype Int64:
  3. 26
  4. julia>

由 REPL 环境回显的内容可知,我们创建了一个零维的视图。什么叫零维呢?如果说二维是一个面、一维是一条线的话,那么零维就是一个点。零维的数组或视图就相当于一个标量(scalar)。所谓的标量,可以说就是不包含其他值的单一值。像数值、字符值、字符串值、符号、类型、函数,以及一些常见的单例如missingnothing等都属于标量。

零维数组没有任何的维度,这意味着在任何维度上它们都没有所谓的长度。因此,把size函数用在它们身上就只会返回空的元组。不过它们却有总长度,而且这个总长度总是1。这是因为它们终归还是数组,并且里面终归还是有一个元素值的。相关的代码如下:

  1. julia> size(array4d_view1)
  2. ()
  3. julia> ndims(array4d_view1), length(array4d_view1)
  4. (0, 1)
  5. julia> eltype(array4d_view1)
  6. Int64
  7. julia>

那么我们怎样才能从中取出那个唯一的元素值呢?答案是,依然使用索引表达式。不过,在针对零维视图的索引表达式中,索引号就变得可有可无了。例如:

  1. julia> array4d_view1[1]
  2. 26
  3. julia> array4d_view1[]
  4. 26
  5. julia

既然我们可以这样取出视图中的元素值,那么必然也可以利用这种方式替换元素值。代码如下:

  1. julia> array4d_view1[] = 260
  2. 260
  3. julia> array4d_view1[]
  4. 260
  5. julia> array4d[26]
  6. 260
  7. julia>

一定要注意,我们对视图中元素值的替换肯定会改变其父数组中的对应元素值。因此,一旦替换了视图array4d_view1中的那个元素值,也就等于替换了数组array4d中与线性索引号26对应的那个元素值。

我们也可以把数组中的多个元素值汇聚到同一个视图里。这时,我们需要用中括号把多个线性索引号包裹起来,并将其作为view函数的第二个参数值。比如:

  1. julia> array4d_view2 = view(array4d, [1,3,5])
  2. 3-element view(::Array{Int64,1}, [1, 3, 5]) with eltype Int64:
  3. 1
  4. 3
  5. 5
  6. julia> array4d_view2[[1, 2, 3]]
  7. 3-element Array{Int64,1}:
  8. 1
  9. 3
  10. 5
  11. julia>

注意,视图中的各个元素值的线性索引号,不一定就等于它们在父数组中的那个线性索引号。就拿视图array4d_view2来说。其中有 3 个元素值,它们在这个视图中的线性索引号分别是123。但是,后两个元素值在该视图的父数组array4d中的线性索引号却分别是35。也就是说,视图上分配的线性索引号与它的父数组没有任何关系。它们是单独排列的,互不干扰。

我们若想要通过array4d_view2替换掉其父数组中的元素值也很容易。代码如下:

  1. julia> array4d_view2[[1,2,3]] = [10, 30, 50]
  2. 3-element Array{Int64,1}:
  3. 10
  4. 30
  5. 50
  6. julia> array4d[[1, 3, 5]]
  7. 3-element Array{Int64,1}:
  8. 10
  9. 30
  10. 50
  11. julia>

在这里,我们需要小心的地方是,等号两边的视图或数组所包含的元素值的数量必须一致,否则替换就无法成功完成。

另外,除了线性索引,我们还可以在创建视图的时候使用笛卡尔索引。不过,笛卡尔索引在这里就不需要由中括号包裹了。更确切地说,在调用view函数的时候,笛卡尔索引中的每一个部分都需要作为一个独立的参数值。就像这样:

  1. julia> array4d_view3 = view(array4d, :, 1, 2, 2)
  2. 3-element view(::Array{Int64,4}, :, 1, 2, 2) with eltype Int64:
  3. 28
  4. 29
  5. 30
  6. julia>

上面这个视图引用的是数组array4d里的一个列向量中的所有元素值。而这个列向量就是array4d中的第 2 个三维数组中的第 2 个二维数组中的第 1 个一维数组。下面我们来替换它引用的那些元素值:

  1. julia> array4d_view3[:] = [280, 290, 300]
  2. 3-element Array{Int64,1}:
  3. 280
  4. 290
  5. 300
  6. julia> array4d[:, 1, 2, 2]
  7. 3-element Array{Int64,1}:
  8. 280
  9. 290
  10. 300
  11. julia>

怎么样?是不是很容易呢?只要理解了视图的本质,这就绝对算不上难事。你可以把视图想象成一个窗口。我们可以通过这个窗口看到其父数组中的一部分甚至全部的元素值。而且,更重要的是,透过这个窗口我们还可以直接存取那些看得到的元素值。

顺便说一下,当我们拿到一个视图时,可以通过调用parent函数得到它的父数组本身。如:

  1. julia> parent(array4d_view3) === array4d
  2. true
  3. julia>

另外,我们还可以通过调用parentindices函数获得视图里的所有元素值在其父数组中的索引号(的另一种表现形式)。如:

  1. julia> parentindices(array4d_view3)
  2. (Base.Slice(Base.OneTo(3)), 1, 2, 2)
  3. julia> CartesianIndices(ans)
  4. 3×1×1×1 CartesianIndices{4,NTuple{4,UnitRange{Int64}}}:
  5. [:, :, 1, 1] =
  6. CartesianIndex(1, 1, 2, 2)
  7. CartesianIndex(2, 1, 2, 2)
  8. CartesianIndex(3, 1, 2, 2)
  9. julia> array4d[ans]
  10. 3×1×1×1 Array{Int64,4}:
  11. [:, :, 1, 1] =
  12. 280
  13. 290
  14. 300
  15. julia> vec(ans)
  16. 3-element Array{Int64,1}:
  17. 280
  18. 290
  19. 300
  20. julia> array4d[:, 1, 2, 2]
  21. 3-element Array{Int64,1}:
  22. 280
  23. 290
  24. 300
  25. julia>

可以看到,我们需要对parentindices函数的调用结果做进一步的转换。这主要是因为,视图中的每一个元素值都会有自己的父数组索引。而这些索引无法仅由单个值来表示,甚至无法被简单地表示出来。

幸好CartesianIndices函数可以正确地识别出parentindices函数返回的结果值,并产出一个笛卡尔索引的序列。而且,这样的序列可以被直接应用在针对数组的索引表达式中。不过,如此索引出的结果可能会与直接索引(如array4d[:, 1, 2, 2])得出的结果在尺寸上有所不同。如果一定要保持一致,我们可以再调用一下vec函数。这个函数能够沿着线性索引号把一个多维数组的复本捋直,让它变成一个一维数组。

总之,视图是一个基于数组的窗口。它能够让我们直接改动窗口内的元素值,同时又可以保护窗口之外的那些元素值。说它是修改数组的一把利器一点也不为过。

9.6.3 一些专用函数

除了上述的修改方式之外,Julia 还为数组提供了大量的专用函数。我在这里只简要地列举一下其中比较有特点的一些函数。注意,它们的名称都是以!结尾的。

  • circshift!函数:该函数可以在数组的一个或多个维度上循环式地挪动元素。我们之前说过,在某个维度上的元素指的可能是元素值,也可能是低维数组。所以在这里,在第一个维度上挪动的单元是元素值,而在更高维度上挪动的单元则是相应的低维数组。例如:数组[1, 2, 3, 4]在按照线性索引的顺序挪动 1 次之后就生成了[4, 1, 2, 3]
  • accumulate!函数:该函数可以面向数组在某个维度上的元素做累积计算。例如,数组[1, 3, 5, 7]在经过累积加法操作之后就生成了[1, 4, 9, 16]。目的数组中的第 1 个元素值完全取自源数组中的第 1 个元素值1。而这个元素值和源数组中的第 2 个元素值3相加,就得到了目的数组的第 2 个元素值4。然后,这个元素值再与源数组中的第 3 个元素值5相加,就得到了目的数组的第 3 个元素值9。以此类推。
  • cumprod!函数:该函数可以面向数组在某个维度上的元素做累积乘法。实际上,调用表达式cumprod!(dest, src)就相当于accumulate!(*, dest, src)
  • cumsum!函数:该函数可以面向数组在某个维度上的元素做累积加法。实际上,调用表达式cumsum!(dest, src)就相当于accumulate!(+, dest, src)
  • permute!函数:该函数可以置换向量中的元素值。更具体地讲,它可以根据第二个参数值给定的索引号序列,重新排列第一个参数值中的元素。例如,如果变量v的值是[15, 24, 33, 42],且变量p的值为[4, 2, 3, 1],那么调用表达式permute!(v, p)的执行就会让v的值变成[42, 24, 33, 15]
  • invpermute!函数:该函数可以对向量中的元素值进行逆置换。也就是说,它的功能与permute!函数的功能是互逆的。例如,调用表达式invpermute!(permute!(v, p), p)会让变量v的值最终依然为原值。
  • reverse!函数:该函数可以逆序排列向量中的元素值。例如,若变量v的值是[1, 2, 3, 4],则表达式reverse!(v)的求值结果就是[4, 3, 2, 1]

另外,Julia 还提供了很多与线性代数有关的函数。比如,可以转置向量和矩阵的transpose!函数、可以做向量标准化的normalize!函数、可以计算矩阵与矩阵或矩阵与向量的乘积的mul!函数、可以对数组中的元素值进行缩放的lmul!rmul!函数、可以求共轭转置数组的adjoint!函数、可以获得矩阵特征值的eigvals!函数、可以计算奇异值分解的svd!函数,等等。它们与其他众多不会修改原值的线性代数函数一起被定义在了LinearAlgebra模块里。我们在做数据特征工程或者构建机器学习模型的时候很可能会直接或间接地用到它们。