3.3 原生数据结构

Julia 有多种原生数据结构。 它们都是某种结构化数据形式的抽象。 本书将讨论最常用的数据结构。 它们都能够保存同类型或异构的数据。 因为它们都是集合, 所以都能通过 for 循环进行 遍历 。 接下来的讨论包括 StringTupleNamedTupleUnitRangeArraysPairDict, 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]
  2. add_one!(my_data)
  3. 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. "
  1. This is a big multiline string.
  2. As you can see.
  3. 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"
  3. hello * goodbye
  1. HelloGoodbye

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

    • 第一个参数的 子串

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

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

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

    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, breturn (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"
  4. 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} ,其中 TUnitRange 中元素的类型:

  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} 表示所有元素的类型都是 Int64Vector。另外 Matrix{AbstractFloat} 表示一个Matrix,其中所有元素的类型都是 AbstractFloat 的子类型。

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

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]
  2. my_example_matrix = [[1 2 3]
  3. [4 5 6]
  4. [7 8 9]]

首先考虑向量。 假设要访问向量的第二个元素。 你只需要在 [] 括号内添加对应索引

  1. my_example_vector[2]
  1. 2

关于矩阵的语法也是如此。 但因为矩阵是二维数组,需要 同时 指定行和列。 接下来检索位于第二行(第一维)、第一列(第二维)的元素:

  1. my_example_matrix[2, 1]
  1. 4

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

  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]

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

矩阵同样支持 beginend

  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]
  2. empty_vector = Int64[]
  3. for i in simple_vector
  4. push!(empty_vector, i + 1)
  5. end
  6. empty_vector
  1. [2, 3, 4]

有时,你不想要迭代数组的每个元素,而是迭代每个数组索引。 可以使用 eachindex 函数结合 for 循环来迭代每个数组索引

然后,此处也展示一个向量的例子:

  1. forty_twos = [42, 42, 42]
  2. empty_vector = Int64[]
  3. for i in eachindex(forty_twos)
  4. push!(empty_vector, i)
  5. end
  6. 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]]
  3. row_major = [[1 2]
  4. [3 4]]

如果遍历的是以列优先方式存储的向量,那么结果将是有序的:

  1. indexes = Int64[]
  2. for i in column_major
  3. push!(indexes, i)
  4. end
  5. indexes
  1. [1, 2, 3, 4]

然而,如果遍历的是以其他方式存储的向量,那么结果将不是有序的:

  1. indexes = Int64[]
  2. for i in row_major
  3. push!(indexes, i)
  4. end
  5. 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

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

  1. my_pair.first
  1. Julia
  1. my_pair.second
  1. 42

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

  1. name2number_map["one"]
  1. 1

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

  1. name2number_map["three"] = 3
  1. 3

可以使用 keysin 检查一个 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]
  3. 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]
  2. 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, 刘贵欣 (中文翻译), 田俊 (中文审校)