3.2 语法

Julia 是一种即时编译的动态类型语言。 这意味着不像 C++ 或 FORTRAN 那样,需要在运行之前编译程序。 相反,Julia 会读取你的代码,并在运行前编译部分程序。 同时,你不需要为每一处代码显式地指定类型,Julia会在运行时推断类型。

Julia 与其他动态语言(如 R 和 Python)之间的主要区别如下。 首先,Julia 允许用户进行类型声明 。你应该在 为什么选择 Julia? (Section 2): 一节已经见过类型声明,就是一些跟在变量后的双冒号 :: 。 但是,如果你不想指定变量或函数的类型,Julia 将会很乐意推断(猜测)它们。

其次,Julia 允许用户通过多重派发定义不同参数类型组合的函数行为。 本书将会在 Section 2.3 讨论多重派发。 定义不同函数行为的方法是使用相同的函数名称定义新的函数,但将这些函数用于不同的参数类型。

3.2.1 变量

变量是在计算机中以特定名称存储的值,以便后面读取或更改此值。 Julia 有很多数据类型,但在数据科学中主要使用:

  • 整数: Int64
  • 实数: Float64
  • 布尔型: Bool
  • 字符串: String

整数和实数默认使用 64 位存储,这就是为什么它们的类型名称带有“64”后缀。 如果需要更高或更低的精度,Julia 还有 Int8 类型和 Int128 类型,其中 Int8 类型用于低精度,Int128 类型用于高精度。 多数情况下,用户不需要关心精度问题,使用默认值即可。

创建新变量的方法是在左侧写变量名并在右侧写其值,并在中间插入= 赋值运算符。 例如:

  1. name = "Julia"
  2. age = 9
  1. 9

请注意,最后一行代码 (age) 的值已打印到控制台。 上面的代码定义了两个变量 nameage。 将变量名称输入可重新得到变量的值:

  1. name
  1. Julia

如果要为现有变量定义新值,可以重复赋值中的步骤。 请注意,Julia 现在将使用新值覆盖旧值。 假设 Julia 已经过了生日,现在是 10 岁:

  1. age = 10
  1. 10

我们可以对 name 进行同样的操作。假设 Julia 因为惊人的速度获得了一些头衔。那么,我们可以更改 name 的值:

  1. name = "Julia Rapidus"
  1. Julia Rapidus

也可以对变量进行乘除法等运算。 将 age 乘以 12,可以得到 Julia 以月为单位的年龄:

  1. 12 * age
  1. 120

使用 typeof 函数可以查看变量的类型:

  1. typeof(age)
  1. Int64

接下来的问题是:“我还能对整数做什么?” Julia 中 有一个非常好用的函数 methodswith ,它可以为输出所有可用于指定类型的函数。 此处限制代码只显示前五行:

  1. first(methodswith(Int64), 5)
  1. [1] logmvbeta(p::Int64, a::T, b::T) where T<:Real in StatsFuns at /home/runner/.julia/packages/StatsFuns/mQJB7/src/misc.jl:22
  2. [2] logmvbeta(p::Int64, a::Real, b::Real) in StatsFuns at /home/runner/.julia/packages/StatsFuns/mQJB7/src/misc.jl:23
  3. [3] logmvgamma(p::Int64, a::Real) in StatsFuns at /home/runner/.julia/packages/StatsFuns/mQJB7/src/misc.jl:8
  4. [4] read(t::HTTP.ConnectionPool.Transaction, nb::Int64) in HTTP.ConnectionPool at /home/runner/.julia/packages/HTTP/aTjcj/src/ConnectionPool.jl:232
  5. [5] write(ctx::MbedTLS.MD, i::Union{Float16, Float32, Float64, Int128, Int16, Int32, Int64, UInt128, UInt16, UInt32, UInt64}) in MbedTLS at /home/runner/.julia/packages/MbedTLS/lqmet/src/md.jl:140

3.2.2 用户定义类型

不凭借任何依赖关系或层次结构来组织多个变量是不现实的。 在 Julia 中,我们可以使用 struct(也称为复合类型)来定义结构化数据。 在每个 struct 中都可以定义一组字段。 它们不同于 Julia 语言内核中已经默认定义的原始类型(例如 IntegerFloat)。 由于大多数 struct 都是用户定义的,因此它们也被称为用户定义类型。

例如,创建 struct 表示用于科学计算的开源编程语言。 在 struct 中定义一组相应类型的字段:

  1. struct Language
  2. name::String
  3. title::String
  4. year_of_birth::Int64
  5. fast::Bool
  6. end

可以通过将 struct 作为参数传递给 fieldnames检查字段名称列表:

  1. fieldnames(Language)
  1. (:name, :title, :year_of_birth, :fast)

要使用 struct,必须创建单个实例(或“对象”),每个struct实例的字段值都是特定的。 如下所示,创建两个实例 Julia 和 Python:

  1. julia = Language("Julia", "Rapidus", 2012, true)
  2. python = Language("Python", "Letargicus", 1991, false)
  1. Language("Python", "Letargicus", 1991, false)

struct 实例的值在构造后无法修改。 如果需要,可以创建 mutable struct。 但请注意,可变对象一般来说更慢且更容易出现错误。 因此,尽可能确保所有类型都是 不可变的。 接下来创建一个 mutable struct

  1. mutable struct MutableLanguage
  2. name::String
  3. title::String
  4. year_of_birth::Int64
  5. fast::Bool
  6. end
  7. julia_mutable = MutableLanguage("Julia", "Rapidus", 2012, true)
  1. MutableLanguage("Julia", "Rapidus", 2012, true)

假设想要改变 julia_mutable 的标题。 因为 julia_mutablemutable struct 的实例,所以该操作可行:

  1. julia_mutable.title = "Python Obliteratus"
  2. julia_mutable
  1. MutableLanguage("Julia", "Python Obliteratus", 2012, true)

3.2.3 布尔运算和数值比较

上节讨论了类型,本节讨论布尔运算和数值比较。

Julia 中有三种布尔运算符:

  • !NOT
  • &&AND
  • ||OR

一些例子如下:

  1. !true
  1. false
  1. (false && true) || (!false)
  1. true
  1. (6 isa Int64) && (6 isa Real)
  1. true

关于数值比较,Julia有三种主要的比较类型:

  1. 相等:两者的关系为 相等不等
    • \== “相等”
    • != 或 ≠ “不等”
  2. 小于: 两者的关系为 小于小于等于
    • < “小于”
    • <= 或 ≤ “小于等于”
  3. 大于: 两者的关系为 大于大于等于
    • > “大于”
    • >= 或 ≥ “大于等于”

下面是一些例子:

  1. 1 == 1
  1. true
  1. 1 >= 10
  1. false

甚至可以比较不同类型:

  1. 1 == 1.0
  1. true

还可以将布尔运算与数值比较:

  1. (1 != 10) || (3.14 <= 2.71)
  1. true

3.2.4 函数

上节学习了如何定义变量和自定义类型 struct,本节讨论 函数。 在 Julia 里,函数是 一组参数值到一个或多个返回值的映射。 基础语法如下所示:

  1. function function_name(arg1, arg2)
  2. result = stuff with the arg1 and arg2
  3. return result
  4. end

函数声明以关键字 function 开始,后接函数名称。 然后在 () 里定义参数, 这些参数由 , 分隔。 接着在函数体内部定义我们希望 Julia 对传入参数执行的操作。 函数里定义的所有变量都会在函数返回后删除。这很不错,因为有点像自动垃圾回收。 在函数体内的所有操作完成后,Julia 使用 return 关键字返回最终结果。 最后,Julia 以 end 关键字结束函数定义。

还有一种紧凑的 赋值形式

  1. f_name(arg1, arg2) = stuff with the arg1 and arg2

这种形式更加紧凑,但 等效于 前面的同名函数。 根据经验,当代码符合一行最多只有92字符时,紧凑形式更加合适。 否则,只需使用带 function 关键字的较长形式。 接下来深入讨论一些例子。

3.2.4.1 创建函数

下面是一个将传入数字相加的函数:

  1. function add_numbers(x, y)
  2. return x + y
  3. end
  1. add_numbers (generic function with 1 method)

接下来调用 add_numbers 函数:

  1. add_numbers(17, 29)
  1. 46

它也适用于浮点数:

  1. add_numbers(3.14, 2.72)
  1. 5.86

另外,还可以通过制定类型声明来创建自定义函数行为。 假设创建一个 round_number 函数, 它在传入参数类型是 Float64Int64 时进行不同的操作:

  1. function round_number(x::Float64)
  2. return round(x)
  3. end
  4. function round_number(x::Int64)
  5. return x
  6. end
  1. round_number (generic function with 2 methods)

可以看到,它是具有多种方法的函数:

  1. methods(round_number)
  1. round_number(x::Float64) in Main at none:1
  1. round_number(x::Int64) in Main at none:5

但问题是:如果想对 32 位浮点数 Float32 或者 8 位整数 Int8 作四舍五入,该怎么办?

如果想定义关于所有浮点数和整数类型的函数,那么需要使用 abstract type 作为函数签名, 例如 AbstractFloatInteger

  1. function round_number(x::AbstractFloat)
  2. return round(x)
  3. end
  1. round_number (generic function with 3 methods)

现在该函数适用于任何的浮点数类型:

  1. x_32 = Float32(1.1)
  2. round_number(x_32)
  1. 1.0f0

NOTE: 可以使用 supertypessubtypes 函数查看类型间的关系。

接下来回到之前定义的 Language struct。 这就是一个多重派发的例子。 下面将扩展 Base.show 函数,该函数打印实例的类型和 struct 的内容。

默认情况下, struct 有基本的输出样式,正如在 python 例子中看到的那样。 可以为 Language 类型定义新的 Base.show 方法, 以便为编程语言实例提供更漂亮的输出。 该方法将更清晰地打印编程语言的姓名,称号和年龄。 函数 Base.show 接收两个参数,第一个是 IO 类型的 io ,另一个是 Language 类型的 l

  1. Base.show(io::IO, l::Language) = print(
  2. io, l.name, ", ",
  3. 2021 - l.year_of_birth, " years old, ",
  4. "has the following titles: ", l.title
  5. )

现在查看 python 如何输出:

  1. python
  1. Python, 30 years old, has the following titles: Letargicus

3.2.4.2 多返回值

一个函数可以返回两个以上的值。 下面看一个新函数 add_multiply

  1. function add_multiply(x, y)
  2. addition = x + y
  3. multiplication = x * y
  4. return addition, multiplication
  5. end
  1. add_multiply (generic function with 1 method)

再接收返回值时,有两种写法:

  1. 与返回值的形式类似,依次为每个返回值定义一个变量,在本例中则需要两个变量:

    1. return_1, return_2 = add_multiply(1, 2)
    2. return_2
    1. 2
  2. 也可以定义一个变量来接受所有的返回值,然后通过 firstlast 访问每个返回值:

    1. all_returns = add_multiply(1, 2)
    2. last(all_returns)
    1. 2

3.2.4.3 关键字参数

某些函数可以接受关键字参数而不是位置参数。 这些参数与常规参数类似,只是定义在常规函数参数之后且使用分号 ; 分隔。 例如,定义 logarithm 函数,该函数默认使用基 \(e\) (2.718281828459045)作为关键字参数。 注意,此处使用抽象类型 Real,以便于覆盖从 IntegerAbstractFloat 派生的所有类型,这两种类型本身也都是 Real 的子类型:

  1. AbstractFloat <: Real && Integer <: Real
  1. true
  1. function logarithm(x::Real; base::Real=2.7182818284590)
  2. return log(base, x)
  3. end
  1. logarithm (generic function with 1 method)

当未指定 base 参数时函数正常运行,这是因为函数声明中提供了 默认参数

  1. logarithm(10)
  1. 2.3025850929940845

同时也可以指定与默认值不同的 base 值:

  1. logarithm(10; base=2)
  1. 3.3219280948873626

3.2.4.4 匿名函数

很多情况下,我们不关心函数名称,只想快速创建函数。 因此我们需要 匿名函数 。 Julia 数据科学工作流中经常会用到它。 例如,在使用 DataFrames.jl (Section 4) 或 Makie.jl (Section 5) 时,时常需要一个临时函数来筛选数据或者格式化图标签。 这就是使用匿名函数的时机。 当我们不想创建函数时它特别有用,因为一个简单的 in-place 语句就够用了。

它的语法特别简单, 只需使用 ->-> 的左侧定义参数名称。 -> 的右侧定义了想对左侧参数进行的操作。 考虑这样一个例子, 假设想通过指数函数来抵消对数运算:

  1. map(x -> 2.7182818284590^x, logarithm(2))
  1. 2.0

这里使用 map 函数方便地将匿名函数(第一个参数)映射到了 logarithm(2) (第二个参数)。 因此,我们得到了相同的数字,因为指数运算和对数运算是互逆的(在选择相同的基 – 2.7182818284590 时)。

3.2.5 条件表达式 If-Elseif-Else

在大多数语言中,用户可以控制程序的执行流。 我们可依据情况使计算机做这一件或另外一件事。 Julia 使用 ifelseifelse 关键字进行流程控制。 它们也被称为条件语句。

if 关键字执行一个表达式,,然后根据表达式的结果为 true 还是 false 执行相应分支的代码。 在复杂的控制流中,可以使用 elseif 组合多个 if 条件。 最后,如果 ifelseif 分支的语句都被执行为 true,那么我们可以定义另外的分支。 这就是 else 关键字的作用。 与之前见到的关键字运算符一样,我们必须告诉 Julia 条件语句以 end 关键字结束。

下面是一个包含所有 if-elseif-else 关键字的例子:

  1. a = 1
  2. b = 2
  3. if a < b
  4. "a is less than b"
  5. elseif a > b
  6. "a is greater than b"
  7. else
  8. "a is equal to b"
  9. end
  1. a is less than b

我们甚至可将其包装成函数 compare:

  1. function compare(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. compare(3.14, 3.14)

a is equal to b

3.2.6 For 循环

Julia 中的经典 for 循环遵循与条件语句类似的语法。 它以 for 关键字开始。 然后,向 Julia 指定一组要 “循环” 的语句。 另外,与其他一样,它也以 end 关键字结束。

比如使用如下的 for 循环使 Julia 打印 1-10 的数字:

  1. for i in 1:10
  2. println(i)
  3. end

3.2.7 while 循环

while 循环是前面的条件语句和 for 循环的结合体。 在 while 循环中,当条件为 true 时将一直执行循环体。 语法与之前的语句相同。 以 while 开始,紧跟计算结果为 truefalse 的条件表达式。 它仍以 end 关键字结束。

例子如下:

  1. n = 0
  2. while n < 3
  3. global n += 1
  4. end
  5. n
  1. 3

可以看到,我们不得不使用 global 关键字。 这是因为, 在条件语句中,循环和函数内定义的变量仅存在于其内部。 这就是变量的 作用域 。 我们需要通过 global 关键字告诉 Julia while 循环中的 n 是全局作用域中的 n。 最后,循环体使用的 += 运算符是 n = n + 1 的缩写。

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