3.3 原生数据结构
Julia 有多种原生数据结构。 它们都是某种结构化数据形式的抽象。 本书将讨论最常用的数据结构。 它们都能够保存同类型或异构的数据。 因为它们都是集合, 所以都能通过 for
循环进行 遍历 。 接下来的讨论包括 String
, Tuple
, NamedTuple
, UnitRange
, Arrays
, Pair
, Dict
, Symbol
。
当在 Julia 中偶然发现某种数据结构时,可以使用 methodswith
函数查看能接收该数据结构作为参数的方法。 Julia 中方法和函数的区别如下。 如前面讨论的那样,每一个函数对应多种方法。 因此值得将 methodswith
函数收藏到你的技巧包里。 例如,让我们看看当对 String
应用该函数时会发生什么:
first(methodswith(String), 5)
[1] write(fp::FilePathsBase.SystemPath, x::Union{String, Vector{UInt8}}) in FilePathsBase at /home/runner/.julia/packages/FilePathsBase/9kSEl/src/system.jl:380
[2] write(fp::FilePathsBase.SystemPath, x::Union{String, Vector{UInt8}}, mode) in FilePathsBase at /home/runner/.julia/packages/FilePathsBase/9kSEl/src/system.jl:380
[3] write(iod::HTTP.DebugRequest.IODebug, x::String) in HTTP.DebugRequest at /home/runner/.julia/packages/HTTP/aTjcj/src/IODebug.jl:38
[4] write(buffer::FilePathsBase.FileBuffer, x::String) in FilePathsBase at /home/runner/.julia/packages/FilePathsBase/9kSEl/src/buffer.jl:85
[5] write(io::IO, s::Union{SubString{String}, String}) in Base at strings/io.jl:244
3.3.1 对运算符和函数进行广播
在深入研究数据结构前,我们需要先讨论广播(也被称为 向量化)和 .
点运算符。
可以使用点运算符广播像 *
(乘)或 +
(加)这样的数学运算。 例如,添加广播只需将 +
改为 .+
:
[1, 2, 3] .+ 1
[2, 3, 4]
函数也能通过这种操作实现广播。 (技术上讲,数学运算或中缀运算符也是函数,但这不重要。) 还记得 logarithm
函数吗?
logarithm.([1, 2, 3])
[0.0, 0.6931471805599569, 1.0986122886681282]
3.3.2 带感叹号 !
的函数
当函数改变了一个或多个它们的参数时, 按照 Julia 惯例,应该在函数名后追加 !
。 这个惯例警告用户该函数 并不单纯,它具有 副作用。 当想要更新大型数据结构或变量容器时,具有 副作用 的 Julia 函数非常有用,因为它不存在创建新实例的所有开销。
例如,可以定义一个函数,它将向量 V
的每个元素加1:
function add_one!(V)
for i in 1:length(V)
V[i] += 1
end
return nothing
end
my_data = [1, 2, 3]
add_one!(my_data)
my_data
[2, 3, 4]
3.3.3 字符串
Julia 中使用双引号分隔符表示 字符串 :
typeof("This is a string")
String
也可以定义一个多行字符串:
text = "
This is a big multiline string.
As you can see.
It is still a String to Julia.
"
This is a big multiline string.
As you can see.
It is still a String to Julia.
但使用三引号通常更清晰:
s = """
This is a big multiline string with a nested "quotation".
As you can see.
It is still a String to Julia.
"""
This is a big multiline string with a nested "quotation".
As you can see.
It is still a String to Julia.
当使用三引号时,Julia 会忽略开头的缩进和换行。 这提升了代码可读性,因为你需要缩进代码,但这些空格不能截断字符串。
3.3.3.1 字符串连接
一个常见的字符串操作就是 字符串连接。 假设你想通过连接两个或多个字符串来创建一个新的字符串。 这在 Julia 中可以通过 *
运算符或 join
函数实现。 这个符号看起来是一个令人费解的选择,事实上也确实费解。 现在,许多 Julia 基础库都在使用该符号,因此它也被保留在 Julia 语言中。 如果你感兴趣,可以阅读 2015 年 GitHub 上关于它的讨论: https://github.com/JuliaLang/julia/issues/11030.
hello = "Hello"
goodbye = "Goodbye"
hello * goodbye
HelloGoodbye
如上所示,代码将会自动忽略 hello
和 goodbye
之间的空格。 可以使用 *
连接额外的字符串 " "
以添加空格,但当连接两个以上字符串时会变得很笨重。 此时就是 join
的用武之地。 仅仅需要将 []
中的字符串和分隔符作为参数传递:
join([hello, goodbye], " ")
Hello Goodbye
3.3.3.2 字符串插值
连接字符串可能会变得很复杂。 我们也可以使用 字符串插值 更直观地实现某些功能。 它看来就是:使用美元符号 $
在字符串中插入你想包含的内容。 以下是之前的例子,改为使用字符串插值:
"$hello $goodbye"
Hello Goodbye
甚至也支持在函数中进行字符串插值。 回到 Section 3.2.5 中的 test
函数,并用插值重新实现:
function test_interpolated(a, b)
if a < b
"$a is less than $b"
elseif a > b
"$a is greater than $b"
else
"$a is equal to $b"
end
end
test_interpolated(3.14, 3.14)
3.14 is equal to 3.14
3.3.3.3 字符串处理
Julia 中有多个函数处理字符串。 接下来将讨论那些最常用的函数。 另外注意,这些函数大多数都支持 正则表达式 (RegEx) 作为参数。 本书不包含 RegEx,但可以自主学习,尤其是如果你的大多数工作都需要处理文本数据。
首先,定义一个供后续使用的字符串:
julia_string = "Julia is an amazing open source programming language"
Julia is an amazing open source programming language
contains
,startswith
和endswith
: 条件函数 (返回true
或false
) 如果第二个参数是:第一个参数的 子串
contains(julia_string, "Julia")
true
第一个参数的 前缀
startswith(julia_string, "Julia")
true
第一个参数的 后缀
endswith(julia_string, "Julia")
false
lowercase
,uppercase
,titlecase
和lowercasefirst
:lowercase(julia_string)
julia is an amazing open source programming language
uppercase(julia_string)
JULIA IS AN AMAZING OPEN SOURCE PROGRAMMING LANGUAGE
titlecase(julia_string)
Julia Is An Amazing Open Source Programming Language
lowercasefirst(julia_string)
julia is an amazing open source programming language
replace
:介绍一种称为Pair
的新语法:replace(julia_string, "amazing" => "awesome")
Julia is an awesome open source programming language
split
:使用分隔符分隔字符串:split(julia_string, " ")
SubString{String}["Julia", "is", "an", "amazing", "open", "source", "programming", "language"]
3.3.3.4 字符串转换
我们经常需要在 Julia 中 转换 类型。 可以使用 string
函数将数字转为字符串:
my_number = 123
typeof(string(my_number))
String
有时需要逆向操作:将字符串转为数字。 Julia 中有个方便的函数 parse
。
typeof(parse(Int64, "123"))
Int64
时常希望能够安全地进行这些转换。 此时就需要介绍 tryparse
函数。 它具有与 parse
相同的功能,但只会返回请求类型的值或者nothing
。 当我们想要避免错误时 tryparse
会变得很有用。 当然,你需要之后手动处理这些 nothing
值。
tryparse(Int64, "A very non-numeric string")
nothing
3.3.4 元组(Tuple)
Julia 中有一类名为 元组 的特殊数据类型。 它们经常用在函数中,而函数又是 Julia 的重要组成部分,因此每一个 Julia 用户都应该了解元组的基础。
元组是包含多种不同类型的固定长度容器. 同时元组是 不可变对象,这意味着实例化后不能更改。 创建元组的方法是:使用 ()
作为开头和结尾,并使用 ,
作为值间的分隔符:
my_tuple = (1, 3.14, "Julia")
(1, 3.14, "Julia")
这里创建了包含三个值的元组。 每一个值都是不同的类型。 可以使用索引访问每一个元素。 如下所示:
my_tuple[2]
3.14
也可以使用 for
关键字遍历元组。 还将函数作用于元组。 但 永远不能改变元组的每一个值 , 因为它们是 不可变的。
还记得 Section 3.2.4.2 中返回多个值的函数吗? 查看 add_multiply
函数返回值的类型:
return_multiple = add_multiply(1, 2)
typeof(return_multiple)
Tuple{Int64, Int64}
这是因为 return a, b
与 return (a, b)
等价:
1, 2
(1, 2)
现在就可以发现它们之间的联系了。
关于元组还有一种用法。 当想给匿名函数传递多个变量时,猜猜你需要用什么? 当然还是元组!
map((x, y) -> x^y, 2, 3)
8
或两个以上参数:
map((x, y, z) -> x^y + z, 2, 3, 1)
9
3.3.5 命名元组
有时需要给元组中的值命名。 这就是需要用 命名元组 (named tuple) 的地方。 它的功能基本与元组一致: 它是 不可变的,并且能够接收 任意类型的值。
命名元组的构造与元组的构造稍有不同。 你已经熟悉使用括号 ()
和逗号 ,
分隔符。 但现在你需要 给值命名:
my_namedtuple = (i=1, f=3.14, s="Julia")
(i = 1, f = 3.14, s = "Julia")
可以向元组那样通过索引访问命名元组的元素。另外,还可以使用 .
结合名称访问。
my_namedtuple.s
Julia
为了完成命名元组的讨论,下面介绍一种 Julia 代码中常见的 快捷 语法。 Julia 用户通常使用括号 ()
和逗号 ,
创建命名元组,但并没有命名值。 为了给值命名,在命名元组的构造开始时,首先在值之前添加 ;
。 当组成命名元组的值已经在变量中定义,或者你想避免过长的行时,这一语法非常有用:
i = 1
f = 3.14
s = "Julia"
my_quick_namedtuple = (; i, f, s)
(i = 1, f = 3.14, s = "Julia")
3.3.6 Ranges
Julia 中的 range 表示一段开始和结束边界之间的序列。 语法是 start:stop
:
1:10
1:10
如下所示, range 实例的类型是 UnitRange{T}
,其中 T
是 UnitRange
中元素的类型:
typeof(1:10)
UnitRange{Int64}
如果收集所有值将得到:
[x for x in 1:10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
也可以构造其它类型的 range:
typeof(1.0:10.0)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
有时希望改变序列默认的步长。 这可以通过在 range 语法中添加步长实现,即 start:step:stop
。 例如,假设想要得到从 0 到 1,步长为 0.2 的 Float64
range :
0.0:0.2:1.0
0.0:0.2:1.0
如果要将 range “实例化” 到集合中, 可以使用函数 collect
:
collect(1:10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
这将得到一个边界范围内的指定类型数组。 既然提到数组,那接下来就讨论它。
3.3.7 数组
在最基本的形式中, 数组能够包含多种对象。 例如,一维数组可以包含多个数。
myarray = [1, 2, 3]
[1, 2, 3]
大多数情况下,由于性能原因需要构造单一类型的数组,但请注意它们也可以包含不同类型的对象:
myarray = ["text", 1, :symbol]
Any["text", 1, :symbol]
数组是数据科学家的生计之道,因为它们是大多数 数据操作 和 数据可视化 工作流的基础。
因此,数组是非常重要的数据结构。
3.3.7.1 数组类型
首先以 数组类型 开始。 这里有很多中类型,但本节主要关注数据科学中两种最常用的类型:
Vector{T}
: 一维 数组。Array{T, 1}
的别名。Matrix{T}
: 二维 数组。Array{T, 2}
的别名。
注意这里的 T
是数组元素的类型。 例如, Vector{Int64}
表示所有元素的类型都是 Int64
的 Vector
。另外 Matrix{AbstractFloat}
表示一个Matrix
,其中所有元素的类型都是 AbstractFloat
的子类型。
大多数情况下,特别是在处理表格数据时,我们使用的是一维或二维数组。 它们都是 Julia 中的 Array
类型。 但是,可以使用简洁清晰的语法操作 Vector
和 Matrix
。
3.3.7.2 数组构造
如何 构造 数组呢? 本届的开始,我们使用低级的方式构造数组。 在某些情况下,编写高性能代码就需要这样的做法。 然而,在大多数情况下,这不是必需的。同时可以安全地使用更简便的方法创建数组。 本节稍后讨论这些更简便的方法。
用于 Julia 数组的低级构造器是 默认构造器。 它接手元素类型作为 {}
括号内的类型参数,并将元素类型传递到构造器里,构造器后跟需要的维度。 通常使用未定义元素初始化向量和矩阵,即将 undef
参数作为传递到构造器里的类型。 如下构造一个含 10 个 undef
Float64
元素的向量:
my_vector = Vector{Float64}(undef, 10)
[0.0, 6.92973948346246e-310, 6.92973948346246e-310, 0.0, 6.9297091522515e-310, 6.9297091522515e-310, 0.0, 6.929739491143e-310, 6.92973948249567e-310, 0.0]
矩阵的构造方式是,向构造器传递两个维度参数:一个用于 行 ,另一个用于 列。 例如,具有 10 行 2列 undef
元素的矩阵以如下方式实例化:
my_matrix = Matrix{Float64}(undef, 10, 2)
10×2 Matrix{Float64}:
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
6.92968e-310 6.92968e-310
对于构造最常见元素类型的数组,Julia 中有一些语法别名 :
zeros
将所有元素初始化为 0。 注意默认类型为Float64
,如果需要可以更改类型:my_vector_zeros = zeros(10)
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
my_matrix_zeros = zeros(Int64, 10, 2)
10×2 Matrix{Int64}:
0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 0
ones
将所有元素初始化为 1。my_vector_ones = ones(Int64, 10)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
my_matrix_ones = ones(10, 2)
10×2 Matrix{Float64}:
1.0 1.0
1.0 1.0
1.0 1.0
1.0 1.0
1.0 1.0
1.0 1.0
1.0 1.0
1.0 1.0
1.0 1.0
1.0 1.0
对于其他的元素,可以先创建全为 undef
元素的数组,然后使用 fill!
函数将想要的元素填充到数组的每一个元素上。 下面是一个关于 3.14
(\(\pi\)) 的例子:
my_matrix_π = Matrix{Float64}(undef, 2, 2)
fill!(my_matrix_π, 3.14)
2×2 Matrix{Float64}:
3.14 3.14
3.14 3.14
也可以使用 数组字面量 创建数组: 例如,这是 2x2 的整数数组:
[[1 2]
[3 4]]
2×2 Matrix{Int64}:
1 2
3 4
数组字面量能在 []
括号前接收指定的类型。 所以,如果想得到与之前相同的数组,但类型应是浮点数,那么应按如下定义:
Float64[[1 2]
[3 4]]
2×2 Matrix{Float64}:
1.0 2.0
3.0 4.0
这也能够用于向量:
Bool[0, 1, 0, 1]
Bool[0, 1, 0, 1]
甚至可以使用数组构造器 组合和匹配 数组字面量:
[ones(Int, 2, 2) zeros(Int, 2, 2)]
2×4 Matrix{Int64}:
1 1 0 0
1 1 0 0
[zeros(Int, 2, 2)
ones(Int, 2, 2)]
4×2 Matrix{Int64}:
0 0
0 0
1 1
1 1
[ones(Int, 2, 2) [1; 2]
[3 4] 5]
3×3 Matrix{Int64}:
1 1 1
1 1 2
3 4 5
另一种创建数组的强大方法是 数组推断(array comprehension)。 这种创建数组的方式在大多数情况下更好:因为它能够避免循环,索引以及其他容易出错的操作。 你可以在 []
括号内编写要执行的语句。 例如,你想创建一个包含 1 到 10 的平方的向量:
[x^2 for x in 1:10]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
它也支持多个输入:
[x*y for x in 1:10 for y in 1:2]
[1, 2, 2, 4, 3, 6, 4, 8, 5, 10, 6, 12, 7, 14, 8, 16, 9, 18, 10, 20]
另外还能使用条件语句:
[x^2 for x in 1:10 if isodd(x)]
[1, 9, 25, 49, 81]
结合数组字面量,你还可以在 []
括号前指定需要的类型:
Float64[x^2 for x in 1:10 if isodd(x)]
[1.0, 9.0, 25.0, 49.0, 81.0]
最后,还可以使用 串联函数 创建数组。 串联是计算机编程中的标准术语,意为 “连接在一起”。 例如, 将字符串 "aa"
和 "bb"
串联并得到 "aabb"
:
"aa" * "bb"
aabb
因此,也可以通过串联数组来创建数组:
cat
:沿着指定的dims
串联输入的数组cat(ones(2), zeros(2), dims=1)
[1.0, 1.0, 0.0, 0.0]
cat(ones(2), zeros(2), dims=2)
2×2 Matrix{Float64}:
1.0 0.0
1.0 0.0
vcat
: 垂直串联,cat(...; dims=1)
的缩写vcat(ones(2), zeros(2))
[1.0, 1.0, 0.0, 0.0]
hcat
: 水平串联,cat(...; dims=2)
的缩写hcat(ones(2), zeros(2))
2×2 Matrix{Float64}:
1.0 0.0
1.0 0.0
3.3.7.3 数组检测
当拥有一些数组时,下一步应是对它们进行 检测 。 Julia 中提供了许多方便的函数,这使得用户能够检测任何数组。
知道数组中的 元素类型 是非常有用的。 这会用到 eltype
函数:
eltype(my_matrix_π)
Float64
了解到类型后,可能还会对 数组的维度 感兴趣。 Julia 中有多个用于检测数组维度的函数:
length
: 元素的总数length(my_matrix_π)
4
ndims
: 维度的个数ndims(my_matrix_π)
2
size
: 此例有一些复杂。 默认情况下将返回包含所有数组维度的元组。size(my_matrix_π)
(2, 2)
你可以在
size
的第二个参数指定想要的维度。 如下,第二个轴为列:size(my_matrix_π, 2)
2
3.3.7.4 数组索引和切片
有时希望仅仅检测数组的一部分。 这就需要 索引 和 切片。 如果想要考察向量的某一部分,或者矩阵的某一行或某一列,那么你可能需要 索引数组。
首先创建一个向量和矩阵作为示例:
my_example_vector = [1, 2, 3, 4, 5]
my_example_matrix = [[1 2 3]
[4 5 6]
[7 8 9]]
首先考虑向量。 假设要访问向量的第二个元素。 你只需要在 []
括号内添加对应索引:
my_example_vector[2]
2
关于矩阵的语法也是如此。 但因为矩阵是二维数组,需要 同时 指定行和列。 接下来检索位于第二行(第一维)、第一列(第二维)的元素:
my_example_matrix[2, 1]
4
Julia 也为数组的 第一个 和 最后一个 元素定义了特殊的关键字: begin
和 end
。 例如,可以如下方式检索向量的倒数第二个元素:
my_example_vector[end-1]
4
这也适用于矩阵。 可以如下方式检索位于最后一行、第二列的元素。
my_example_matrix[end, begin+1]
8
通常我们不仅对单个数组元素感兴趣,还想获得 数组的子集。 这可以通过数组 切片 实现。 它使用与索引相同的语法,但需要添加冒号 :
来表示数组切片的边界。 例如,假设想要获得向量的第二个到第四个元素:
my_example_vector[2:4]
[2, 3, 4]
可以对矩阵作同样的事。 特别地,对于矩阵,仅使用冒号 :
就可以获得指定维度的所有元素。 例如,想要获得第二行的所有元素。
my_example_matrix[2, :]
[4, 5, 6]
上面这段代码可被解释为 “获取第二行的所有列”。
矩阵同样支持 begin
和 end
:
my_example_matrix[begin+1:end, end]
[6, 9]
3.3.7.5 数组操作
我们有多种 操作 数组的方式。 第一种操作数组的方式是 数组的单个元素。 只需索引数组的单个元素,则使用等号 =
赋值:
my_example_matrix[2, 2] = 42
my_example_matrix
3×3 Matrix{Int64}:
1 2 3
4 42 6
7 8 9
另外,也可以操作数组的子集。 在此例中,对数组进行切片并使用 =
赋值:
my_example_matrix[3, :] = [17, 16, 15]
my_example_matrix
3×3 Matrix{Int64}:
1 2 3
4 42 6
17 16 15
注意,此处使用向量赋值,这是因为数组切片的类型就是 Vector
:
typeof(my_example_matrix[3, :])
Vector{Int64} (alias for Array{Int64, 1})
第二种操作数组的方式是 改变形状。 假设你有 6 个元素的向量,但想将其变成 3x2 的矩阵。 这可以通过 reshape
实现,具体操作是将数组传递给第一个参数,并将维度构成的元组传递给第二个参数。
six_vector = [1, 2, 3, 4, 5, 6]
three_two_matrix = reshape(six_vector, (3, 2))
three_two_matrix
3×2 Matrix{Int64}:
1 4
2 5
3 6
通过指定只有 1 维的维度元组,你可以将其变回向量:
reshape(three_two_matrix, (6, ))
[1, 2, 3, 4, 5, 6]
第三种操作数组的方式是 按元素应用函数。 这会用到点运算符 .
,其也被称为 广播。
logarithm.(my_example_matrix)
3×3 Matrix{Float64}:
0.0 0.693147 1.09861
1.38629 3.73767 1.79176
2.83321 2.77259 2.70805
Julia中的点运算符非常通用。 可以使用它广播中缀运算符:
my_example_matrix .+ 100
3×3 Matrix{Int64}:
101 102 103
104 142 106
117 116 115
另一种在向量中广播函数的方法是使用 map
:
map(logarithm, my_example_matrix)
3×3 Matrix{Float64}:
0.0 0.693147 1.09861
1.38629 3.73767 1.79176
2.83321 2.77259 2.70805
对于匿名函数, map
通常可读性更好。 例如,
map(x -> 3x, my_example_matrix)
3×3 Matrix{Int64}:
3 6 9
12 126 18
51 48 45
上面的例子看起来相当清晰。 不过,如下的广播代码也能实现相同功能:
(x -> 3x).(my_example_matrix)
3×3 Matrix{Int64}:
3 6 9
12 126 18
51 48 45
其次,map
也适用于数组切片:
map(x -> x + 100, my_example_matrix[:, 3])
[103, 106, 115]
最后,在某些情况下,特别是处理表格数据时,我们想要 沿着特定的数组维度应用函数。 这可以通过 mapslices
函数实现。 与 map
类似,第一个元素是函数而第二个元素是数组。 唯一的变化是,需要传入 dims
参数指定操作数组元素的维度。
例如,将 sum
函数传给 mapslices
,维度参数分别指定为行(dims=1
)和列(dims=2
):
# rows
mapslices(sum, my_example_matrix; dims=1)
1×3 Matrix{Int64}:
22 60 24
# columns
mapslices(sum, my_example_matrix; dims=2)
3×1 Matrix{Int64}:
6
52
48
3.3.7.6 数组迭代
常见的操作是 使用 for
循环迭代数组。 应用于数组的 for
循环会逐个返回元素。
最简单的例子是迭代向量。
simple_vector = [1, 2, 3]
empty_vector = Int64[]
for i in simple_vector
push!(empty_vector, i + 1)
end
empty_vector
[2, 3, 4]
有时,你不想要迭代数组的每个元素,而是迭代每个数组索引。 可以使用 eachindex
函数结合 for
循环来迭代每个数组索引。
然后,此处也展示一个向量的例子:
forty_twos = [42, 42, 42]
empty_vector = Int64[]
for i in eachindex(forty_twos)
push!(empty_vector, i)
end
empty_vector
[1, 2, 3]
在上例中,eachindex(forty_twos)
函数返回的是 forty_twos
的索引,即 [1, 2, 3]
。
类似地,也可以迭代矩阵。 标准 for
循环的迭代顺序是先列后行。 它首先遍历第 1 列的所有元素,从第一行和最后一行,然后对第2列进行同样的遍历,直到循环完所有列。
对于熟悉其他编程语言的用户: 与大多数科学计算编程语言一样,Julia 是“列优先存储”。 列优先存储意味着每一列的元素在内存中的存储位置是相邻的13。 这也意味着,沿列遍历会比沿行遍历更快。
所以,查看如下的例子:
column_major = [[1 3]
[2 4]]
row_major = [[1 2]
[3 4]]
如果遍历的是以列优先方式存储的向量,那么结果将是有序的:
indexes = Int64[]
for i in column_major
push!(indexes, i)
end
indexes
[1, 2, 3, 4]
然而,如果遍历的是以其他方式存储的向量,那么结果将不是有序的:
indexes = Int64[]
for i in row_major
push!(indexes, i)
end
indexes
[1, 3, 2, 4]
通常更好的做法是,在进行这些循环时使用特定的函数:
eachcol
: 先沿着列方向迭代first(eachcol(column_major))
[1, 2]
eachrow
: 先沿着行方向迭代first(eachrow(column_major))
[1, 3]
3.3.8 Pair
与有关数组的超长章节相比,关于 Pair 的章节将是简短的。 Pair
是一种包含两个对象的数据结构 (一般属于彼此)。 在 Julia 中,可以使用如下的语法构造 Pair
:
my_pair = "Julia" => 42
"Julia" => 42
这两个元素分别存储在字段 first
和 second
。
my_pair.first
Julia
my_pair.second
42
但,在大多数情况下,使用 first
和 last
更简单14:
first(my_pair)
Julia
last(my_pair)
42
Pair
广泛应用于数组操作和数据可视化。本书的 DataFrames.jl
(Section 4) 和 Makie.jl
(Section 5) 章节将会在主要程序函数中用到由各种对象构成的 Pair
。 例如,在 DataFrames.jl
这一章,可以看到 :a => :b
的用途是将 :a
重命名为 :b
。
3.3.9 字典
如何你理解什么是 Pair
, 那么理解 Dict
也不会成为问题。 实际上,Dict
是从键 (key) 到值 (values) 的映射。 映射的意思是说,如果你向 Dict
提供一些键,然后 Dict
能够告诉你哪些值属于这些键。 key
和 value
可以是任何类型,但 key
通常是字符串。
Julia 中有两种构造 Dict
的方法。 第一种是向 Dict
构造器传递由 (key, value)
元组构成的向量:
name2number_map = Dict([("one", 1), ("two", 2)])
Dict{String, Int64} with 2 entries:
"two" => 2
"one" => 1
还有一种可读性更高的写法,其基于上节中提到的 Pair
类型。 即也可以向 Dict
构造器传递多组 key => value
这样的 Pair
:
name2number_map = Dict("one" => 1, "two" => 2)
Dict{String, Int64} with 2 entries:
"two" => 2
"one" => 1
使用相应的 key
作为索引即可检索到 Dict
的 value
:
name2number_map["one"]
1
如果要增加新的条目,可使用所需的 key
作为 Dict
的索引,并使用赋值运算符为其赋值 value
:
name2number_map["three"] = 3
3
可以使用 keys
和 in
检查一个 Dict
是否有特定的 key
:
"two" in keys(name2number_map)
true
可以使用 delete!
函数删除 key
:
delete!(name2number_map, "three")
Dict{String, Int64} with 2 entries:
"two" => 2
"one" => 1
或者,可以使用 pop!
函数在返回值时删除键:
popped_value = pop!(name2number_map, "two")
2
现在, name2number_map
仅有一个 key
:
name2number_map
Dict{String, Int64} with 1 entry:
"one" => 1
DataFrames.jl
(Section 4) 中的数据操作和 Makie.jl
(Section 5) 中的数据可视化也用到了很多 Dict
。 因此,了解它们的基本功能十分重要。
另外还有一种非常有用的 Dict
构造方法。 假设有两个向量,然后想用它们要构造一个 Dict
,即其中一个作为 key
,另一个作为 value
。 那么可以使用 zip
函数将两个对象 “粘合” 起来(就像拉链那样):
A = ["one", "two", "three"]
B = [1, 2, 3]
name2number_map = Dict(zip(A, B))
Dict{String, Int64} with 3 entries:
"two" => 2
"one" => 1
"three" => 3
例如,获得数字 3 的方式为:
name2number_map["three"]
3
3.3.10 Symbol
Symbol
实际上 并不是 一种数据结构。 它是一种类型,并且其行为类似于字符串。 与引号包围文本的字符串不同,Symbol
以冒号 (:) 开始并且可以包含下划线:
sym = :some_text
:some_text
可以轻松地将 Symbol
转换为字符串,反之亦然:
s = string(sym)
some_text
sym = Symbol(s)
:some_text
使用 Symbol
的好处是会少键入一个字符,即 :some_text
相对于 "some text"
。 DataFrames.jl
(Section 4) 中的数据操作和 Makie.jl
(Section 5) 中的数据可视化将会多次用到 Symbol
。
3.3.11 Splat 运算符
Julia 中有一种 splatting
运算符 ...
,它被用于在函数调用时转换 参数序列。 在 数据操作 和 数据可视化 章节中,我们偶尔会在调用某些函数时使用 splatting
。
结合例子学习 splatting
是最直观的方法。 如下的 add_elements
函数将传入的三个参数相加:
add_elements(a, b, c) = a + b + c
add_elements (generic function with 1 method)
现在,假设有一个三个元素构成的集合。 一种普通的方法是,将集合的三个元素逐个传递为函数参数,如下所示:
my_collection = [1, 2, 3]
add_elements(my_collection[1], my_collection[2], my_collection[3])
6
接下来使用展开运算符 ...
,它将接收一个集合(通常是数组,向量,元组,或 range
)并将其转化为参数序列:
add_elements(my_collection...)
6
集合后的 ...
用于将集合转化为参数序列。 对于上述例子,两种传入参数的方式等价:
add_elements(my_collection...) == add_elements(my_collection[1], my_collection[2], my_collection[3])
true
任何时候,若 Julia 在函数调用中发现了展开运算符,那么它会将运算符前的集合转化为一组逗号分隔的参数序列。
这也适用于 range 类型:
add_elements(1:3...)
6
CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso, 刘贵欣 (中文翻译), 田俊 (中文审校)