Julia 学习笔记——4. 函数基础
Julia 中的函数与 Python
很类似,都是作为语言的一等公民,包括直接把函数视作变量,可以用函数赋值和作为参数传递等都是一样的,两者最大的区别可能就是关键字从
Python 的 def 换成 Julia 的
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 是
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::Int64, y::Int64)::Int64
x + y
end
h(1, 2) # 3
如果函数体中的实际返回值类型与函数声明中的返回值类型不匹配,Julia
会对其进行类型转换。 1
2
3
4
5
6function h2(x::Int64, y::Int64)::Int64
Int32(x + y)
end
h2(1, 2) # 3
typeof(h2(1,2)) # Int64
返回值类型在 Julia 中很少使用,Julia 可以自动推断返回值类型。
Julia 对函数参数则不会进行自动的类型转换。 1
h(Int32(1), 2) # error
参数类型在 Julia 中非常重要,Julia 通过同名函数的不同参数列表来进行动态分派,这是 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
在计算参数的默认值时,只有先前的参数才在作用域内,例如
1
2
3
4
5
6
7
8b = 10
function g(a = b, b = a + 1)
println("a=", a)
println("b=", b)
end
g() # g(10, 11)
这里 a = b 会使用外部作用域的变量
b,而不是后面的参数 b。
位置参数和关键字参数
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
支持在定义时把函数形参整体写成一个元组,在调用时同样也需要将参数打包为一个元组进行传递,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
匿名函数可以在定义时立刻调用,例如 1
(x -> 2*x)(3) # 6
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
do 语句可以用来实现类似 Python with
语句的效果,例如打开文件操作的代码块 1
2
3open("outfile", "w") do io
write(io, data)
end
可以有如下的 toy 实现 1
2
3
4
5
6
7
8function open(f::Function, args...)
io = open(args...)
try
f(io)
finally
close(io)
end
end
补充
操作符也是函数
Julia 的大多数操作符(除了需要短路求值的 && 和
||)其实都是特殊的函数,可以使用函数的语法来调用
1
21+2+3 # 6
+(1,2,3) # 6
两者是完全等价的,实际上编译器会把前者翻译为后者。
还可以将操作符赋值给变量,但是它不支持中缀表达式 1
2f = +
f(1,2,3) # 6
向量化函数的点语法
Julia 可以很方便地将函数调用变成向量化版本 1
2f.(A)
sin.(A)
涉及到多个参数时,会首先对其进行广播以协调数组的尺寸
1
2
3f.(args...)
# i.e.
broadcast(f, args...)
如果广播失败,会抛出错误。
需要注意的是:关键字参数不会被广播处理,而是原样保留,在逐个分量的函数调用中忠实传递,例如
1
2
3round.(X, digits=3)
# i.e.
broadcast(x -> round(x, digits=3), x)
对于嵌套的函数调用,向量化调用会尝试将其融合到一个广播循环中,例如
1
2
3sin.(cos.(X))
# i.e.
broadcast(x -> sin(cos(x)), X)
实际只有一次循环,最终的结果相当于 1
[sin(cos(x)) for x in X]
如果融合失败,会抛出错误。 1
sin.(sort(cos.(X))) # error
.= 可以用来预分配运算结果,例如 1
2
3X .= sin.(Y)
# i.e.
broadcast!(sin, X, Y)
函数复合与链式调用
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
将其与匿名函数结合可以实现更加丰富的功能,例如 1
21:3 .|> (x -> x^2) |> sum |> sqrt
# 3.7416573867739413
注意这里的 .|> 把 |>
与点操作进行了组合。
