在Vectors概念中,我们说过“数组是 Julia 语言的核心”,而向量就是一维数组。
既然如此,我们完全可以期待这门语言提供许多灵活而强大的方式,来_对向量做各种操作_,不管这具体指什么。
关于术语的一点说明:虽然本文大量谈到“向量”,但其中很多内容同样适用于任何可迭代类型:范围、元组、集合,以及其他各种类型。
有些非常简单的函数接受向量输入,并且(对于一维输入)返回标量输出。
v = [2, 3, 4]
length(v) # => 3
sum(v) # => 9
等我们学到多维数组概念时就会更清楚:这其实是_降维_,而不一定是返回标量。 如果这句话让你摸不着头脑,现在先不用管它。
这类函数还有很多。
看看Statistics概念中的一些例子。
也有一些函数可以同时作用于多个向量,比如(非常实用的)zip。
julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])
# convert iterator to vector
julia> collect(z)
3-element Vector{Tuple{Int64, Char, String}}:
(1, 'a', "I")
(2, 'b', "make")
(3, 'c', "tuples")
zip() 接受任意数量的类向量输入,并返回一个由元组组成的迭代器。
这些输入通常长度相同。 如果其中一个更短,其他的就会被截断到最短的长度:这_也许_正是你想要的结果,但_更常见的情况_是代码里的一个 bug。
假设你有一个数值向量,想把每个值都减去 0.5。
julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
1.2
1.5
1.7
julia> v - 0.5
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)
这样行不通,那减去另一个向量呢?
julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
成功了,但向量一长,这种方式既繁琐又耗内存。
根据你在课程体系中已经学到的内容,你大概还能想到其他做法:
[x - 0.5 for x in v] 能得到想要的结果(Python 风格)。map(x -> x - 0.5, v) 同样可行(Haskell 风格,不过很多语言里都很常见)。幸运的是,Julia 有一个“神奇”的点号,可以非常简单地解决这个问题:你只需要写 v .- 0.5。
下一节会解释其中的原因。
那么,v - 0.5 会失败,而 v .- 0.5 能成功,我们需要弄明白这个点号到底在做什么。
它做了两件事,两件事合在一起就得到了想要的结果。
首先,在任何中缀运算符_前面_加一个点,意思是“把这个运算分别应用到每个元素上”。
同样,在函数名_后面_加一个点可以把它“向量化”,即使这个函数原本是为标量输入编写的。
julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
顺便说一句,中缀运算符其实只是底层函数的语法糖。
也就是说,例如 [1, 5, 10] .% 3 会被翻译转换成 mod.([1, 5, 10], 3),随后执行 mod. 这种写法(两种写法都会返回 [1, 2, 1])。
在前面的例子里我们看到,可以相减两个长度相同的向量,不过要明白 .- 比 - 更_安全_,因为它把逐元素的意图表达得很清楚。
julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
那长度不同的向量呢?
julia> v .- [0.5, 0.5]
ERROR: DimensionMismatch: arrays could not be broadcast to a common size
julia> v .- [0.5,]
3-element Vector{Float64}:
0.7
1.0
1.2
一般来说,长度不同就是错误,_除非_其中一方的长度为 1(严格来说,这是一个“单元素”维度)。
像 [0.5,] 或者干脆 0.5 这样的单元素值,会自动通过重复扩展到所需的长度。
这正是broadcasting的核心。
如果有人担心这种“重复”会占用内存,大可放心:它的实现非常高效,并不会真的在内存中复制这些值。
熟悉其他语言中广播的程序员要注意:Julia 的做法(大体上)与 NumPy 相似,但对尺寸不匹配的容忍度比 R 低得多。
如果在意内存占用,原地操作是减少内存分配的常见做法。
不过上面例子中的广播运算会创建一个新的 Vector,而不是修改原来的那个。
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
1
2
3
要想原地修改 v,这个修改就必须是广播的。
julia> v .= v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v .+= 1 # equivalent operation to above
3-element Vector{Int64}:
3
4
5
julia> v
3-element Vector{Int64}:
3
4
5
但要小心!赋值运算符 .= 前面的那个点号很重要。
julia> v = [1, 2, 3];
julia> v = v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
2
3
4
看起来效果是一样的,但这里 v .+ 1 先创建了一个新向量 [2, 3, 4],再把它赋值给变量 v,原来的向量 [1, 2, 3] 则留在内存里等待垃圾回收。
这样一来,它占用的内存是前一个例子的两倍,而前一个例子会复用为初始向量分配的内存。
同样,用长度相同的向量做广播时,也有类似的细微差别。
例如,给定两个长度相同的向量 v 和 w:
v .= w 会把 w 的一个copy放进 v 所在的内存位置。之后修改 v 不会影响 w,反之亦然。v = w 会产生另一个指向 w 所在内存位置的指针,名字叫 v。之后修改 v 会反映到 w 上,反之亦然。这一小节偏数学,大多数同学不必真正弄懂它。 不过它是个有用的提醒,当你看到意料之外的错误信息时,也许有助于调试。
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v * v
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})
# look, no commas
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
julia> u * v
1-element Vector{Int64}:
14
julia> v * u
3×3 Matrix{Int64}:
1 2 3
2 4 6
3 6 9
如果你恰好有线性代数的基础,那么(1)你并不是典型的 Exercism 用户_(但我们非常欢迎你!)_,(2)你可能会认出 v 是列向量,u 是行向量,u * v 是内积,而 v * u 是外积。
Julia 在这一点上,和其他所有事情一样,都遵循数学的规则。
**其他人请注意:**只需明白我们为什么建议做逐元素计算时总是用带点的运算符:v .* v 的效果和你想的完全一样,得到 [1, 4, 9]。
前面的概念里已经讲过如何按下标选取向量的元素。
a = collect('A':'Z') # => 26-element Vector{Char}
# index with an integer
a[2] # => 'B'
# index with a range
a[12:2:18] # => ['L', 'N', 'P, 'R']
# index with another vector
a[ [1, 3, 5] ] # => ['A', 'C', 'E']
还可以选出满足某个逻辑表达式(严格来说是“谓词”)的元素。 这通常需要用到广播。
julia> a[a .< 'D']
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
表达式越复杂,点号就越多(不过它们很小,敲起来也不费劲)。
julia> a[a .< 'D' .|| a .> 'W']
6-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)
提醒一下,这里的“向量”其实可以是任何合适的有序可迭代对象,比如一个范围:
julia> n = 3:10
3:10
julia> n[isodd.(n)]
4-element Vector{Int64}:
3
5
7
9
在内部,谓词会被转换成一个 BitVector,然后被当作下标使用。
julia> condition = a .< 'D'
26-element BitVector:
1
1
1
0
# display truncated
julia> a[condition]
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)