Julia 是一种即时编译的动态类型语言。 这意味着不像 C++ 或 FORTRAN 那样,需要在运行之前编译程序。 相反,Julia 会读取你的代码,并在运行前编译部分程序。 同时,你不需要为每一处代码显式地指定类型,Julia会在运行时推断类型。
Julia 与其他动态语言(如 R 和 Python)之间的主要区别如下。 首先,Julia 允许用户进行类型声明 。你应该在 为什么选择 Julia? (Section 2): 一节已经见过类型声明,就是一些跟在变量后的双冒号 ::
。 但是,如果你不想指定变量或函数的类型,Julia 将会很乐意推断(猜测)它们。
其次,Julia 允许用户通过多重派发定义不同参数类型组合的函数行为。 本书将会在 Section 2.3 讨论多重派发。 定义不同函数行为的方法是使用相同的函数名称定义新的函数,但将这些函数用于不同的参数类型。
变量是在计算机中以特定名称存储的值,以便后面读取或更改此值。 Julia 有很多数据类型,但在数据科学中主要使用:
Int64
Float64
Bool
String
整数和实数默认使用 64 位存储,这就是为什么它们的类型名称带有“64”后缀。 如果需要更高或更低的精度,Julia 还有 Int8
类型和 Int128
类型,其中 Int8
类型用于低精度,Int128
类型用于高精度。 多数情况下,用户不需要关心精度问题,使用默认值即可。
创建新变量的方法是在左侧写变量名并在右侧写其值,并在中间插入=
赋值运算符。 例如:
name = "Julia"
age = 9
9
请注意,最后一行代码 (age
) 的值已打印到控制台。 上面的代码定义了两个变量 name
和 age
。 将变量名称输入可重新得到变量的值:
name
Julia
如果要为现有变量定义新值,可以重复赋值中的步骤。 请注意,Julia 现在将使用新值覆盖旧值。 假设 Julia 已经过了生日,现在是 10 岁:
age = 10
10
我们可以对 name
进行同样的操作。假设 Julia 因为惊人的速度获得了一些头衔。那么,我们可以更改 name
的值:
name = "Julia Rapidus"
Julia Rapidus
也可以对变量进行乘除法等运算。 将 age
乘以 12,可以得到 Julia 以月为单位的年龄:
12 * age
120
使用 typeof
函数可以查看变量的类型:
typeof(age)
Int64
接下来的问题是:“我还能对整数做什么?” Julia 中 有一个非常好用的函数 methodswith
,它可以为输出所有可用于指定类型的函数。 此处限制代码只显示前五行:
first(methodswith(Int64), 5)
[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] logmvbeta(p::Int64, a::Real, b::Real) in StatsFuns at /home/runner/.julia/packages/StatsFuns/mQJB7/src/misc.jl:23
[3] logmvgamma(p::Int64, a::Real) in StatsFuns at /home/runner/.julia/packages/StatsFuns/mQJB7/src/misc.jl:8
[4] read(t::HTTP.ConnectionPool.Transaction, nb::Int64) in HTTP.ConnectionPool at /home/runner/.julia/packages/HTTP/aTjcj/src/ConnectionPool.jl:232
[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/Vaaz8/src/md.jl:140
不凭借任何依赖关系或层次结构来组织多个变量是不现实的。 在 Julia 中,我们可以使用 struct
(也称为复合类型)来定义结构化数据。 在每个 struct
中都可以定义一组字段。 它们不同于 Julia 语言内核中已经默认定义的原始类型(例如 Integer
和 Float
)。 由于大多数 struct
都是用户定义的,因此它们也被称为用户定义类型。
例如,创建 struct
表示用于科学计算的开源编程语言。 在 struct
中定义一组相应类型的字段:
struct Language
name::String
title::String
year_of_birth::Int64
fast::Bool
end
可以通过将 struct
作为参数传递给 fieldnames
检查字段名称列表:
fieldnames(Language)
(:name, :title, :year_of_birth, :fast)
要使用 struct
,必须创建单个实例(或“对象”),每个struct
实例的字段值都是特定的。 如下所示,创建两个实例 Julia 和 Python:
julia = Language("Julia", "Rapidus", 2012, true)
python = Language("Python", "Letargicus", 1991, false)
Language("Python", "Letargicus", 1991, false)
struct
实例的值在构造后无法修改。 如果需要,可以创建 mutable struct
。 但请注意,可变对象一般来说更慢且更容易出现错误。 因此,尽可能确保所有类型都是 不可变的。 接下来创建一个 mutable struct
:
mutable struct MutableLanguage
name::String
title::String
year_of_birth::Int64
fast::Bool
end
julia_mutable = MutableLanguage("Julia", "Rapidus", 2012, true)
MutableLanguage("Julia", "Rapidus", 2012, true)
假设想要改变 julia_mutable
的标题。 因为 julia_mutable
是 mutable struct
的实例,所以该操作可行:
julia_mutable.title = "Python Obliteratus"
julia_mutable
MutableLanguage("Julia", "Python Obliteratus", 2012, true)
上节讨论了类型,本节讨论布尔运算和数值比较。
Julia 中有三种布尔运算符:
!
: NOT&&
: AND||
: OR一些例子如下:
!true
false
(false && true) || (!false)
true
(6 isa Int64) && (6 isa Real)
true
关于数值比较,Julia有三种主要的比较类型:
下面是一些例子:
1 == 1
true
1 >= 10
false
甚至可以比较不同类型:
1 == 1.0
true
还可以将布尔运算与数值比较:
(1 != 10) || (3.14 <= 2.71)
true
上节学习了如何定义变量和自定义类型 struct
,本节讨论 函数。 在 Julia 里,函数是 一组参数值到一个或多个返回值的映射。 基础语法如下所示:
function function_name(arg1, arg2)
result = stuff with the arg1 and arg2
return result
end
函数声明以关键字 function
开始,后接函数名称。 然后在 ()
里定义参数, 这些参数由 ,
分隔。 接着在函数体内部定义我们希望 Julia 对传入参数执行的操作。 函数里定义的所有变量都会在函数返回后删除。这很不错,因为有点像自动垃圾回收。 在函数体内的所有操作完成后,Julia 使用 return
关键字返回最终结果。 最后,Julia 以 end
关键字结束函数定义。
还有一种紧凑的 赋值形式:
f_name(arg1, arg2) = stuff with the arg1 and arg2
这种形式更加紧凑,但 等效于 前面的同名函数。 根据经验,当代码符合一行最多只有92字符时,紧凑形式更加合适。 否则,只需使用带 function
关键字的较长形式。 接下来深入讨论一些例子。
下面是一个将传入数字相加的函数:
function add_numbers(x, y)
return x + y
end
add_numbers (generic function with 1 method)
接下来调用 add_numbers
函数:
add_numbers(17, 29)
46
它也适用于浮点数:
add_numbers(3.14, 2.72)
5.86
另外,还可以通过制定类型声明来创建自定义函数行为。 假设创建一个 round_number
函数, 它在传入参数类型是 Float64
或 Int64
时进行不同的操作:
function round_number(x::Float64)
return round(x)
end
function round_number(x::Int64)
return x
end
round_number (generic function with 2 methods)
可以看到,它是具有多种方法的函数:
methods(round_number)
round_number(x::Float64) in Main at none:1
round_number(x::Int64) in Main at none:5
但问题是:如果想对 32 位浮点数 Float32
或者 8 位整数 Int8
作四舍五入,该怎么办?
如果想定义关于所有浮点数和整数类型的函数,那么需要使用 abstract type 作为函数签名, 例如 AbstractFloat
或 Integer
:
function round_number(x::AbstractFloat)
return round(x)
end
round_number (generic function with 3 methods)
现在该函数适用于任何的浮点数类型:
x_32 = Float32(1.1)
round_number(x_32)
1.0f0
NOTE: 可以使用
supertypes
和subtypes
函数查看类型间的关系。
接下来回到之前定义的 Language
struct
。 这就是一个多重派发的例子。 下面将扩展 Base.show
函数,该函数打印实例的类型和 struct
的内容。
默认情况下, struct
有基本的输出样式,正如在 python
例子中看到的那样。 可以为 Language
类型定义新的 Base.show
方法, 以便为编程语言实例提供更漂亮的输出。 该方法将更清晰地打印编程语言的姓名,称号和年龄。 函数 Base.show
接收两个参数,第一个是 IO
类型的 io
,另一个是 Language
类型的 l
:
Base.show(io::IO, l::Language) = print(
io, l.name, ", ",
2021 - l.year_of_birth, " years old, ",
"has the following titles: ", l.title
)
现在查看 python
如何输出:
python
Python, 30 years old, has the following titles: Letargicus
一个函数可以返回两个以上的值。 下面看一个新函数 add_multiply
:
function add_multiply(x, y)
addition = x + y
multiplication = x * y
return addition, multiplication
end
add_multiply (generic function with 1 method)
再接收返回值时,有两种写法:
与返回值的形式类似,依次为每个返回值定义一个变量,在本例中则需要两个变量:
return_1, return_2 = add_multiply(1, 2)
return_2
2
也可以定义一个变量来接受所有的返回值,然后通过 first
或 last
访问每个返回值:
all_returns = add_multiply(1, 2)
last(all_returns)
2
某些函数可以接受关键字参数而不是位置参数。 这些参数与常规参数类似,只是定义在常规函数参数之后且使用分号 ;
分隔。 例如,定义 logarithm
函数,该函数默认使用基 \(e\) (2.718281828459045)作为关键字参数。 注意,此处使用抽象类型 Real
,以便于覆盖从 Integer
和 AbstractFloat
派生的所有类型,这两种类型本身也都是 Real
的子类型:
AbstractFloat <: Real && Integer <: Real
true
function logarithm(x::Real; base::Real=2.7182818284590)
return log(base, x)
end
logarithm (generic function with 1 method)
当未指定 base
参数时函数正常运行,这是因为函数声明中提供了 默认参数 :
logarithm(10)
2.3025850929940845
同时也可以指定与默认值不同的 base
值:
logarithm(10; base=2)
3.3219280948873626
很多情况下,我们不关心函数名称,只想快速创建函数。 因此我们需要 匿名函数 。 Julia 数据科学工作流中经常会用到它。 例如,在使用 DataFrames.jl
(Section 4) 或 Makie.jl
(Section 5) 时,时常需要一个临时函数来筛选数据或者格式化图标签。 这就是使用匿名函数的时机。 当我们不想创建函数时它特别有用,因为一个简单的 in-place 语句就够用了。
它的语法特别简单, 只需使用 ->
。 ->
的左侧定义参数名称。 ->
的右侧定义了想对左侧参数进行的操作。 考虑这样一个例子, 假设想通过指数函数来抵消对数运算:
map(x -> 2.7182818284590^x, logarithm(2))
2.0
这里使用 map
函数方便地将匿名函数(第一个参数)映射到了 logarithm(2)
(第二个参数)。 因此,我们得到了相同的数字,因为指数运算和对数运算是互逆的(在选择相同的基 – 2.7182818284590 时)。
在大多数语言中,用户可以控制程序的执行流。 我们可依据情况使计算机做这一件或另外一件事。 Julia 使用 if
,elseif
和 else
关键字进行流程控制。 它们也被称为条件语句。
if
关键字执行一个表达式,,然后根据表达式的结果为 true
还是 false
执行相应分支的代码。 在复杂的控制流中,可以使用 elseif
组合多个 if
条件。 最后,如果 if
或 elseif
分支的语句都被执行为 true
,那么我们可以定义另外的分支。 这就是 else
关键字的作用。 与之前见到的关键字运算符一样,我们必须告诉 Julia 条件语句以 end
关键字结束。
下面是一个包含所有 if
-elseif
-else
关键字的例子:
a = 1
b = 2
if a < b
"a is less than b"
elseif a > b
"a is greater than b"
else
"a is equal to b"
end
a is less than b
我们甚至可将其包装成函数 compare
:
function compare(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
compare(3.14, 3.14)
a is equal to b
Julia 中的经典 for 循环遵循与条件语句类似的语法。 它以 for
关键字开始。 然后,向 Julia 指定一组要 “循环” 的语句。 另外,与其他一样,它也以 end
关键字结束。
比如使用如下的 for 循环使 Julia 打印 1-10 的数字:
for i in 1:10
println(i)
end
while 循环是前面的条件语句和 for 循环的结合体。 在 while 循环中,当条件为 true
时将一直执行循环体。 语法与之前的语句相同。 以 while
开始,紧跟计算结果为 true
或 false
的条件表达式。 它仍以 end
关键字结束。
例子如下:
n = 0
while n < 3
global n += 1
end
n
3
可以看到,我们不得不使用 global
关键字。 这是因为, 在条件语句中,循环和函数内定义的变量仅存在于其内部。 这就是变量的 作用域 。 我们需要通过 global
关键字告诉 Julia while
循环中的 n
是全局作用域中的 n
。 最后,循环体使用的 +=
运算符是 n = n + 1
的缩写。