Julia 学习笔记——1. 概述
尝试学一下 Julia,但是这份笔记的时间跨度可能很长,有空就写一点吧。
需要说明的是,这个系列不是零基础的 Julia 笔记,而是直接从 Python 和 MATLAB 到 Julia 的迁移学习,并且笔记是围绕主题进行的,不是逐渐展开的。
介绍
Julia是一门新兴的科学计算语言,它的设计目标非常宏大:希望 Julia 可以同时拥有 Python/MATLAB 一样的易用性和 C/C++/Fortran 一样的运算效率。
Julia有很多与众不同的特点:
- 从设计定位来说,Julia 具有后发优势,是为 JIT 编译而量身定制的语言,目的是解决现在泾渭分明的动态语言和静态语言之间,优劣不能兼顾,因而必须组合使用的问题。
- 从计算效率的角度,虽然和Python在使用时很相似,但是Julia的计算效率却比Python高很多。Python的科学计算模块Numpy等必须依赖于C/C++编写的底层库,但是Julia自身就可以提供足够的科学计算能力(主要是通过预编译实现的)。
- 从语法设计的角度,Python并没有关注于科学计算编程中的便利性,Julia和MATLAB、Fortran、R语言的定位类似,充分为科学计算的效率和便利性考虑,提供了足够多的语法糖。这些语法糖其实有利有弊,一个典型的特征是索引从0还是从1开始,选择从1开始的语言具有明显的科学计算定位。
- 作为一种新兴语言,采用了很多较新颖的语法设计,比如抛弃了传统的面向对象编程,仅支持C风格的结构体等。
但是Julia还是有很多争议的,比如:
- Julia的发展时间太短了,主要的语法特性并不稳定;
- 各种基础库不如其它语言全面和健壮,甚至有的基础库还存在低级问题;
- 用户群体相比其他语言太小,相应的教程和社区讨论相对缺乏;
- 第三方库不够丰富,由于跨语言调用比较方便,出现了大量依赖 Python 包的趋势。
Julia最理想的应用场景如下:
- 使用与 Python/MATLAB 相当的简单方式编写 demo 代码;
- 对现有代码进行简单优化,就可以达到与 C/C++/Fortran 相当的性能。
在整个过程中,不需要从动态语言 Python/MATLAB 迁移到静态语言 C/C++/Fortran 的代码重写或者跨语言组合方案,完全可以通过 Julia 自身来实现。
综合考虑,还是简单学习一下吧。
语法概述
Julia 的语法非常像 Python(Numpy) 和 MATLAB 的缝合怪,主要呈现如下特征:
- 由于设计目标并不是 Python 那样的通用语言,而是类似 MATLAB
那样的科学计算语言:
- 相对于 Python,Julia 提供很多科学计算的语法糖,使得语句相比 Python 更加简洁
- 相对于 MATLAB,Julia 没有语法设计上的奇葩历史包袱,语法更加合理
- 虽然仍然属于动态语言,但是 Julia 为了实现高效运算,在语法设计上相比于 Python/MATLAB 更加 “静态” 化,对于类型系统的处理更加严格规范。
为了避免混淆,在学习 Julia 之前非常有必要了解一下它们的主要语法差异(下面对于 Python 的讨论默认包含 Numpy)。
关于语法结构:
- Julia 使用
end标记结束,与 MATLAB 风格类似,Python 的语法结构非常依赖代码缩进,Julia 对代码缩进并不敏感,不需要游标卡尺。 - Julia 使用
#表示注释,与 Python 相同;MATLAB 则使用%表示注释 - Julia 没有 Python 的
pass关键词。 - Julia 的条件语句中使用
elseif,与 MATLAB 一致,而 Python 使用elif。 - Julia 的语句不鼓励使用
;结束,但是使用;不会报错,只是影响输出(在 REPL 中直接执行语句时,如果没有;会展示结果;但是在脚本中执行语句时,始终不会展示结果);MATLAB 不会区分 REPL 和脚本执行的情况,缺少;总是会打印语句的结果,因此通常都会使用;结尾。 - Julia 不需要续行语法,不完整的表达式会自动续行,MATLAB 需要使用
...续行,Python 使用\续行。
关于数组:
- Julia 提供的
Vector{Any}数组比较高效,Python 的原生列表灵活但非常缓慢,Numpy 数组则比较类似。 - Julia 与 MATLAB/Fortran 一样采用列优先排序,Numpy 主要采用行优先排序。
- Julia 有真正的一维数组,MATLAB 只有矩阵伪装的数组:尺寸为
(1,N)或(N,1)的向量。 - Julia 使用
a[i,j]访问数组元素,与 Numpy 类似,而 MATLAB 使用a(i,j)。 - Julia 中的数组尺寸相对固定,不允许在赋值过程中自动增长数组,MATLAB 则允许动态增长。
- Julia 使用
*可以直接进行矩阵乘法,而 Python 需要使用@。 - Julia 使用
'表示转置或共轭转置,而 Python 则需使用.T。 - Julia 建议使用
filter或filter!对数组的一部分进行处理,而 MATLAB 和 Numpy 通常使用逻辑索引。
关于索引和切片:
- Julia 索引从 1 开始,与 MATLAB 一致,而 Python 索引从 0 开始。
- Julia 支持使用
begin表示第一个元素的索引,使用end表示最后一个元素的索引,MATLAB 仅提供end,Python 则使用负数索引表示,例如-1表示最后一个。 - Julia 的切片语法为
start:step:stop,步长在中间,范围是严格闭区间。Python 切片语法是start:stop:step,步长在最后,范围是左闭右开。 - Julia
的切片语法比较严格,不允许缺省起点终点,例如
a[2:end]不可以省略end。 - Numpy 的高级索引非常奇怪(不是张量积),Julia 在类似情况采用了与 MATLAB 相似的处理。
- Julia 通过切片提取数组的部分元素时,默认会进行拷贝,也可以显式标记为视图,而 Numpy 的情况比较复杂,可能是拷贝或视图。
- Julia
的切片(
a:b、a:b:c)会构造实际的特殊对象,MATLAB 的切片则会对应完整向量,Python 只有在特定代码中才支持切片的语法。
关于数据类型:
- Julia 对于整数和浮点数等的区分比较严格,在实际运算时可能自动转换,也可能需要显式转换,比 Python 的类型处理更严格,MATLAB 几乎不需要关注数据类型,绝大多数情况下使用默认的浮点数即可。
- Julia 的整数类型通常是固定位数的,并不是 Python 那样的任意精度整数,这主要是为了运算效率。
- Julia 使用
nothing专门表示空值,与 Python 的None类似,MATLAB 则通常使用[]。 - Julia 可以使用与 Python 类似的语法进行类型标注,但是 Julia 的类型标注是强制的,而 Python 的类型注解只是参考性的,仅对代码提示和静态分析有意义。(不过 Julia 官方文档并不推荐用户手动进行大量的类型标注)
关于函数:
- Julia 支持给函数参数提供默认值,Python
的默认值求值只发生在定义时,但是 Julia
的默认值求值会发生在每一次调用时,MATLAB 不支持参数默认值,需要通过
nargin间接实现。 - Julia 关于关键字参数,与 Python 不同,Julia 严格区分位置参数和关键字参数的使用,MATLAB 需要特殊实现才能使用关键字参数。
- Julia 为所有函数提供了动态分派机制(类似于C++的虚函数),MATLAB 有类似的动态重载机制,Python 不支持函数重载。
- Julia 即使无参数的函数调用也不允许省略括号,例如
rand()。只有MATLAB才会有省略括号的这种垃圾语法。 - Julia 可以使用
return关键字返回结果,或者将最后一个表达式自动返回;Python 使用return关键字返回结果,否则在缺省时只会返回None。MATLAB 必须在函数定义时声明返回值,return语句完全不携带返回值,只是提前结束。 - Julia 可以用函数返回多个值组成的元组,与 Python 几乎一样,而 MATLAB
需要基于
nargout实现返回多个值。 - Julia 几乎舍弃了面向对象机制,结构体只能用于存储数据,函数会根据参数类型区分不同的实现,而 MATLAB 和 Python 都支持面向对象机制。
关于变量:
- Julia 在赋值和传参过程中并不会发生拷贝,存在浅拷贝和视图等复杂机制,MATLAB 则采用值传递+懒拷贝机制。
- 全局变量的存在对于运算效率的影响都是负面的,对于 Julia 同样如此。
- Julia 对变量作用域的处理与 Python 类似,代码块可能会引入新的局部作用域,而 MATLAB 除了默认的全局工作区之外,几乎只有函数才可以开辟独立的工作区,在使用上非常不便。
关于字面量:
- Julia 将整数字面量例如
42视作合适位数的整数类型,Python 统一视作任意精度整数,MATLAB 则统一视作浮点数,除非显式声明为其他类型。 - Julia 的复数单位为
im,MATLAB的复数单位为i和j,Python的复数单位只有j。 - Julia 只支持双引号的字符串,单引号表示字符;Python 单引号和双引号均可,不区分字符和字符串;MATLAB 的单引号是字符数组,双引号则是字符串,两者属于不同类型,但是很多操作相互兼容。
关于运算符:
- Julia 支持
+=等运算符,但是与 Numpy 数组不同,+=并不是就地运算,a += b始终是a = a + b的简写,MATLAB 完全不支持这类运算符。 - Julia 有很多专门的逐点运算符,并且在语法上严格区分普通运算符和逐点运算符,比 MATLAB 和 Python 的处理都更加严格。
- Julia 使用
^表示指数运算,与 MATLAB 相同,而 Python 使用**。 - Julia 使用
!=表示不等号,而 MATLAB 使用~=。 - Julia 使用
%表示取余,Python 使用%表示模运算,两者在处理负数时存在差异。MATLAB 的%用于表示注释。 - Julia 支持三元运算符
x > 0 ? 1 : -1,MATLAB 不支持,Python 则有类似的条件表达式1 if x > 0 else -1。 a^b^c的结合规则细节:在 Julia 中被认为是a^(b^c),而在 MATLAB 中被认为是(a^b)^c。
其他:
- 关于布尔值:Julia 在
if语句等只能使用显式的布尔值,或者将其它语句显式转换为布尔值,甚至不支持 0 和 1 的隐式转换,不支持其他非零值到布尔值的显式转换,而 MATLAB 和 Python 允许很多情况下到布尔值的隐式转换。 - 关于
ans:在 REPL 中,Julia 使用ans存储上一条命令的结果,MATLAB 也使用ans,Python 在 REPL 中使用_。在非交互式模式中,Julia 和 Python 都不会专门存储上一条命令结果,而 MATLAB 总是会设置。 - Julia 使用
_代表丢弃的值,MATLAB 使用~,而 Python 仍然使用_。 - 关于垃圾回收:Julia 没有类似于 MATLAB 的
clear函数可以手动清理对象,可以使用A = nothing来释放内存,但是实际行为仍然取决于底层的垃圾回收机制。 - Julia 不建议像 Python 那样判断一个脚本是作为主脚本还是库文件被导入,最好对脚本和库文件有不一样的写法,而不是兼顾两者。
除此之外,Julia 还有很多参考其它语言的部分:
- 完全支持 Unicode 字符作为变量名,包括中文和其它各种特殊符号,考虑到很多 Unicode 字符的形态非常相似,Julia 也进行了对应的优化处理;(但是考虑到编程习惯,除了数学公式中常见的希腊字母等,仍然不建议其他情况使用非ASCII字符作为变量名)
- 支持名称为
@开头的宏,并且这里的宏并不是类似于C语言中的简单文本替换,而是更复杂的基于 AST 的代码转换;(Python/MATLAB 没有编译的概念,因此没有宏,而且 Python 设计者明确反对使用宏,认为会降低代码可读性) - 按照约定,如果在函数名称的结尾有
!(这是函数名称的一部分),表示这个函数会修改参数,例如push!。 - 与 Python 和 MATLAB 不同,Julia 支持固定类型的“常量”,使用
const修饰,此时仍然允许改变这个变量的具体值,但是不允许改变类型。
如果考虑底层实现,那么 Julia 与 MATLAB/Python 实际上几乎没有什么共同点,反而更应该把 Julia 和 C/C++ 进行对比:C++ 有明确分开的编译期和运行期,Julia 则将两者混合到了一起(在后台偷偷地编译),这导致用户始终需要编译器才能运行 Julia 脚本,而 C++ 在运行阶段是完全不依赖编译器的。
Julia 使用了一个基于 LLVM 的 JIT 编译器,编译行为对用户基本上是透明的,用户在基本使用时无需考虑这些因素,但是在考虑代码的效率优化以及高性能计算时需要考虑这部分因素。
例如 C++ 的模板实例化完全发生在编译期,而 Julia 则会在函数首次调用时(注意不是函数定义时)针对不同的类型进行实例化,也就是在后台偷偷编译,这当然会带来非常明显的性能开销,在下一次遇到相同类型时就不需要编译了,此时的速度才可以与 C/C++ 相当。
Julia 与 MATLAB/Python 在向量化方面存在截然不同的表现:
- MATLAB 和 Python 的 for 循环非常慢,通常必须使用向量化语法,进而调用底层代码来加速;
- Julia 直接使用 for 循环已经足够高效,虽然也支持向量化,但是向量化并不会带来明显的性能差异,甚至可能表现得更加糟糕,因为可能产生额外的数据拷贝,对于 Julia 来说,向量化的意义只是写法更加简洁。
对于这部分只是简单了解,因此没有详细展开。
基本语法
注释
Julia 和 Python 一样使用 # 表示注释,支持多行注释:使用
#= 开始,使用 =# 结束,但是并不常见。
标识符
标识符可以使用字母,下划线或数字组成,对大小写敏感,数字不能在开头。
Julia 实际和 Python 3 一样,可以使用除了内部标识符和运算符之外的绝大多数 Unicode 字符作为标识符,不需要局限于英文字母,而且某些语义相同、形态相似的 Unicode 字符会被视作相同字符。
不建议使用英文和数字下划线之外的标识符。从 Julia 的文档可以看出,Unicode 字符支持给语法解析带来了很多困难,所以实在不理解设计者为什么非要维护这个糟糕的语法糖。
变量
在入门阶段,可以认为 Julia 的变量机制和 Python 基本类似,因此不再展开。
Julia 有内置常量或函数,比如 pi 和
ans,甚至允许我们覆盖内置的定义,例如重新赋值
pi=3,但是前提是在此之前我们从未使用过它,否则赋值会报错。
在交互式环境中,上一次的表达式结果会自动存储在特殊的内置变量
ans 中,但是如果我们在第一次就对其赋值,那么
ans 的默认功能就会失效。
常量
与 Python 不同,Julia 支持定义固定类型的常量,例如 1
2
3
4
5
6
7
8
9const a = 1;
a = 1; # 无实质更改,不会报错或提示
a = 2; # 不改变类型,但是改变值,会发出警告
# WARNING: redefinition of constant Main.a. This may fail, cause incorrect answers, or produce other errors.
a = 3.0; # 改变类型,会报错
# ERROR: invalid redefinition of constant Main.a
注意:
- 在定义 const 变量之前,必须确保它不是一个已经定义的变量,否则定义会报错。
- 虽然修改 const 变量的值可能不会报错,但是仍然不建议修改,因为与 C/C++ 类似,常量的修改不符合语义,可能在部分代码的编译中直接固定了常量的值,后续对常量的修改可能不会生效,进而导致产生不符合预期的结果。
const a, b = 1, 2会同时创建两个常量。
命名规范
虽然 Julia 对标识符的限制很少,但是 Julia 文档还是提供了一份命名规范:
- 变量使用小写加下划线
- 常量使用全大写
- 函数和宏的名称使用小写加下划线
- 对于会修改输入变量的函数,名称以
!结尾,这些函数又叫做 “mutating” 或 “in-place” 函数 - 类型和模块名称使用大驼峰
但是 Julia 不鼓励大量使用下划线,除非缺少下划线时的名称难以理解。
shell/REPL
需要介绍一下 Julia 的 shell/REPL 中的相关操作,它内置了多个模式,可以根据当前的提示符来区分:
julia>表示 Julia 的普通模式,以 REPL 形式运行 Julia 代码,这是默认模式,其它特殊模式都可以通过backspace退回到普通模式help?>表示 Julia 的帮助模式,普通模式按?进入,例如在普通模式可以输入?sqrt查询相关内容,在读取?时自动进入帮助模式,在展示帮助信息后自动退出(@v1.10) pkg>表示 Julia 的包管理模式,普通模式按]进入,Julia 提供了一整套包管理的命令,暂不讨论shell>表示 Julia 的 shell 模式,普通模式按;进入,注意这里的 shell 模式并不是 bash/pwsh,而是非常受限的,有很多 shell 的内置命令都不能使用。
在各种模式下(包括执行脚本)都需要注意当前的工作目录:
- 在普通模式,可以使用
pwd()获取当前工作目录,使用cd()可以改变当前工作目录,在 Shell 模式则可以使用常见的目录切换命令。 - 与 MATLAB 类似,对于 julia
脚本,最好在开头把工作目录设置为脚本所在目录(使用
cd(@__DIR__),这样脚本中的文件路径都是相对于脚本所在位置的,保证脚本可靠运行。
Julia 的这种分模式的处理显然吸取了 MATLAB 虚拟终端的设计失败教训。
关于特殊符号的输入:
- 有时需要使用一些 Unicode 字符,在 REPL 中通常直接使用 LaTeX
代码(例如
\div)加上 tab键就可以切换(在 Jupyter Notebook 中也支持)。 - 如果不知道特殊符号应该如何输入,可以在 REPL 中输入
?进入帮助模式,然后粘贴对应的 Unicode 字符,就可以获得关于这个字符的输入方法。
exit() 或 ctrl+d 可以退出 REPL。
输入输出基础
简单介绍一下基本的输入输出操作。
print() 和 println()
可以向控制台输出内容,区别是后者自带一个回车。 1
2print("hello,world!\n")
println("hello,world!")
也可以直接输出多个字符串,中间不会加入分隔符 1
print("a","b") # ab
Julia 支持和 Python 的 f-string
类似的格式化输出方式,在字符串中可以使用 $
插入变量或表达式,有歧义时需要加小括号,例如 1
2
3
4
5a = 1;
print("a=$a") # a=1
b = 2;
print("$(a+b)") # 3
readline()
可以从控制台获取一行输入数据,得到的是字符串数据,例如 1
s = readline()
通常需要对输入数据进行类型转换,可以使用 parse()
函数,例如 1
2s = readline() # "123"
x = parse(Int, s) # 123
日志打印
Julia 内置了一些信息输出的宏,包括
@show、@debug、@info、@warn
和 @error,例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16julia> @show "hi"
"hi" = "hi"
"hi"
julia> @debug "hi"
julia> @info "hi"
[ Info: hi
julia> @warn "hi"
┌ Warning: hi
└ @ Main REPL[12]:1
julia> @error "hi"
┌ Error: hi
└ @ Main REPL[13]:1
注意这里 @debug 的信息默认不会显示出来。
更完整的日志系统由
Logging这个包提供支持。
