Mojo 学习 —— 函数
之前的介绍中提到,
Mojo
可以使用
fn
和
def
来定义函数,但它们有不同的默认行为
def
和 fn
都很好用,并不存在优劣之分。至于使用哪种风格最适合特定任务,则取决于个人喜好。
定义在结构体中的函数称为方法,功能和函数是一样的,只是不同的叫法
def 函数
def
可以像在 Python
中那样使用,具有相同的动态性和灵活性。例如,下面这个函数在 Python
和 Mojo
中作用相同
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=5
和 Type=Int
调用,然后以 value=11.7
和 Type=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_string
。VariadicPack
是空的,因此 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)
在什么情况下要编写只包含位置参数的函数呢
- 参数名称对调用者没有意义。
- 您希望以后可以自由更改参数名称,而不会破坏向后的兼容性
例如,在 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)
只有关键字的参数通常有默认值,但这不是必需的。如果仅关键字参数没有设置默认值,则它是一个必需的关键字参数,必须指定,并且必须通过关键字的方式来指定参数值
仅关键字参数中,任何必需的关键字参数必须在任何可选的关键字参数之前。
参数的顺序遵循以下规则
- 必需的位置参数
- 可选的位置参数
- 可变参数
- 必需的关键字参数
- 可选的关键字参数
- 可变关键字参数。
函数重载
如果使用 def
定义 Python
形式的没有指定参数类型的函数,则该函数可以接受任何类型的数据,也就是说,它是一个泛型函数,不需要重载
而所有 fn
定义的函数都必须指定参数类型,因此,如果希望一个函数能处理不同的数据类型,就需要实现不同版本的函数,并且每个版本指定不同的参数类型。这就是所谓的 “函数重载”
例如,下面的 add
函数,可以接受 Int
、String
类型或可以隐式转换为这两种类型的数据
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
版本作为后备。
关于对编译时参数的重载,会在后面说明