Julia 学习笔记——2. 字面量、基本数据类型与运算符
这一篇主要整理 Julia 中最常见的基本数据类型,以及与这些类型直接相关的运算符语义。
整数与浮点数
整数
Julia 支持固定长度的整数类型,包括有符号和无符号的版本,例如 Int8,Int32,UInt32 等。对于十进制整数字面量,在64位系统中默认使用 Int64 类型整数。可以使用 typeof() 查看字面量的类型(Python 对应的函数为 type())
1 | typeof(1) # Int64 |
类型也是可以作为参数进行运算的,例如使用 typemin() 和 typemax() 直接查看类型的最大最小值
1 | typemin(Int32) # -2147483648 |
注意:
- Julia 支持使用
_作为数字的分隔符,可以提高可读性,例如10_000。 - 由于整数的位数是固定的,在运算中自然也存在溢出问题,与其它语言中的处理类似,不再赘述。
- Python 的整数是无限精度的,Julia 也提供了
BigInt类型以支持无限精度。
对于非十进制的整数字面量,Julia 采用了不同的处理:默认将其视作无类型整数,并自动选择合适的位数,例如
1 | typeof(0x11) # UInt8 |
除此之外,对于非十进制的字面量还有很多细节处理的差异,谨慎使用。
布尔值
提供布尔类型的 true 和 false。
在底层实现时,Julia 采用 8 位整数存储的 0 和 1 分别代表 true 和 false。相应的类型转换为
1 | Bool(1) # true |
需要注意的是,Julia 不会把非零整数全部转换为 true,下面的语句报错
1 | Bool(2) # error |
在类型系统中,Bool 属于 Integer 的子类型
1 | julia> Bool <: Integer |
浮点数
Julia 支持基本的浮点数类型,包括默认使用的双精度浮点数 Float64,单精度浮点数 Float32 和半精度浮点数 Float16。
Julia 在科学记数法上有一个比较值得注意的细节:使用 e 对应 Float64 字面量,使用 f 则对应 Float32 字面量。
1 | typeof(1.1e-3) # Float64 |
可以使用类型名将整数转换为浮点数,例如
1 | Float64(1) # 1.0 |
Julia 为不同浮点数类型分别提供了对应的 NaN 和 Inf 常量:
- Float64:
NaN和Inf - Float32:
NaN32和Inf32 - Float16:
NaN16和Inf16
关于这些特殊值的运算都符合 IEEE 标准,因此不同语言之间并没有什么区别。
Julia 的 eps 和 MATLAB 类似,作为一个函数,可以传入一个浮点数或者浮点数类型:
- 如果传入的是浮点数,那么返回的就是它附近的浮点数间隙;
- 如果传入的是浮点数类型,返回的是
1.0附近的浮点数间隙; - 缺省时相当于
eps(Float64)。
1 | eps(1000.0) # 1.1368683772161603e-13 |
Julia 提供了 zero 和 one 函数来创建指定类型的 0 和 1,可以传入类型或者一个数,在某些情况下可以避免不必要的类型转换。
例如
1 | zero(1.0) # 0.0 # Float64 |
在整数和浮点数混合运算时,Julia 会自动提升到合适的浮点类型进行处理,这部分行为相对直接。
1 | 1 + 0.2 # 1.2 |
复数与有理数
复数
Julia 支持复数,使用全局常量 im 表示虚数单位,例如
1 | 1 + 2im |
例如
1 | typeof(1+im) # Complex{Int64} |
这表明复数并不是简单的一个类型,实际类型与复数的实部虚部所属的类型有关。
在数学中,实数是复数的一个子类,但是在编程实现中很难如此实现,通常实数(包括浮点数和整数)是单独的一套类型体系,复数则是一个几乎与此无关的类型。
涉及到 im 的字面量需要特别注意运算优先级的差异,例如
1 | 3/4*im # 0.0 + 0.75im |
更建议的做法是使用 complex 函数来创建复数,例如
1 | complex(1,2) # 1 + 2im |
注意:Julia 严格区分字面量是否是复数类型,例如 -1 和 -1+0im 是完全不同的
1 | -1 == -1+0im # true |
在使用 sqrt() 时的差异非常明显
1 | sqrt(-1) # error |
有理数
Julia 支持有理数类型,可以通过 // 运算符创建,例如
1 | 2//3 |
注意:
- Julia 会自动对分数进行化简,并且保证分母非负。
- Julia 允许分母为0,例如
5//0,但是不允许分子分母同时为 0。 - Julia 不支持涉及浮点数的
//运算
字符与字符串
与 Python 不同,Julia 使用单引号 ' 表示字符,使用双引号 " 来表示字符串,两者是不同的类型,不可以混用。例如
1 | typeof('a') # Char |
字符的底层当然是基于 ASCII 存储的,因此也支持与数值的运算,例如
1 | 'a' + 1 # 'b' |
Julia 支持多行字符串和 raw 标记的原始字符串(禁用 \ 转义,适合用于 Windows 路径),例如
1 | text = """ |
Julia 有如下两种方式拼接字符串,注意使用 * 而不是 + 进行拼接!
1 | "Hello, " * "world!" # "Hello, world!" |
使用 ^ 表示字符串的重复
1 | "abc"^4 # "abcabcabcabc" |
与 Python 的 f-string 类似,在字符串中可以使用 $ 插入变量或表达式,有歧义时可以加括号,例如
1 | name = "Alice" |
Julia 的字符串支持基于字典序的大小比较。
Julia 甚至为版本号提供了一个专门的字符串字面量:版本号字面量,使用 v 作为前缀,例如 VERSION 是一个特殊的常数,代表 Julia 当前版本,可以用下面的语句进行特定版本的处理
1 | if v"0.2" <= VERSION < v"0.3-" |
有很多关于字符串的操作,包括子串的搜索和提取,正则匹配等,但是目前不太需要,因此略去。
nothing、missing 与特殊值
这几个概念在 Julia 里很容易混在一起,但它们的语义其实完全不同。
nothing
nothing 是 Nothing 类型的唯一实例,常见作用包括:
- 表示“这里没有值”
- 表示函数没有有意义的返回结果
- 作为某些可选参数或状态的占位值
例如
1 | typeof(nothing) # Nothing |
它最接近 Python 里的 None,但并不等同于空字符串、空数组或者 false。
missing
missing 主要表示“值缺失但概念上本来应该有一个值”,这一点在数据分析和表格数据处理中非常常见。
例如
1 | typeof(missing) # Missing |
与 nothing 最大的区别在于:missing 会参与缺失传播。
1 | 1 + missing # missing |
这里并不是返回 true 或 false,而是继续返回 missing,表示“结果也无法判断”。
missing 的判断和处理
不能使用 == 来判断一个值是否为 missing,更常见的做法是使用 ismissing():
1 | ismissing(missing) # true |
对于包含缺失值的数据,常见做法是使用 skipmissing():
1 | a = [1, missing, 3, 4] |
NaN
NaN 不是“没有值”,也不是“缺失值”,而是浮点数运算中的非法结果,例如 0.0 / 0.0。
1 | 0.0 / 0.0 # NaN |
NaN 仍然是一个浮点数,只是它代表“不是一个有效数值结果”。
和 IEEE 浮点标准一致:
1 | NaN == NaN # false |
因此:
nothing更偏程序语义;missing更偏数据缺失语义;NaN更偏数值计算错误结果。
这三者不能混用。
运算符
基本运算符
基本运算符和其它语言大体相同,只有几个地方需要特别注意:
a*b:乘法a/b:除法a\b:反向除法,a\b等价于b/aa^b:幂运算a%b:取余a ÷ b:整除==:相等,这和isequal函数不太一样!=: 不等===:严格比较二进制数据相等
注意:
- 涉及到
NaN和Inf的等号和不等号判断可能违反直觉,例如NaN != NaN,但是这也是遵循浮点数标准的。 - 部分运算符对应的 Unicode(LaTeX) 字符也可以正常使用,例如不等号也支持
\leq ===、==和isequal的语义存在细微区别,尤其在某些边缘例子中,一般来说,===最严格,isequal最弱。
与 Python 一样,Julia 支持链式比较,例如
1 | 1 < 2 <= 2 < 3 == 3 > 2 >= 1 == 1 < 3 != 5 # true |
Julia 支持 += 等复合赋值运算符,例如 a += b 等价于 a = a + b,这个过程中可能改变 a 的类型,例如
1 | a=0; typeof(a) # Int64 |
和 Python 一样,这类运算符的优先级很低。
Numpy 的
+=等运算符是就地更新的,而 Julia 并不存在这样的区别,a += b始终等价于a = a + b。
! 是逻辑非,&& 是逻辑与,|| 是逻辑或,并且 && 和 || 都是短路运算,例如
1 | true && false # false |
~,& 和 | 是按位运算符,例如
1 | ~0 # -1 |
点运算符
Julia 为每一个二元运算符提供了点运算符的变体,例如
1 | [1, 2, 3] ^ 2 # error |
但是加上 . 就可以正常执行
1 | [1, 2, 3] .^ 2 # [1, 4, 9] |
. 的含义为对逐个元素进行运算。
实际上,a .^ b 会被处理为 (^).(a,b),首先对两个输入进行广播操作,使得不同的尺寸相匹配,然后进行逐个元素运算。
除了二元运算符,函数调用也可以变成点运算版本,例如 f.(A)。
在一个复杂的表达式中,. 运算会自动融合,例如
1 | 2 .* A.^2 .+ sin.(A) |
这里涉及到的点运算会合并到一起,实际只会执行一次对 A 的循环遍历。
如果需要对一个复杂表达式的各个运算都改成点运算版本,也可以直接使用 @. 这个宏,例如
1 | @. 2A^2 + sin(A)) |
在函数的复合调用中,点运算也会自动融合,例如
1 | f.(g.(A)) |
复合赋值运算符也支持点运算的版本,例如
1 | A .+= B |
将点运算符用于数值字面量可能会导致歧义。例如 1.+x 既可以表示 1. + x,也可以表示 1 .+ x?
遇到这种存在歧义的情况时,Julia 会直接报错,可以用空格消除歧义。
1 | x = [1,2,3] |
补充
类型转换
可以直接使用 T(x) 把 x 转换为类型 T,例如
1 | Int32(10) |
对于基本数据类型,这通常是调用 convert() 函数实现的,例如
1 | convert(Int32, 10) |
类型提升
在不同类型的数据进行混合数值运算时,通常会将其提升为统一的数据类型,例如
1 | 1 + 0.3 # 1.3 |
可以用 promote() 函数探究类型提升的细节,例如
1 | promote(1,0.3) # (1.0, 0.3) |
对于自定义的类型,也支持给 Julia 提供对应的类型提升规则,例如
1 | promote_rule(::Type{Float64}, ::Type{Float32}) = Float64 |
数值字面量系数
这是一种主要面向数学书写习惯的语法糖,但也确实给代码可读性和语法解析带来了额外负担。
Julia 允许变量直接跟在一个数值字面量后,暗指乘法,例如
1 | x = 3 |
数值字面量系数的优先级跟一元运算符相同,比如取相反数,例如:
2^3x会被解析成2^(3x)2x^3会被解析成2*(x^3)
括号表达式在某些情况下可以被用作变量的系数,暗指表达式与变量相乘,例如
1 | x = 3 |
但是这玩意其实很鸡肋,因为下面的表达式都是语法错误:
1 | x = 3 |
而且这里还可能和其它语法产生冲突,例如函数调用,十六进制的前缀0x,用于浮点数表示的f和e,以及虚数单位im等。
总的来说,字面量系数这种语法糖了解即可,实际代码中并不建议主动使用。
