Julia 学习笔记——7. 多维数组
数组是 Julia 最核心的主题之一。前面几篇虽然已经零散提过一些数组相关语法,但如果不专门单独整理一篇,很多地方还是会比较乱。
这一篇主要记录:
- Julia 的数组字面量和创建方式;
- 索引、切片、视图、形状变换;
- 常见的数组运算、拼接、遍历和广播;
- Julia 数组系统里最值得重点记住的设计点。
概述
Julia 的 Array 是按列优先(column-major)存储的,这一点和 MATLAB、Fortran 一致,和 Numpy 默认的行优先风格不同。
默认读者已经熟悉 Python/Numpy 或 MATLAB,因此这里不再解释数组的基础概念,直接看 Julia 数组系统里最容易影响实际写法的部分:一维向量和二维矩阵的区分、闭区间切片、默认拷贝而不是默认视图、广播和点语法。
此外 Julia 的数组有几个很鲜明的特点:
- 真正区分一维向量
Vector和二维矩阵Matrix; - 索引从 1 开始;
- 切片范围是闭区间;
- 普通切片默认会拷贝;
- 广播和点语法是数组代码的核心写法之一。
数组类型与维度
Array{T,N}
Julia 的一般数组类型可以写成 Array{T,N}:
T表示元素类型;N表示维度数。
例如
1 | Vector{Int64} == Array{Int64, 1} |
当然平时更多还是直接使用别名 Vector 和 Matrix。
查看数组类型和尺寸:
1 | A = [1 2 3; 4 5 6] |
创建数组
数组字面量
一维数组最简单:
1 | a = [1, 2, 3] |
二维数组字面量比较像 MATLAB,但又不完全一样:
1 | A = [1 2 3 |
这里有几个规则:
- 同一行元素之间用空格或
,分隔; - 不同行之间用换行或
;分隔; - 字面量本质上带有“拼接”的语义。
例如下面也可以:
1 | B = [1 2 3; 4 5 6] |
不过复杂数组字面量很容易变得难读,因此推荐:
- 简单矩阵可以直接写字面量;
- 稍复杂时优先用
reshape、hcat、vcat等显式函数。
指定元素类型
可以在字面量前显式指定元素类型:
1 | Float32[1, 2, 3] # Float32[1.0, 2.0, 3.0] |
如果字面量里元素类型不完全一致,Julia 会尝试自动提升到公共类型:
1 | [1, 2.0, 3] # Vector{Float64} |
zeros、ones、fill
最常见的初始化函数:
1 | zeros(3) # 长度 3 的 Float64 向量 |
注意 fill(x, dims...) 会把同一个值反复填入整个数组。如果 x 是可变对象,就要小心共享引用的问题。
例如
1 | A = fill([], 3) |
这和 Python 中的 [[]] * 3 是同类陷阱。
rand 与 similar
随机数组:
1 | rand(3) # 3 个 [0,1) 随机数 |
基于已有数组创建“同形状/相近类型”的新数组:
1 | A = rand(2, 3) |
similar 在写泛型数组代码时很常用。
推导式
Julia 支持和 Python 很像的推导式:
1 | [i^2 for i = 1:5] |
二维推导式:
1 | [(i, j) for i = 1:2, j = 1:3] |
这会直接生成一个二维数组,而不是 Python 那种“列表里套列表”的默认表现。
collect
很多范围、生成器、迭代器本身并不立刻变成普通数组,可以用 collect 显式收集:
1 | 1:5 # UnitRange |
特殊数组
Julia 标准库里已经提供了不少特殊矩阵类型,来自 LinearAlgebra:
1 | using LinearAlgebra |
这些类型不是普通 Matrix,但在很多线性代数运算中更高效,也更能表达语义。
例如
1 | Diagonal([1, 2, 3]) * [1, 1, 1] # [1, 2, 3] |
另外,范围对象本身也经常能充当“懒数组”:
1 | 1:10 |
很多时候没必要急着 collect。
索引与切片
基础索引
Julia 索引从 1 开始:
1 | a = [10, 20, 30] |
二维数组:
1 | A = [1 2 3; 4 5 6] |
范围切片
切片用范围对象:
1 | a = [10, 20, 30, 40, 50] |
注意这里是闭区间。
二维切片:
1 | A[:, 2] # 第 2 列 |
使用整数向量或布尔向量索引
Julia 支持高级索引,例如
1 | a[[1, 3, 5]] |
也支持布尔索引:
1 | a = [10, 20, 30, 40] |
这一点和 Numpy 比较接近。
CartesianIndex
对于多维数组,Julia 还提供了 CartesianIndex 来统一表示坐标:
1 | A = reshape(1:9, 3, 3) |
在写泛型多维代码时会比较有用。
切片默认拷贝与视图
默认是拷贝
这点非常重要:Julia 普通切片默认返回的是拷贝,而不是视图。
例如
1 | a = [1, 2, 3, 4] |
这和 Numpy 常见切片默认视图的习惯不同。
view 与 @views
如果需要视图,可以显式使用 view:
1 | a = [1, 2, 3, 4] |
对于一段代码中的多个切片,也可以使用 @views 宏:
1 | begin |
这样这些切片会尽量转成视图。
形状变换
reshape
reshape 可以改变数组视图的形状:
1 | a = collect(1:12) |
注意 Julia 按列优先填充:
1 | reshape(1:6, 2, 3) |
这和 Numpy 的默认阅读直觉很不一样,需要适应一下。
vec、dropdims、permutedims
常见形状操作:
1 | vec(A) # 拉平成向量 |
对于二维实数矩阵,转置最常见的是
1 | A' |
注意 ' 对复数数组是共轭转置;如果只想单纯交换维度而不共轭,更偏向用 transpose 或 permutedims。
拼接与重复
vcat、hcat、cat
Julia 的数组字面量其实本身就和拼接语法有关,因此也有对应的显式函数:
1 | vcat([1, 2], [3, 4]) # [1, 2, 3, 4] |
字面量和函数大致对应:
1 | [a; b] # 类似 vcat(a, b) |
repeat
重复数组:
1 | repeat([1, 2], 3) # [1, 2, 1, 2, 1, 2] |
数组遍历
eachindex
对于数组遍历,Julia 官方非常推荐 eachindex:
1 | for i in eachindex(a) |
相比直接写 1:length(a),eachindex 在更一般的数组类型上更稳妥,也更容易被优化。
axes
如果要按维度范围写循环,可以用 axes:
1 | for i in axes(A, 1), j in axes(A, 2) |
这里 axes(A, 1) 表示第 1 维允许的索引范围。
eachcol、eachrow、eachslice
按列、按行、按某个维度切片遍历:
1 | for col in eachcol(A) |
这些接口比自己手写 A[:, j] 更清晰,也更适合泛型代码。
广播与点语法
基础点调用
这是 Julia 数组代码里最有代表性的特性之一。
1 | [1, 2, 3] .^ 2 # [1, 4, 9] |
点语法表示逐元素调用,并自动进行广播。
广播规则
例如
1 | A = [1, 2, 3] |
标量会被自动广播到每个位置。涉及多个数组时,会先尝试协调尺寸。
点赋值
就地更新常见写法:
1 | A .= sin.(A) |
这在数值计算里非常重要,因为它避免了不必要的临时数组分配。
广播融合
像下面这种写法
1 | @. 2A^2 + sin(A) |
或者
1 | 2 .* A.^2 .+ sin.(A) |
通常会融合成一次广播循环,而不是多次单独遍历。
这也是 Julia 向量化写法有竞争力的重要原因。
映射、过滤和归约
map
1 | map(x -> x^2, [1, 2, 3]) # [1, 4, 9] |
map 更强调“对元素做函数变换”,而广播更强调“按数组形状规则逐元素对齐计算”。
很多一元简单操作,两者都能做,但语义侧重点略有不同。
filter
1 | filter(iseven, [1, 2, 3, 4, 5, 6]) # [2, 4, 6] |
sum、prod、maximum
1 | sum([1, 2, 3]) # 6 |
对矩阵按维度归约:
1 | sum(A; dims=1) # 按列求和 |
修改数组
常见的原地修改操作:
1 | push!(a, 10) |
这里带 ! 的约定和前面说的一样,表示函数会修改参数本身。
数组与线性代数
对于 Julia 来说,数组不仅是容器,还是线性代数运算的基本对象。
例如
1 | x = [1, 2, 3] |
可以直接写
1 | dot(x, y) |
其中矩阵乘法直接使用 *,不需要像 Python/Numpy 那样再专门区分出一个 @。
不过也要注意:
*是线性代数意义的乘法;.*才是逐元素乘法;^和.^也有类似区别。
小结
如果从 Python/Numpy 迁移到 Julia,多维数组这一篇最需要记住的大概是:
- Julia 的数组是列优先,索引从 1 开始;
Vector和Matrix是不同的维度概念,不要再拿“行向量/列向量都是二维矩阵”那套 MATLAB 历史包袱来理解;- 普通切片默认拷贝,视图要用
view; - 广播和点语法是 Julia 数值代码的主力写法;
- 原地更新时优先考虑
.=和各种!函数。
