Julia 学习笔记——4. 函数基础
Julia 的函数与 Python 很类似,最大的区别可能就是关键字
def 换成
function,除此以外都是一些细节差异。
基础
简单例子
最基本的例子如下 1
2
3
4
5function f(x,y)
x + y
end
f(1,2) # 3
和 Python 一样,Julia 的函数不需要额外的类似函数句柄的机制,函数对象可以和普通的值一样被传递。
1 | function f(x,y) |
函数的赋值形式
Julia 支持以单行的赋值形式定义函数 1
f(x, y) = x + y;
此时的函数体只能是一个单行表达式,这个表达式的结果自然就是函数的返回值。由于函数分派机制的存在,这种简单函数在 Julia 中实际上很常见。
return 语句
Julia 可以使用 return 语句返回值,但是和 Python
不同的是,Julia 在不使用 return
语句时,自动将最后一个表达式的结果作为函数返回值。为了代码可读性,还是不建议省略
return。
例如 1
2
3
4
5
6
7
8function update(x)
println("x = $x");
x + 1 # return x + 1
end
y = update(1)
# x = 1
# 2
如果希望函数不提供返回值,有以下几种等效写法(nothing
相当于 Python 的 None) 1
2
3
4
5
6
7
8
9
10
11
12
13
14function hello1(x)
println("Hello, $x");
return nothing
end
function hello2(x)
println("Hello, $x");
return
end
function hello3(x)
println("Hello, $x");
nothing
end
返回值类型
Julia
支持指定函数的参数和返回值的类型,关于参数的类型比较复杂,这里暂时不作讨论,我们关注返回值的类型
1
2
3
4
5function h(x, y)::Int64
x + y
end
h(1, 2.0) # 3
如果函数体中的实际返回值类型与函数声明中的返回值类型不匹配,Julia 会对其进行类型转换。(但是对函数参数则不会进行自动的类型转换)
函数返回多值
与 Python 一样,Julia 可以通过返回元组的形式来返回多值,例如
1
2
3
4
5function test(a)
return (a - 1, a + 1)
end
result = test(10) # (9, 11)
在接收返回值时也可以自动对元组解包,例如 1
2
3r1, r2 = test(10);
# r1 = 9
# r2 = 11
函数参数
传参行为
与 Python 类似,Julia 在函数传参过程中通常不会发生实参到形参的拷贝,而是引用的传递,因此面临和 Python 一样的问题:直接修改函数参数时,如果是复合类型,修改可能会影响到外部。
例如 1
2
3
4
5
6
7
8
9
10
11
12function test(a)
a[1] = 100
return
end
x = [1, 2, 3]
test(x)
x
# 3-element Vector{Int64}:
# 100
# 2
# 3
对于执行过程中会修改参数的函数,在命名上约定使用 !
结尾(! 是函数名称的一部分),例如 1
2
3
4function test!(a)
a[1] = 100
return
end
一个典型的例子是sort和sort!,这是两个不同的函数,前者的排序不会改变参数,后者在排序的同时会就地改变参数。
很多名为abc的函数的实现直接依赖对应的abc!函数:先对参数进行显式拷贝,然后调用abc!,最后返回之前拷贝的副本。
参数默认值
Julia 支持给参数提供默认值,例如 1
2
3
4
5
6function h(x, y = 1)
return x + y
end
h(1) # 2
h(1, 2) # 3
显然提供默认值的参数要放在靠后的位置,此后不能有任何无默认值的参数出现,否则语法上存在歧义,会报错。
Python 的参数默认值只会在定义时计算一次,而 Julia
则会延迟到每一次调用时,因此多个参数的默认值允许相互依赖,例如
1
2
3
4
5function f(x, y = x+1, z = x+y)
println("x=", x)
println("y=", y)
println("z=", z)
end
运行效果如下 1
2
3
4julia> f(3)
x=3
y=4
z=7
位置参数和关键字参数
Julia 支持位置参数和关键字参数,但是比 Python
更加严格:在函数声明时,出现在 ; 之前的视作位置参数,出现在
; 之后的视作关键字参数 1
2
3
4
5function h(x, y; z)
return x + y + z
end
h(1, 2; z = 3) # 6
即使完全使用关键字参数,在定义时形参列表中的 ;
也不允许省略。 1
2
3function h2(; y, z)
return y + z
end
与 Python 不同,Julia
在函数调用时需要严格区分位置参数和关键字参数,不允许以键值对形式给位置参数赋值。
1
h(x = 1, y = 2; z = 3) # ERROR
在函数调用时的语法并没有那么严格,支持各种简写形式。
关键字参数前的
;有时可以使用,替代,而且参数顺序相对自由,例如关键字参数不要求在所有位置参数之后才出现。下面这些写法都是可行的1
2h(1, 2, z = 3) # 6
h(1, z = 3, 2) # 6分号后的关键字参数也可使用
key => value表达式,注意key要使用符号,例如1
h(1 ; :y => 2, :z => 3) # 6
分号后的关键字参数如果只是传递同名参数(例如
:z => z),可以直接简写为名称,例如1
2y = 2; z = 3;
h(1 ; y, z) # 6
如果关键字参数重复出现,会直接抛出异常,而不是用后者覆盖。
这部分的语法比较灵活,但是最好还是用最标准的写法,也就是和函数定义时的写法对应,先提供所有位置参数,然后提供所有键值对参数。
元组参数
Julia
支持在定义时把函数形参整体写成一个元组,在调用时同样也需要将参数打包为一个元组进行传递,例如
1
2
3
4
5function func((a, b))
return a + b
end
func((1, 2)) # 3
这个机制可以极大简化函数复合使用时的写法,将上一个函数返回的元组直接提供给下一个函数,不需要额外的赋值过程,例如
1
2
3
4
5
6
7
8
9function test(a)
return (a - 1, a + 1)
end
function func((a, b))
return a + b
end
func(test(3)) # 6
不定位置参数
与 Python 类似,Julia
提供了不定位置参数的语法,自动将多余的位置参数使用元组打包(类似Python的
*args ),例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function f(x, y, args...)
println("x = $x")
println("y = $y")
println("args = $args")
end
f(1, 2)
# x = 1
# y = 2
# args = ()
f(1, 2, 3, 4)
# x = 1
# y = 2
# args = (3, 4)
位置参数解包
与不定位置参数的语法直接对应,我们也可以在调用函数时,将列表/元组等解包为多个位置参数,依次传递给函数。
1
2
3
4
5
6
7
8
9function g(x, y, z)
return x + y + z
end
g([1, 2, 3]...) # 6
# = g(1, 2, 3)
g((2, 3, 4)...) # 9
# = g(2, 3, 4)
不定关键字参数
与不定位置参数类似,我们也可以使用具名元组来收集多余的关键字参数(Python的
**kwargs 则把多余的关键字参数收集为字典)
1
2
3
4
5
6
7
8
9
10
11
12
13function fn(; kwargs...)
println(typeof(kwargs))
println(kwargs)
end
fn(x = 1)
# Base.Pairs{Symbol, Int64, Tuple{Symbol}, @NamedTuple{x::Int64}}
# Base.Pairs(:x => 1)
fn(x = 1, y = 2)
# Base.Pairs{Symbol, Int64, Tuple{Symbol, Symbol}, @NamedTuple{x::Int64, y::Int64}}
# Base.Pairs(:x => 1, :y => 2)
关键字参数解包
同样地,我们也可以在调用函数时,将具名元组或键类型为Symbol的字典解包为多个关键字参数,依次传递给函数。
例如将具名元组解包 1
2
3nt = (z = 3, y = 2, x = 1)
h(; nt...) # 6
# = h(; z = 3, y = 2, x = 1)
将字典解包(注意字典的键的类型要求必须是Symbol,不能是String)
1
2
3
4
5
6
7function h(; x, y, z)
return x + y + z
end
dic1 = Dict(:x => 1, :y => 2, :z => 3)
h(; dic1...) # 6
# = h(; x = 1, y = 2, z = 3)
注意这里的 ; 都不可以省略,否则 Julia
会尝试将其解包后作为位置参数传递。
匿名函数
基础
Julia 支持匿名函数,写法非常数学化,比其它各种语言中的 lambda
语句都更加自然,例如 1
2
3
4f1 = x -> x^2 + 1;
f1(1) # 2
f1(2) # 5
涉及多个参数和无参数的匿名函数写法如下(也就是对参数列表必须使用小括号)
1
2
3
4
5
6
7f2 = (x,y) -> x + y;
f2(1,2) # 3
f0 = () -> println("Hello World");
f0() # Hello World
不建议使用下面这种无意义的匿名函数封装,可以直接用函数本身
1
x -> f(x)
匿名函数的主要用法是作为参数传递给其他函数,例如 1
2
3
4
5
6
7
8
9
10
11
12
13map(x -> 2*x, [1, 2, 3])
# 3-element Vector{Int64}:
# 2
# 4
# 6
filter(x -> x % 2 == 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
# 5-element Vector{Int64}:
# 2
# 4
# 6
# 8
# 10
do 语句
考虑到 map
等函数所接收的第一个参数可能是非常复杂的函数,此时使用匿名函数(即使加上begin ... end语句)的写法会非常难看
1
2
3
4
5
6
7
8
9
10
11
12
13map(x -> begin
if x == 1
return 1
else
return x * 2
end
end,
[1, 2, 3])
# 3-element Vector{Int64}:
# 1
# 4
# 6
Julia 为此提供了一个专门的 do
语法,可以将函数需要的匿名函数参数的定义后置,实际运算中作为一个匿名函数传递给函数的第一个参数
1
2
3
4
5
6
7
8
9
10
11
12map([1, 2, 3]) do x
if x == 1
return 1
else
return x * 2
end
end
# 3-element Vector{Int64}:
# 1
# 4
# 6
补充
函数复合
Julia 为函数的复合调用提供了一些语法糖。
第一个是完全遵循数学表示习惯的 \circ,例如
1
2
3
4
5
6
7
8f(x) = x * x;
g(x) = x + 1;
f(g(10)) # 121
g(f(10)) # 101
(f ∘ g)(10) # 121
(g ∘ f)(10) # 101
第二种则是管道风格的调用语法 |>,例如
1
2
3sqrt(sum(1:10)) # 7.416198487095663
1:10 |> sum |> sqrt # 7.416198487095663
