Mojo 学习 —— 数据类型
文章目录
Mojo
中的所有值都有相关的数据类型,大多数类型都是由结构体定义的命名类型。之所以这么说,是因为类型由类型的名称而不是结构决定的
但也有一些类型没有定义为结构体:
- 函数的类型是由其签名决定的
NoneType
是一种只有一个实例(None
对象)的类型,用于表示 “没有值”
Mojo
自带的标准库提供了大量有用的类型和实用功能,每个标准库类型的定义都与用户定义的类型一样,甚至包括 Int
和 String
这样的基本类型。
最常见的类型是内置类型,这些类型无需导入就可使用,包括数值、字符串、布尔值等类型。
标准库还包含许多其他类型,可以根据需要导入,包括集合类型、与文件系统交互和获取系统信息的实用程序等。
数值类型
Mojo
最基本的数字类型是 Int
,它表示系统支持的最大大小的有符号整数,通常是 64
位或 32
位。
Mojo
还内置了各种精度的整数和浮点数值类型
所有数值类型都支持常用的数值运算符和位运算符,以及 math
模块提供的一些额外的数学函数
您可能想知道何时使用 Int
,何时使用其他整数类型。一般来说,在需要整数类型且不需要特定位宽时,Int
是安全的默认值。在 API
中使用 Int
作为默认整数类型,可以使应用程序接口更加一致和可预测。
浮点数
浮点类型表示的是实数。由于并非所有实数都能用有限位数表示,因此浮点数不能精确地表示每个值。
上表中列出的浮点类型 Float64
、Float32
和 Float16
中每种类型都包括一个符号位、一组表示指数的位和另一组表示分数或小数的位。
指数值为全一或全零的数字表示特殊值,允许浮点数表示无穷大、负无穷大、带符号的零和非数字 (NaN
)。有关数字表示方法的更多详情,请参阅维基百科上的 IEEE_754
关于浮点数值,有几点需要注意:
- 四舍五入错误。四舍五入可能会产生意想不到的结果。例如,
1/3
在这些浮点格式中无法精确表示。对浮点数执行的操作越多,四舍五入误差就越大。 - 连续数字之间的间距。在浮点数格式的范围内,连续数字之间的间距是可变的。对于接近零的数字,两个连续值之间的间距非常小;而对于较大的正数和负数,两个连续数字之间的间距大于
1
,因此可能无法表示连续的整数。
因为这些值是近似值,所以很少用相等运算符(==
)来比较它们。考虑下面的例子:
var big_num = 1.0e16
var bigger_num = big_num+1.0
print(big_num == bigger_num)
# True
比较运算符(<
、>=
等)适用于浮点数。您还可以使用 math.isclose
函数比较两个浮点数是否在指定公差范围内相等。
from math import isclose
print(isclose(big_num, bigger_num))
# True
数字字面值
除了这些数值类型外,标准库还提供了整数和浮点字面类型 IntLiteral
和 FloatLiteral
这些字面类型在编译时用于替换代码中出现的字面数字。一般情况下,您不应该自己实例化这些类型。
常用的数字字面值
在编译时,字面类型是任意精度(也称为无限精度)值,因此编译器在执行编译时不会出现溢出或舍入错误。
在运行时,这些值被转换为有限精度类型,即整数值转换为 Int
类型,浮点数值转换为 Float64
类型。(将只能在编译时存在的值转换为运行时值的过程称为实体化)。
下面的代码示例显示了任意精度计算与运行时使用 Float64
值进行的相同计算之间的差异,后者会出现舍入误差。
var arbitrary_precision = 3.0 * (4.0 / 3.0 - 1.0)
var three = 3.0
var finite_precision = three * (4.0 / three - 1.0)
print(arbitrary_precision, finite_precision)
输出结果是
1.0 0.99999999999999978
SIMD
为了支持高性能的数字处理,Mojo
使用 SIMD
类型作为其数字类型的基础,上面列举的所有数值类型都是长度为 1
的 SIMD
向量,例如 Int8 = SIMD[int8, 1]
SIMD
(单指令多数据)是一种处理器技术,它允许您一次对整个操作数集执行操作。Mojo
的 SIMD
类型抽象了 SIMD
操作。
一个 SIMD
值表示一个 SIMD
向量,也就是说,一个固定大小的值数组,可以放入处理器的寄存器中。
SIMD
向量由两个参数定义:
- 一个
DType
值,定义向量中的数据类型(例如,32
位浮点数)。 - 向量中元素的个数,必须是
2
的幂。
例如,您可以像这样定义一个包含四个 Float32
值的向量:
var vec = SIMD[DType.float32, 4](3.0, 2.0, 2.0, 1.0)
SIMD
值的数学运算按元素的方式应用于向量中的每个单独的元素。例如:
var vec1 = SIMD[DType.int8, 4](2, 3, 5, 7)
var vec2 = SIMD[DType.int8, 4](1, 2, 3, 4)
var product = vec1 * vec2
print(product)
输出为
[2, 6, 15, 28]
标量值
SIMD
模块定义了几个类型别名,它们是不同类型 SIMD
向量的简写。特别是,Scalar
类型只是一个具有单个元素的 SIMD
向量。表 1
中列出的数字类型,如 Int8
和 Float32
,实际上是不同类型标量值的类型别名:
alias Scalar = SIMD[size=1]
alias Int8 = Scalar[DType.int8]
alias Float32 = Scalar[DType.float32]
乍一看,这可能有点令人困惑,但这意味着无论您是在处理单个 Float32
值还是 Float32
值的向量,都要执行一样的数学运算
DType 类型
DType
结构体描述了 SIMD
向量可以保存的不同数据类型,并定义了许多用于操作这些数据类型的实用函数。
DType
结构体还定义了一组别名,作为不同数据类型的标识符,如 DType.int8
和 DType.float32
。在声明 SIMD
向量时使用这些别名
var v: SIMD[DType.float64, 16]
注意 DType.float64
不是一个类型,它是一个描述数据类型的值。不能创建 DType.float64
类型的变量。您可以创建类型为 SIMD[DType.float64, 1]
(或 Float64
)的变量。
from math.limit import max_finite, min_finite
def describeDType[dtype: DType]():
print(dtype, "is floating point:", dtype.is_floating_point())
print(dtype, "is integral:", dtype.is_integral())
print("Min/max finite values for", dtype)
print(min_finite[dtype](), max_finite[dtype]())
describeDType[DType.float32]()
输出为
float32 is floating point: True
float32 is integral: False
Min/max finite values for float32
-3.4028234663852886e+38 3.4028234663852886e+38
标准库中还有其他几种数据类型也使用 DType
抽象,如 bool
和 invalid
等
字符串
Mojo
的 String
类型表示可变字符串(不同于 Python
的不可变字符串)。
字符串支持各种操作符和常用方法。例如
var s: String = "Testing"
s += " Mojo strings"
print(s)
输出
Testing Mojo strings
大多数标准库类型都符合 Stringable
特性,它表示一种可以转换为字符串的类型。使用 String(value)
或 str(value)
可以明确地将数值转换为字符串
当使用 +
操作符将值连接到字符串时,Stringable
会隐式地将变量转换为 String
类型
var s = str("Items in list: ") + 5
print(s)
输出
Items in list: 5
字符串字面值
与数字类型一样,标准库也包含字符串字面类型,用于表示程序源代码中的字面字符串。字符串字面值用单引号或双引号括起来。
相邻的字面量会连接在一起,因此您可以使用多行字面量来定义一个长字符串,例如
var s = "A very long string which is "
"broken into two literals for legibility."
要定义多行字符串,也可以用三个单引号或双引号括起来
var s = """
Multi-line string literals let you
enter long blocks of text, including
newlines."""
请注意,三重双引号形式也可以用于编写 API
文档,类似 Python
与 IntLiteral
和 FloatLiteral
不同,StringLiteral
不会自动具体化为运行时类型。在某些情况下,您可能需要使用内置的 str
方法手动将 StringLiteral
值转换为 String
例如,在连接字符串和其他值时
var a = 5
var b: String = "String literals may not concatenate "
print(a + b)
如果缺少类型注释,上面的代码会报错
布尔值
Mojo
的 Bool
类型表示布尔值,可以使用 not
运算符否定布尔值
var conditionA = False
var conditionB: Bool
conditionB = not conditionA
print(conditionA, conditionB)
与 Python
中的用法基本类似,任何实现了 Boolable
特性的类型都有布尔表示法,即可以当做布尔值来用。
例如,如果集合包含任何元素,则为 True
;如果集合为空,则为 False
;如果字符串的长度不为零,则其为 True
集合类型
Mojo 标准库还有一组基本集合类型,可用于构建更复杂的数据结构,包括
List
,一个动态大小的数组Dict
:键值对关联数组,即字典Set
:包含唯一项目的无序集合Optional
:表示可能存在也可能不存在的值,即可选的值
集合类型是泛型,只能保存特定类型的值(如 Int
或 Float64
),可以使用编译时参数指定数据类型
例如,你可以这样创建一个包含 Int
值的 List
var l = List[Int](1, 2, 3, 4)
但并不是必须的,如果 Mojo
可以推断出数据类型,则可以省略编译时参数,例如,上面的代码等价于
var l = List(1, 2, 3, 4)
当然,你如果需要存储一组可变类型的值,可以使用 Variant
类型。
例如,Variant[Int32, Float64]
在任何时候都可以保存 Int32
或 Float64
值
from collections import List
from utils import Variant
alias IntOrString = Variant[Int, String]
fn main():
var list = List[IntOrString]()
list.append(IntOrString(1))
list.append(IntOrString(String('abc')))
@parameter
fn to_string(x: IntOrString) -> String:
if x.isa[String]():
return x.get[String]()[]
return str(x.get[Int]()[])
@unroll
for i in range(len(list)):
print(to_string(list[i]), end=' ')
@parameter
装饰器用于if
语句或嵌套函数中添加,以便在编译时将代码编译到程序中。@unroll
装饰器可在编译时展开循环。
输出结果
1 abc
List
List
是一个动态大小的元素数组,存储的元素需要符合 CollectionElement
特性,这意味着项目必须是可复制和可移动的。
大多数常见的标准库类型,如 Int
、String
和 SIMD
,都符合这一特性。使用方式如下
var l = List[String]()
List
类型包含部分 Python
list
的方法,例如
from collections import List
fn main():
var list = List(2, 3, 5)
list.append(7)
list.append(11)
print("Popping last item from list: ", list.pop())
for idx in range(len(list)):
print(list[idx], end=", ")
输出结果
Popping last item from list: 11
2, 3, 5, 7,
如果报错 'List[Int]' value has no attribute 'pop'
,可能需要更新到最新版本
目前使用 List
时有一些限制:
- 现在还不能使用字面值初始化一个
List
,只能用构造函数的方式
var list: List[Int] = [2, 3, 5]
- 目前还不支持使用
print
打印 List
迭代器返回的是值的引用,需要使用[]
来解引用
for elem in list:
print(elem[], end=", ")
Dict
Dict
类型是一个关联数组,用于保存键值对。需要指定键类型和值类型来创建 Dict
。例如:
var values = Dict[String, Float64]()
字典的键类型必须符合 KeyElement
特性,值元素必须符合 CollectionElement
特性
字典类型支持增删改查,Dict
迭代器都会产生引用,因此需要使用解引用操作符 []
,例如:
from collections import Dict
fn main():
var d = Dict[String, Float64]()
d["plasticity"] = 3.1
d["elasticity"] = 1.3
d["electricity"] = 9.7
for item in d.items():
print(item[].key, item[].value)
输出
plasticity 3.1000000000000001
elasticity 1.3
electricity 9.6999999999999993
Set
Set
类型表示一组唯一的值。集合的元素类型必须符合 KeyElement
特征
您可以从集合中添加和删除元素,测试集合中是否存在某个值,并执行集合操作,如两个集合之间的交集
from collections import Set
fn main():
var i_like = Set("sushi", "ice cream", "tacos", "pho")
var you_like = Set("burgers", "tacos", "salad", "ice cream")
var we_like = i_like.intersection(you_like)
print("We both like:")
for item in we_like:
print("-", item[])
Optional
Optional
表示一个可能存在也可能不存在的值。可以保存任何符合 CollectionElement
特性的类型
可以创建一个包含或不包含值的 Optional
类型,例如
from collections import Optional
var opt1 = Optional(5)
var opt2: Optional[Int] = 5
var opt3 = Optional[Int]()
var opt4: Optional[Int] = None
当一个 Optional
持有一个值时,它会被认为是 True
,否则为 False
如果 Optional
持有值时,可以使用 value
方法获取该值的引用。但是,在没有值的 Optional
上调用 value
会导致未定义的行为,所以在调用 value
方法前,要确定是否有值
var opt: Optional[String] = str("Testing")
if opt:
var value_ref = opt.value()
print(value_ref[])
或者,也可以使用 or_else
方法,如果有存储值,则返回该值,否则返回用户指定的默认值
var custom_greeting: Optional[String] = None
print(custom_greeting.or_else("Hello"))
custom_greeting = str("Hi")
print(custom_greeting.or_else("Hello"))
输出
Hello
Hi
AnyType 和 AnyRegType
在 Mojo
的 API
中,你还会看到 AnyType
和 AnyRegType
这两个引用。它们实际上是元类型,即类型的类型。
AnyType
:
表示任何 Mojo
类型,Mojo
将 AnyType
视为一种特殊的特性。
AnyRegType
:
是一个元类型,代表任何标记为寄存器可传递的 Mojo
类型。
你会在这样的函数签名中看到它们
fn any_type_function[ValueType: AnyRegType](value: ValueType):
...
您可以将其理解为 any_type_function
有一个参数,即 ValueType
类型的值,其中 ValueType
是在编译时确定的寄存器可传递类型
标准库中仍有一些这样的代码,但已逐渐迁移到更通用的代码中,不再区分寄存器可传递类型和纯内存类型
其他类型
在文档的很多地方,你可以看到 register-passable
、memory-only
以及 trivial
类型的说法。
寄存器和内存类型
寄存器可传递类型和仅内存类型是根据它们保存数据的方式来区分的:
- 寄存器可传递类型:
它完全由固定大小的数据类型组成,这些数据类型(理论上)可以存储在机器寄存器中。寄存器可传递类型可以包括其他类型,只要它们也是寄存器可传递类型。例如,Int
、Bool
和 SIMD
都是可寄存器传递类型。因此,一个寄存器可传递的结构体可以包含 Int
和 Bool
字段,但不能包含字符串字段。可以使用 @register_passable
装饰器声明寄存器可传递类型。
- 仅内存类型:
包括任何不符合寄存器可传递类型描述的类型。特别是这些类型通常使用指针或引用来管理堆分配的内存。例如,String
、List
和 Dict
都是仅内存类型的例子。
我们的长期目标是使这种区分对用户透明,并确保所有应用程序接口都能与寄存器可传递类型和仅内存类型一起使用。但现在,您会看到一些标准库类型只能与寄存器可传递类型一起使用,或者只能与仅内存类型一起使用。
trivial 类型
除了这两种类型,Mojo
还有 trivial
类型。从概念上讲,trivial
类型只是一种在其生命周期方法中不需要任何自定义逻辑的类型。
组成 trivial
类型实例的 bits
是可复制或可移动的,而不需要知道它们在做什么。
目前,trivial
类型是使用 @register_passable(trivial)
装饰器声明的。trivial
类型不应仅限于寄存器可传递类型,因此我们打算将来将 trivial
类型与 @register_passable
装饰器分开。