Mojo 学习 —— 函数

Mojo 学习 —— 函数


之前的介绍中提到, Mojo 可以使用 fndef 来定义函数,但它们有不同的默认行为

deffn 都很好用,并不存在优劣之分。至于使用哪种风格最适合特定任务,则取决于个人喜好。

定义在结构体中的函数称为方法,功能和函数是一样的,只是不同的叫法

def 函数

def 可以像在 Python 中那样使用,具有相同的动态性和灵活性。例如,下面这个函数在 PythonMojo 中作用相同

def greet(name):
    greeting = "Hello, " + name + "!"
    return greeting

Mojo 中,你还可以为参数指定类型以及返回值类型,可以用 var 声明带或不带显式类型的变量

def greet(name: String) -> String:
    var greeting = "Hello, " + name + "!"
    return greeting

def 函数的关键点在于:

  • 不需要声明参数的类型,所有未声明参数都解析为 object
  • 不需要声明返回值的类型,默认也是 object
  • 参数是可变的,如果参数是 object 类型,传递的是引用,其他类型都是传递值(owned
  • 内部变量不需要用 var 声明

object 类型

def 中未声明类型的参数或返回值,它们会变成 object 类型,它与标准库中的其他类型不一样

object 类型是动态类型,因为它实际上可以表示 Mojo 标准库中的任何类型,它所代表的实际类型是在程序运行时推断出来的

虽然它提供的动态类型带来了更多的灵活性,与 Python 的兼容性更好。但是,缺少类型信息容易在程序运行时引发错误

因此,在使用 object 类型和其他强类型值时要小心,它们的行为可能会不一致,因为 object 是标准库中唯一不符合完整值语义的类型

fn 函数

fn 函数提供了严格的类型检查和额外的内存安全性。它迫使你在 def 中编写原本可以省略内容,并确保你不会意外地更改接收到的参数。

例如,使用 fn 的定义上面的函数

fn greet(name: String) -> String:
    var greeting = "Hello, " + name + "!"
    return greeting

def 函数相比,fn 函数在内部更严格,必须加上类型注释

fn 函数的关键点在于:

  • 参数必须指定类型(结构体方法中的 self 参数除外)
  • 返回值必须指定类型,除非不返回值。如果不指定返回类型,则默认为 None(即没有返回值)
  • 默认情况下,参数传递的是不可变引用(值为只读,即 borrowed,也可以使用 inout 声明可变引用或使用 owned 声明传递值)
  • 必须使用 var 关键字声明变量
  • 如果函数内部会引发异常,则必须使用 raises 关键字明确声明,而 def 函数不需要声明异常

通过强制添加这些类型检查,有助于避免各种运行时错误。

def 函数中的动态类型相比,它还能提高性能,无需在运行时花费额外的开销来确定要使用的数据类型,因为类型在编译时就已经确定了

函数参数

Mojo 的函数参数使用基本与 Python 一样,运行可选参数、可变参数和关键字参数等

可选参数

可选参数是指包含默认值的参数,例如

fn pow(base: Int, exp: Int = 2) -> Int:
    return base ** exp

fn fn():
    var z = pow(3)
    print(z)

exp 是可选参数,默认为 2

但是,不能为声明为 inout 的参数定义默认值,且可选参数要在必须参数之后

必须参数

关键字参数的指定格式为 argument_name = argument_value。您可以按任意顺序传递关键字参数,和 Python 是一样的

fn pow(base: Int, exp: Int = 2) -> Int:
    return base ** exp

fn main():
    var z = pow(exp=3, base=2)
    print(z)

可变参数

可变参数允许函数接受可变数量的参数,使用变量参数语法 *argument_name

fn sum(*values: Int) -> Int

可变参数在必须参数之后,关键字参数之前,和 Python 一样

根据参数类型的不同,可变参数可分为两类:

  • 同质可变参数,即所有传递的参数类型相同,例如全部为 Int 或全部为 String
  • 异质可变参数,可接受一组不同类型的参数
同质可变参数

同质可变参数声明的形式为 *argument_name: argument_type,上面的求和函数就是一个例子

在函数体内部,可变参数可以用迭代列表的形式访问。目前,根据参数是寄存器可传递类型(如 Int)还是仅内存类型(如 String),在处理时存在一些差异

寄存器可传递类型:可将其作为 VariadicList 类型,用 for..in 可以循环遍历这些值

fn sum(*values: Int) -> Int:
  var sum: Int = 0
  for value in values:
    sum = sum+value
  return sum
  
fn main():
  var total = sum(1, 2, 3, 4)
  print('total is', total)
// total is 10

内存类型:可将其作为 VariadicListMem 类型,用 for..in 循环直接遍历该列表会为每个值生成一个引用,而不是值本身。所以必须在添加一个 [] 操作符,来解引用并获取这些值。例如

def make_worldly(borrowed *strs: String):
    for i in strs:
        print(i[])

fn main() raises:
    make_worldly("hello", "world", "mojo")
// hello
// world
// mojo

或者,使用下标索引的形式访问,则不需要解引用。例如

fn make_worldly(*strs: String):
    for i in range(len(strs)):
        print(strs[i] + " world")

fn main():
    make_worldly("hello", "Hi")
// hello world
// Hi world
异质可变参数

异构可变参数要比同构可变参数复杂一些,需要用到特性和 parameter。这种语法看起来可能会显得有些陌生。带有异质可变参数的函数形式如下

def count_many_things[*ArgTypes: Intable](*args: *ArgTypes):
    ...

[*ArgTypes: Intable] 指明函数的编译时参数列表是 ArgTypes,这是一个类型列表,每种类型都符合 Intable 特性。

运行时参数列表 (*args: *ArgTypes) 具有我们熟悉的 *args 变量参数,但其类型不是单一类型,而是定义为 *ArgTypes 的类型列表

这意味着 args 中的每个参数在 ArgTypes 中都有对应的类型,因此 args[n] 属于 ArgTypes[n] 类型。

在函数内部,args 是一个 VariadicPack 类型,处理参数的最简单方法是使用 each() 方法传入一个函数来遍历

看一个具体的例子

fn count_many_things[*ArgTypes: Intable](*args: *ArgTypes) -> Int:
    var total = 0

    @parameter
    fn add[Type: Intable](value: Type):
        total += int(value)

    args.each[add]()
    return total

print('total is', count_many_things(5, 11.7, 12))
// total is 28

在上面的示例中,add() 函数依次被应用于每个 args 中的参数,然后执行加法。例如,add 首先以 value=5Type=Int 调用,然后以 value=11.7Type=Float64 调用。

请注意在调用 count_many_things 时,实际上并不需要传入参数类型列表,只需传入参数,Mojo 会自行生成 ArgTypes 列表

作为一个小的优化,如果你的函数会经常以单一参数调用时,你可以用一个单一参数和一个可变参数来定义你的函数。这样,在简单情况下就可以绕过 VariadicPack 的填充和遍历。

例如,如果 print_string 函数打印的是单个字符串,那么就可以用这样的代码重新实现变量 print 函数:

fn print_string(s: String):
    print(s, end="")

fn print_many[T: Stringable, *Ts: Stringable](first: T, *rest: *Ts):
    print_string(str(first))

    @parameter
    fn print_elt[T: Stringable](a: T):
        print_string(" ")
        print_string(a)
    rest.each[print_elt]()

fn main():
    print_many("Bob")

如果使用单参数调用 print_many,则会直接调用 print_stringVariadicPack 是空的,因此 each 会立即返回,而不会调用 print_elt 函数。

可变关键字参数

Mojo 函数还支持可变关键字参数(**kwargs)。可变关键字参数允许用户传递任意数量的关键字参数。其语法为 **kw_argument_name

fn print_nicely(**kwargs: Int) raises:
    for key in kwargs.keys():
        print(key[], "=", kwargs[key[]])

fn main() raises:
    print_nicely(a=7, y=8)
// a = 7
// y = 8

在这个例子中,参数名称 kwargs 是一个占位符,可接受任意数量的关键字参数。

在函数的主体中,可以以关键字和参数值字典的形式访问参数(例如 OwnedKwargsDict 的实例)

但是目前这种方法还是有一些局限性

  • 可变关键字参数总是被隐式地视为使用 owned 声明的参数,不能以其他方式声明
  • 所有可变关键字参数必须具有相同的类型。例如,如果参数是 **kwargs:Float64 类型,那么参数字典将是 OwnedKwargsDict[Float64] 类型
  • 参数类型必须符合 CollectionElement 特性,也就是说,该类型必须同时是可移动和可复制的
  • 目前还不支持字典解包,例如
fn takes_dict(d: Dict[String, Int]):
  print_nicely(**d)
  • 暂不支持可变的编译时关键字,例如
fn var_kwparams[**kwparams: Int]()

仅位置参数

定义函数时,可以限制某些参数,使其只能作为位置参数传递,或只能作为关键字参数传递。

要定义位置参数,请在参数列表中添加斜线字符 (/)。在 / 之前的任何参数都是位置参数,不能作为关键字参数传递。例如

fn min(a: Int, b: Int, /) -> Int:
    return a if a < b else b

这个 min 函数可以用 min(1, 2) 调用,但不能用关键字调用,如 min(a=1,b=2)

在什么情况下要编写只包含位置参数的函数呢

  1. 参数名称对调用者没有意义。
  2. 您希望以后可以自由更改参数名称,而不会破坏向后的兼容性

例如,在 min 函数中,参数名不会增加任何实际信息,也没有理由用关键字指定参数。

仅关键字参数

仅关键字参数与仅位置参数相反,它们只能通过关键字指定。如果函数接受可变参数,那么在可变参数之后定义的任何参数都将被视为只包含关键字的参数。例如

fn sort(*values: Float64, ascending: Bool = True): ...

在这个例子中,用户可以传递任意数量的 Float64 值,最后有一个可选的关键字参数用来判断是否升序

var a = sort(1.1, 6.5, 4.3, ascending=False)

如果函数不接受可变参数,你可以在参数列表中添加一个星号(*)来分隔只有关键字的参数:

fn kw_only_args(a1: Int, a2: Int, *, double: Bool) -> Int:
    var product = a1 * a2
    if double:
        return product * 2
    else:
        return product

其中,double 必须以关键字参数的方式传入值 var res = kw_only_args(7, 8, double=0.1)

只有关键字的参数通常有默认值,但这不是必需的。如果仅关键字参数没有设置默认值,则它是一个必需的关键字参数,必须指定,并且必须通过关键字的方式来指定参数值

仅关键字参数中,任何必需的关键字参数必须在任何可选的关键字参数之前。

参数的顺序遵循以下规则

  1. 必需的位置参数
  2. 可选的位置参数
  3. 可变参数
  4. 必需的关键字参数
  5. 可选的关键字参数
  6. 可变关键字参数。

函数重载

如果使用 def 定义 Python 形式的没有指定参数类型的函数,则该函数可以接受任何类型的数据,也就是说,它是一个泛型函数,不需要重载

而所有 fn 定义的函数都必须指定参数类型,因此,如果希望一个函数能处理不同的数据类型,就需要实现不同版本的函数,并且每个版本指定不同的参数类型。这就是所谓的 “函数重载

例如,下面的 add 函数,可以接受 IntString 类型或可以隐式转换为这两种类型的数据

fn add(x: Int, y: Int) -> Int:
    return x + y

fn add(x: String, y: String) -> String:
    return x + y

传入其他的类型,编译器会出错。

例如,String 包含一个重载版本的构造函数 (__init__),可以接受一个 StringLiteral 值。因此,您也可以将一个 StringLiteral 传递给一个期望输入值为 String 的函数

在解决重载函数调用时,Mojo 编译器会尝试每个候选函数,并使用能正常工作的那个(如果只有一个版本能正常工作),或者选择最接近的匹配函数(如果它能确定一个接近的匹配函数),或者报告该调用是模棱两可的(如果它无法确定选择哪个)

如果编译器无法确定使用哪个函数,可以通过显式地将值转换为支持的参数类型来解决模糊问题。

例如,在下面的代码中,我们想调用重载的 foo 函数,但两种实现都接受支持从 StringLiteral 隐式转换的参数。因此,对 foo(string) 的调用是模棱两可的,会产生编译器错误。

我们可以通过将值转换为我们真正想要的类型(foo(MyString(string)))来解决这个问题:

@value
struct MyString:
    fn __init__(inout self, string: StringLiteral):
        pass

fn foo(name: String):
    print("String")

fn foo(name: MyString):
    print("MyString")

fn call_foo():
    var string = "Hello"
    foo(MyString(string))

在解析重载函数时,Mojo 不会考虑返回类型或调用时的其他上下文信息,只有参数类型会影响函数的选择。

例如,您可以定义多个 fn 函数重载,然后定义一个或多个不指定所有参数类型的 def 版本作为后备。

关于对编译时参数的重载,会在后面说明

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

名本无名

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

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

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

打赏作者

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

抵扣说明:

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

余额充值