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