这套笔记主要记录从 Python 和 MATLAB 迁移到 Julia 时需要关注的内容。

需要说明的是,这个系列不是零基础入门教程,而是围绕具体主题展开的整理型笔记,因此不同篇之间会有交叉,不严格追求循序渐进。

除非特别说明,下文默认以 Julia 最新版本 1.12 为准。

概述

Julia 对我而言更像是一种值得单独学习的科学计算语言,而不是 Python 或 MATLAB 的某个附属工具。

它最吸引人的地方很明确:

  • 写法上尽量保留动态语言和科学计算语言的易用性;
  • 执行上尽量逼近静态编译语言的性能;
  • 语言本身直接把数组、线性代数、泛型函数和多重分派当作核心设计对象。

介绍

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 常识

运行方式

最基本的几种使用方式如下:

  • 直接启动 Julia REPL,进入交互模式;
  • 执行 .jl 脚本文件;
  • 使用命令行选项直接执行表达式或项目代码。

最常见的命令例如:

1
2
3
4
5
julia

julia hello.jl

julia -e "println(1 + 2)"

其中:

  • 直接输入 julia 会进入 REPL;
  • julia hello.jl 会执行脚本;
  • julia -e 适合快速测试一小段代码。

如果希望脚本执行完成后仍然留在交互环境中,可以使用:

1
julia -i hello.jl

这点和 Python 的 python -i script.py 很类似。

脚本与项目

如果脚本依赖当前目录下的 Julia 项目环境,通常会写成:

1
julia --project=. hello.jl

这表示使用当前目录下的 Project.toml / Manifest.toml 作为环境。

如果只是随手写的小脚本,没有项目环境要求,那么直接执行 julia hello.jl 就够了。

查看版本

查看 Julia 版本最常见的方式:

1
julia --version

输出形如:

1
julia version 1.12.x

REPL 和脚本的区别

和 Python 一样,Julia 的 REPL 与脚本执行体验并不完全一样。

最常见的差异包括:

  • REPL 会自动显示表达式结果,脚本不会;
  • REPL 中有 ans 等交互式便利特性;
  • 某些作用域相关行为在 REPL 和脚本里需要格外注意。

因此,平时可以把它们简单区分为:

  • REPL:适合试验和探索;
  • 脚本:适合真正保存和执行代码。

迁移视角

默认读者已经熟悉 Python 和 MATLAB,这里不会把所有语法差异逐条铺开,而只保留几个会反复影响后续阅读的结论:

  • Julia 的整体定位更接近“高性能科学计算语言”,而不是通用脚本语言,因此数组、线性代数、广播、类型系统和函数分派都处在语言中心。
  • 语法观感上,它同时吸收了 Python、MATLAB、Fortran 等语言的一些习惯,但底层执行模型并不接近这些解释型工作流,而是明显更靠近带 JIT 的编译型路线。
  • 从 Python 迁移时,最需要重新适应的通常是:1-based 索引、闭区间切片、* 直接表示矩阵乘法、严格得多的类型和方法分派、以及更强的编译参与感。
  • 从 MATLAB 迁移时,最需要重新适应的通常是:真正的一维数组、更加严格的作用域与类型语义、模块和包管理、以及不再依赖“脚本工作区 + 向量化优先”的使用习惯。(MATLAB 的语言设计整体已经落后了)
  • Julia 里很多“写起来像脚本语言”的代码,背后其实都在为后续的类型推断和特化编译服务,因此写法是否利于推断,会比 Python 和 MATLAB 中重要得多。
  • 这套笔记后面各篇会在必要处顺手提一下和 Python / MATLAB 的差异,但默认不再展开基础背景。

如果考虑底层实现,那么 Julia 与 MATLAB/Python 实际上并不算同一路线,反而更适合和 C/C++ 对照来看:C++ 有明确分开的编译期和运行期,而 Julia 则把这两部分更紧密地揉在了一起,这导致用户始终需要编译器参与 Julia 程序的执行。

Julia 使用了一个基于 LLVM 的 JIT 编译器,编译行为对用户基本上是透明的,用户在基本使用时无需考虑这些因素,但是在考虑代码的效率优化以及高性能计算时需要考虑这部分因素。

例如 C++ 的模板实例化完全发生在编译期,而 Julia 则会在函数首次调用时(不是函数定义时)针对不同类型触发专门的方法编译,这当然会带来明显的首次调用开销;在下一次遇到相同类型时,这部分代价通常就不需要再次支付了。

Julia 与 MATLAB/Python 在向量化方面存在截然不同的表现:

  • MATLAB 和 Python 的原生 for 循环非常慢,通常必须使用向量化语法,通过调用底层代码来加速;
  • Julia 直接使用原生 for 循环通常已经足够高效,虽然也支持向量化,但向量化并不一定天然更快;如果产生额外的数据拷贝,反而可能更慢。对 Julia 来说,向量化更多是表达风格问题,而不是默认的性能捷径。

基本语法

注释

Julia 和 Python 一样使用 # 表示注释,支持多行注释:使用 #= 开始,使用 =# 结束,但是并不常见。

标识符

标识符可以使用字母,下划线或数字组成,对大小写敏感,数字不能在开头。

Julia 实际和 Python 3 一样,可以使用除了内部标识符和运算符之外的绝大多数 Unicode 字符作为标识符,不需要局限于英文字母,而且某些语义相同、形态相似的 Unicode 字符会被视作相同字符。

虽然 Julia 很强调 Unicode 友好,可以在把科学计算公式翻译为代码时更加方便,但在一般工程代码中,仍然更建议把变量名控制在英文、数字和下划线范围内。

变量

在入门阶段,可以认为 Julia 的变量机制和 Python 基本类似,因此不再展开。

Julia 有内置常量或函数,比如 pians,甚至允许我们覆盖内置的定义,例如重新赋值 pi=3,但是前提是在此之前我们从未使用过它,否则赋值会报错。

在交互式环境中,上一次的表达式结果会自动存储在特殊的内置变量 ans 中,但是如果我们在第一次就对其赋值,那么 ans 的默认功能就会失效。

常量

与 Python 不同,Julia 支持定义固定类型的常量,例如

1
2
3
4
5
6
7
8
9
const 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.12) 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
2
print("hello,world!\n")
println("hello,world!")

也可以直接输出多个字符串,中间不会加入分隔符

1
print("a","b") # ab

Julia 支持和 Python 的 f-string 类似的格式化输出方式,在字符串中可以使用 $ 插入变量或表达式,有歧义时需要加小括号,例如

1
2
3
4
5
a = 1;
print("a=$a") # a=1

b = 2;
print("$(a+b)") # 3

readline() 可以从控制台获取一行输入数据,得到的是字符串数据,例如

1
s = readline()

通常需要对输入数据进行类型转换,可以使用 parse() 函数,例如

1
2
s = 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
16
julia> @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 这个包提供支持。