3.3 原生数据结构

Julia 有多种原生数据结构。 它们都是某种结构化数据形式的抽象。 本书将讨论最常用的数据结构。 它们都能够保存同类型或异构的数据。 因为它们都是集合, 所以都能通过 for 循环进行 遍历 。 接下来的讨论包括 StringTupleNamedTupleUnitRangeArraysPairDict, 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/dVWLK/src/system.jl:380
[2] write(fp::FilePathsBase.SystemPath, x::Union{String, Vector{UInt8}}, mode) in FilePathsBase at /home/runner/.julia/packages/FilePathsBase/dVWLK/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/dVWLK/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

如上所示,代码将会自动忽略 hellogoodbye 之间的空格。 可以使用 * 连接额外的字符串 " "以添加空格,但当连接两个以上字符串时会变得很笨重。 此时就是 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
  1. containsstartswithendswith: 条件函数 (返回 truefalse) 如果第二个参数是:

    • 第一个参数的 子串

      contains(julia_string, "Julia")
      true
    • 第一个参数的 前缀

      startswith(julia_string, "Julia")
      true
    • 第一个参数的 后缀

      endswith(julia_string, "Julia")
      false
  2. lowercaseuppercasetitlecaselowercasefirst

    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
  3. replace:介绍一种称为 Pair 的新语法:

    replace(julia_string, "amazing" => "awesome")
    Julia is an awesome open source programming language
  4. 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, breturn (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} ,其中 TUnitRange 中元素的类型:

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 数组类型

首先以 数组类型 开始。 这里有很多中类型,但本节主要关注数据科学中两种最常用的类型:

注意这里的 T 是数组元素的类型。 例如, Vector{Int64} 表示所有元素的类型都是 Int64Vector。另外 Matrix{AbstractFloat} 表示一个Matrix,其中所有元素的类型都是 AbstractFloat 的子类型。

大多数情况下,特别是在处理表格数据时,我们使用的是一维或二维数组。 它们都是 Julia 中的 Array 类型。 但是,可以使用简洁清晰的语法操作 VectorMatrix

3.3.7.2 数组构造

如何 构造 数组呢? 本届的开始,我们使用低级的方式构造数组。 在某些情况下,编写高性能代码就需要这样的做法。 然而,在大多数情况下,这不是必需的。同时可以安全地使用更简便的方法创建数组。 本节稍后讨论这些更简便的方法。

用于 Julia 数组的低级构造器是 默认构造器。 它接手元素类型作为 {} 括号内的类型参数,并将元素类型传递到构造器里,构造器后跟需要的维度。 通常使用未定义元素初始化向量和矩阵,即将 undef 参数作为传递到构造器里的类型。 如下构造一个含 10 个 undef Float64元素的向量:

my_vector = Vector{Float64}(undef, 10)
[0.0, 6.9077153247747e-310, 6.90771829209107e-310, 0.0, 6.9077153247747e-310, 6.9077153027599e-310, 0.0, 6.9077153247747e-310, 6.90771661543227e-310, 0.0]

矩阵的构造方式是,向构造器传递两个维度参数:一个用于 ,另一个用于 。 例如,具有 10 行 2列 undef 元素的矩阵以如下方式实例化:

my_matrix = Matrix{Float64}(undef, 10, 2)
10×2 Matrix{Float64}:
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310
 6.90773e-310  6.90773e-310

对于构造最常见元素类型的数组,Julia 中有一些语法别名

对于其他的元素,可以先创建全为 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

因此,也可以通过串联数组来创建数组:

3.3.7.3 数组检测

当拥有一些数组时,下一步应是对它们进行 检测 。 Julia 中提供了许多方便的函数,这使得用户能够检测任何数组。

知道数组中的 元素类型 是非常有用的。 这会用到 eltype 函数:

eltype(my_matrix_π)
Float64

了解到类型后,可能还会对 数组的维度 感兴趣。 Julia 中有多个用于检测数组维度的函数:

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 也为数组的 第一个最后一个 元素定义了特殊的关键字: beginend。 例如,可以如下方式检索向量的倒数第二个元素:

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]

上面这段代码可被解释为 “获取第二行的所有列”。

矩阵同样支持 beginend

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]

通常更好的做法是,在进行这些循环时使用特定的函数:

3.3.8 Pair

与有关数组的超长章节相比,关于 Pair 的章节将是简短的。 Pair 是一种包含两个对象的数据结构 (一般属于彼此)。 在 Julia 中,可以使用如下的语法构造 Pair

my_pair = "Julia" => 42
"Julia" => 42

这两个元素分别存储在字段 firstsecond

my_pair.first
Julia
my_pair.second
42

但,在大多数情况下,使用 firstlast 更简单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 能够告诉你哪些值属于这些键。 keyvalue 可以是任何类型,但 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 作为索引即可检索到 Dictvalue

name2number_map["one"]
1

如果要增加新的条目,可使用所需的 key 作为 Dict 的索引,并使用赋值运算符为其赋值 value

name2number_map["three"] = 3
3

可以使用 keysin 检查一个 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

  1. 13. 或者说,指向每一列元素的内存地址指针相邻存储。↩︎

  2. 14. 更简单的原因是 firstlast 也适用于其他集合,所以需要记住的就更少。↩︎



CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso, 刘贵欣 (中文翻译), 田俊 (中文审校)