Mojo 学习 —— 数据类型

Mojo 学习 —— 数据类型


Mojo 中的所有值都有相关的数据类型,大多数类型都是由结构体定义的命名类型。之所以这么说,是因为类型由类型的名称而不是结构决定的

但也有一些类型没有定义为结构体:

  • 函数的类型是由其签名决定的
  • NoneType 是一种只有一个实例(None 对象)的类型,用于表示 “没有值

Mojo 自带的标准库提供了大量有用的类型和实用功能,每个标准库类型的定义都与用户定义的类型一样,甚至包括 IntString 这样的基本类型。

最常见的类型是内置类型,这些类型无需导入就可使用,包括数值、字符串、布尔值等类型。

标准库还包含许多其他类型,可以根据需要导入,包括集合类型、与文件系统交互和获取系统信息的实用程序等。

数值类型

Mojo 最基本的数字类型是 Int,它表示系统支持的最大大小的有符号整数,通常是 64 位或 32 位。

Mojo 还内置了各种精度的整数和浮点数值类型

所有数值类型都支持常用的数值运算符和位运算符,以及 math 模块提供的一些额外的数学函数

您可能想知道何时使用 Int,何时使用其他整数类型。一般来说,在需要整数类型且不需要特定位宽时,Int 是安全的默认值。在 API 中使用 Int 作为默认整数类型,可以使应用程序接口更加一致和可预测。

浮点数

浮点类型表示的是实数。由于并非所有实数都能用有限位数表示,因此浮点数不能精确地表示每个值。

上表中列出的浮点类型 Float64Float32Float16 中每种类型都包括一个符号位、一组表示指数的位和另一组表示分数或小数的位。

指数值为全一或全零的数字表示特殊值,允许浮点数表示无穷大、负无穷大、带符号的零和非数字 (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

数字字面值

除了这些数值类型外,标准库还提供了整数和浮点字面类型 IntLiteralFloatLiteral

这些字面类型在编译时用于替换代码中出现的字面数字。一般情况下,您不应该自己实例化这些类型。

常用的数字字面值

在编译时,字面类型是任意精度(也称为无限精度)值,因此编译器在执行编译时不会出现溢出或舍入错误。

在运行时,这些值被转换为有限精度类型,即整数值转换为 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 类型作为其数字类型的基础,上面列举的所有数值类型都是长度为 1SIMD 向量,例如 Int8 = SIMD[int8, 1]

SIMD(单指令多数据)是一种处理器技术,它允许您一次对整个操作数集执行操作。MojoSIMD 类型抽象了 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 中列出的数字类型,如 Int8Float32,实际上是不同类型标量值的类型别名:

alias Scalar = SIMD[size=1]
alias Int8 = Scalar[DType.int8]
alias Float32 = Scalar[DType.float32]

乍一看,这可能有点令人困惑,但这意味着无论您是在处理单个 Float32 值还是 Float32 值的向量,都要执行一样的数学运算

DType 类型

DType 结构体描述了 SIMD 向量可以保存的不同数据类型,并定义了许多用于操作这些数据类型的实用函数。

DType 结构体还定义了一组别名,作为不同数据类型的标识符,如 DType.int8DType.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 抽象,如 boolinvalid

字符串

MojoString 类型表示可变字符串(不同于 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

IntLiteralFloatLiteral 不同,StringLiteral 不会自动具体化为运行时类型。在某些情况下,您可能需要使用内置的 str 方法手动将 StringLiteral 值转换为 String

例如,在连接字符串和其他值时

var a = 5
var b: String = "String literals may not concatenate "
print(a + b)

如果缺少类型注释,上面的代码会报错

布尔值

MojoBool 类型表示布尔值,可以使用 not 运算符否定布尔值

var conditionA = False
var conditionB: Bool
conditionB = not conditionA
print(conditionA, conditionB)

Python 中的用法基本类似,任何实现了 Boolable 特性的类型都有布尔表示法,即可以当做布尔值来用。

例如,如果集合包含任何元素,则为 True;如果集合为空,则为 False;如果字符串的长度不为零,则其为 True

集合类型

Mojo 标准库还有一组基本集合类型,可用于构建更复杂的数据结构,包括

  • List,一个动态大小的数组
  • Dict:键值对关联数组,即字典
  • Set:包含唯一项目的无序集合
  • Optional:表示可能存在也可能不存在的值,即可选的值

集合类型是泛型,只能保存特定类型的值(如 IntFloat64),可以使用编译时参数指定数据类型

例如,你可以这样创建一个包含 Int 值的 List

var l = List[Int](1, 2, 3, 4)

但并不是必须的,如果 Mojo 可以推断出数据类型,则可以省略编译时参数,例如,上面的代码等价于

var l = List(1, 2, 3, 4)

当然,你如果需要存储一组可变类型的值,可以使用 Variant 类型。

例如,Variant[Int32, Float64] 在任何时候都可以保存 Int32Float64

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 特性,这意味着项目必须是可复制和可移动的。

大多数常见的标准库类型,如 IntStringSIMD,都符合这一特性。使用方式如下

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

MojoAPI 中,你还会看到 AnyTypeAnyRegType 这两个引用。它们实际上是元类型,即类型的类型。

  • AnyType

表示任何 Mojo 类型,MojoAnyType 视为一种特殊的特性。

  • AnyRegType

是一个元类型,代表任何标记为寄存器可传递的 Mojo 类型。

你会在这样的函数签名中看到它们

fn any_type_function[ValueType: AnyRegType](value: ValueType):
    ...

您可以将其理解为 any_type_function 有一个参数,即 ValueType 类型的值,其中 ValueType 是在编译时确定的寄存器可传递类型

标准库中仍有一些这样的代码,但已逐渐迁移到更通用的代码中,不再区分寄存器可传递类型和纯内存类型

其他类型

在文档的很多地方,你可以看到 register-passablememory-only 以及 trivial 类型的说法。

寄存器和内存类型

寄存器可传递类型和仅内存类型是根据它们保存数据的方式来区分的:

  • 寄存器可传递类型:

它完全由固定大小的数据类型组成,这些数据类型(理论上)可以存储在机器寄存器中。寄存器可传递类型可以包括其他类型,只要它们也是寄存器可传递类型。例如,IntBoolSIMD 都是可寄存器传递类型。因此,一个寄存器可传递的结构体可以包含 IntBool 字段,但不能包含字符串字段。可以使用 @register_passable 装饰器声明寄存器可传递类型。

  • 仅内存类型:

包括任何不符合寄存器可传递类型描述的类型。特别是这些类型通常使用指针或引用来管理堆分配的内存。例如,StringListDict 都是仅内存类型的例子。

我们的长期目标是使这种区分对用户透明,并确保所有应用程序接口都能与寄存器可传递类型和仅内存类型一起使用。但现在,您会看到一些标准库类型只能与寄存器可传递类型一起使用,或者只能与仅内存类型一起使用。

trivial 类型

除了这两种类型,Mojo 还有 trivial 类型。从概念上讲,trivial 类型只是一种在其生命周期方法中不需要任何自定义逻辑的类型。

组成 trivial 类型实例的 bits 是可复制或可移动的,而不需要知道它们在做什么。

目前,trivial 类型是使用 @register_passable(trivial) 装饰器声明的。trivial 类型不应仅限于寄存器可传递类型,因此我们打算将来将 trivial 类型与 @register_passable 装饰器分开。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值