Julia 学习笔记——4. 函数基础
Julia 中的函数在表面上和 Python 比较接近,都是语言的一等公民:可以赋值给变量、作为参数传递,也可以作为返回值返回。
不过 Julia 的函数体系又远不只是把 def 换成 function 这么简单,语法会比 Python 严格得多。
函数返回值、默认参数、关键字参数、元组参数,以及后面会单独展开的多重分派,都是这个语言里非常核心的内容。
函数格式
Julia 中最普通的函数定义格式如下:
1 | function function_name(parameter_list) |
其中:
function和end用来包裹函数定义;- 参数列表写在小括号中;
- 返回值可以显式使用
return,也可以依赖最后一个表达式自动返回。
对于非常简单的函数,也可以使用单行赋值形式,后面会单独讨论。
简单示例
简单例子
最基本的例子如下
1 | function f(x,y) |
和 Python 一样,Julia 的函数不需要额外的“函数句柄”机制,函数对象本身就可以像普通值一样传递。
1 | function f(x,y) |
函数的赋值形式
Julia 支持以单行的赋值形式定义简单的函数
1 | f(x, y) = x + y; |
此时的函数体只能是一个单行表达式,这个表达式的结果自然就是函数的返回值。由于函数分派机制的存在,这种简单函数在 Julia 中实际上很常见。
return 语句
Julia 可以使用 return 语句返回值,但是和 Python 不同的是,Julia 在不写 return 时,会自动把最后一个表达式的结果作为函数返回值。
为了代码可读性,复杂函数里通常还是建议显式写出 return。
例如
1 | function update(x) |
如果希望函数不提供返回值,有以下几种等效写法(nothing 是 Nothing 类型的一个实例,相当于 Python 的 None)
1 | function hello1(x) |
参数和返回值类型
Julia 支持指定函数参数和返回值的类型,例如
1 | function h(x::Int64, y::Int64)::Int64 |
如果函数体中的实际返回值类型与函数声明中的返回值类型不匹配,Julia 会对其进行类型转换。
1 | function h2(x::Int64, y::Int64)::Int64 |
返回值类型在 Julia 中很少使用,Julia 可以自动推断返回值类型。
Julia 对函数参数则不会进行自动的类型转换。
1 | h(Int32(1), 2) # error |
Python 的参数类型只是作为参考,与之不同的是,Julia 的参数类型是强制性的,Julia 会根据参数类型选择对应的方法实现,这是后面多重分派机制的基础。
函数返回多值
与 Python 一样,Julia 可以通过返回元组的形式来返回多值,例如
1 | function test(a) |
在接收返回值时也可以自动对元组解包,例如
1 | r1, r2 = test(10); |
函数参数
传参行为
与 Python 类似,Julia 在函数传参过程中通常不会先把实参完整深拷贝一份再交给形参,往往只是浅拷贝,因此面临着类似的问题:如果参数是可变复合对象,那么在函数内部修改它,外部也会看到变化。
例如
1 | function test(a) |
对于执行过程中会修改参数的函数,在命名上约定使用 ! 结尾(注意 ! 是函数名称的一部分),例如
1 | function test!(a) |
一个典型例子是 sort 和 sort!:前者返回排序后的结果但不修改原数组,后者则直接原地修改输入数组。
对于名为 abc 的函数,通常可以偷懒实现为“先拷贝一份输入,再调用对应的 abc!”。
参数默认值
Julia 支持给参数提供默认值,例如
1 | function h(x, y = 1) |
显然提供默认值的参数要放在靠后的位置,此后不能有任何无默认值的参数出现,否则语法上存在歧义,会报错。
Python 的参数默认值只会在定义时计算一次,而 Julia 则会延迟到每一次调用时,因此多个参数的默认值允许相互依赖,例如
1 | function f(x, y = x+1, z = x+y) |
运行效果如下
1 | julia> f(3) |
在计算参数的默认值时,只有先前的参数才在作用域内,例如
1 | b = 10 |
这里 a = b 会使用外部作用域的变量 b,而不是后面的参数 b。
位置参数和关键字参数
Julia 支持位置参数和关键字参数,但是比 Python 更加严格:在函数声明时,出现在 ; 之前的视作位置参数,出现在 ; 之后的视作关键字参数
1 | function h(x, y; z) |
即使完全使用关键字参数,在定义时形参列表中的 ; 也不允许省略。
1 | function h2(; y, z) |
与 Python 不同,Julia 在函数调用时需要严格区分位置参数和关键字参数,不允许以键值对形式给位置参数赋值。
1 | h(x = 1, y = 2; z = 3) # ERROR |
在函数调用时的语法并没有那么严格,支持各种简写形式。
- 关键字参数前的
;有时可以使用,替代,而且参数顺序相对自由,例如关键字参数不要求在所有位置参数之后才出现。下面这些写法都是可行的
1 | h(1, 2, z = 3) # 6 |
- 分号后的关键字参数也可使用
key => value表达式,注意key要使用符号,例如
1 | h(1 ; :y => 2, :z => 3) # 6 |
- 分号后的关键字参数如果只是传递同名参数(例如
:z => z),可以直接简写为名称,例如
1 | y = 2; z = 3; |
如果关键字参数重复出现,会直接抛出异常,而不是用后者覆盖。
这部分语法虽然比较灵活,但实际书写时最好仍然使用最标准的形式:先给出所有位置参数,再给出关键字参数。
元组参数
Julia 支持在定义时把函数形参整体写成一个元组,在调用时同样也需要将参数打包为一个元组进行传递,Julia 会自动执行一次参解包运算,例如(注意这里有两层括号)
1 | function func((a, b)) |
这个机制可以极大简化函数复合使用时的写法,将上一个函数返回的元组直接提供给下一个函数,不需要额外的赋值过程,例如
1 | function test(a) |
不定位置参数
与 Python 类似,Julia 提供了不定位置参数的语法,自动将多余的位置参数使用元组打包(类似Python的 *args ),例如
1 | function f(x, y, args...) |
位置参数解包
与不定位置参数的语法直接对应,我们也可以在调用函数时,将列表/元组等解包为多个位置参数,依次传递给函数。
1 | function g(x, y, z) |
不定关键字参数
与不定位置参数类似,我们也可以使用具名元组来收集多余的关键字参数(Python的 **kwargs 则把多余的关键字参数收集为字典)
1 | function fn(; kwargs...) |
关键字参数解包
同样地,我们也可以在调用函数时,将具名元组或键类型为Symbol的字典解包为多个关键字参数,依次传递给函数。
例如将具名元组解包
1 | nt = (z = 3, y = 2, x = 1) |
将字典解包(注意字典的键的类型要求必须是Symbol,不能是String)
1 | function h(; x, y, z) |
注意这里的 ; 都不可以省略,否则 Julia 会尝试将其解包后作为位置参数传递。
匿名函数
基础
Julia 支持匿名函数,写法非常数学化,比其它各种语言中的 lambda 语句都更加自然,例如
1 | f1 = x -> x^2 + 1; |
涉及多个参数和无参数的匿名函数写法如下(也就是对参数列表必须使用小括号)
1 | f2 = (x,y) -> x + y; |
不建议使用下面这种无意义的匿名函数封装,可以直接用函数本身
1 | x -> f(x) |
匿名函数的主要用法是作为参数传递给其他函数,例如
1 | map(x -> 2*x, [1, 2, 3]) |
匿名函数可以在定义时立刻调用,例如
1 | (x -> 2*x)(3) # 6 |
do 语句
考虑到 map 等函数所接收的第一个参数可能是非常复杂的函数,此时使用匿名函数(即使加上begin ... end语句)的写法会非常难看
1 | map(x -> begin |
Julia 为此提供了一个专门的 do 语法,可以将函数需要的匿名函数参数的定义后置,实际运算中作为一个匿名函数传递给函数的第一个参数
1 | map([1, 2, 3]) do x |
do 语句可以用来实现类似 Python with 语句的效果,例如打开文件操作的代码块
1 | open("outfile", "w") do io |
可以有如下的 toy 实现
1 | function open(f::Function, args...) |
补充
操作符也是函数
Julia 的大多数操作符(除了需要短路求值的 && 和 ||)其实都是特殊的函数,可以使用函数的语法来调用
1 | 1+2+3 # 6 |
两者是完全等价的,实际上编译器会把前者翻译为后者。
还可以将操作符赋值给变量,但是它不支持中缀表达式
1 | f = + |
向量化函数的点语法
Julia 可以很方便地将函数调用变成向量化版本
1 | f.(A) |
涉及到多个参数时,会首先对其进行广播以协调数组的尺寸
1 | f.(args...) |
如果广播失败,会抛出错误。
需要注意的是:关键字参数不会被广播处理,而是原样保留,在逐个分量的函数调用中忠实传递,例如
1 | round.(X, digits=3) |
对于嵌套的函数调用,向量化调用会尝试将其融合到一个广播循环中,例如
1 | sin.(cos.(X)) |
实际只有一次循环,最终的结果相当于
1 | [sin(cos(x)) for x in X] |
如果融合失败,会抛出错误。
1 | sin.(sort(cos.(X))) # error |
.= 可以用来预分配运算结果,例如
1 | X .= sin.(Y) |
函数复合与链式调用
Julia 为函数的复合调用提供了一些语法糖。
第一个是完全遵循数学表示习惯的 \circ,例如
1 | f(x) = x * x; |
第二种则是管道风格的链式调用语法 |>,例如
1 | sqrt(sum(1:10)) # 7.416198487095663 |
将其与匿名函数结合可以实现更加丰富的功能,例如
1 | 1:3 .|> (x -> x^2) |> sum |> sqrt |
注意这里的 .|> 把 |> 与点操作进行了组合。
