3 函数进阶
3.1 参数传递模式
Julia的参数传递是“共享传递”(pass by sharing), 这样可以省去复制的开销。 如果参数是标量的数值、字符串、元组(tuple)这样的非可变类型, 参数原来的值不会被修改; 如果参数是数组这样的可变(mutable)数据类型, 则函数内修改了这些参数保存的值,传入的参数也会被修改。
例如,非可变类型不会被修改:
function f(n)
println("Inside f() before changing, n=", n)
= -1
n println("Inside f() after changing, n=", n)
return
end
function test()
= 1
n f(n)
println("Out of f(), n=", n)
end
test()
## Inside f() before changing, n=1
## Inside f() after changing, n=-1
## Out of f(), n=1
改变了输入的数组(可变类型)的值的例子:
function double!(x)
for i in eachindex(x)
*= 2
x[i] end
end
= [1, 2, 3]
xx double!(xx)
@show xx;
## xx = [2, 4, 6]
Julia函数的命名习惯是, 如果函数会修改自变量的值, 将函数名末尾加上一个叹号后缀。
在上面的例子中,
函数double!()
改变了数组自变量x
的元素的值。
如果直接对自变量赋值,
则相当于在函数中重新绑定x
,
不会对输入的原始数组造成影响,如:
function double_wrong(x)
= 2 .* x
x return x
end
= [1, 2, 3]
xx double_wrong(xx)
@show xx;
## xx = [2, 4, 6]
3.2 无名函数
Julia的函数也是所谓“第一类对象”(first class objects), 可以将函数本身绑定在一个变量上, 函数名并非必须, 允许有无名函数。 无名函数在“函数式编程”(functional programming)范式中有重要作用。
无名函数格式是: 参数表 -> 返回值表达式
,
其中参数表即自变量表,
没有自变量时参数表写()
,
只有一个自变量时可以不写圆括号而只写自变量名,
有多个自变量时将自变量表写成一个元组格式。
比如,函数\(f(x) = x^2 + 1\)又可以写成
-> x^2 + 1
x ## #12 (generic function with 1 method)
无名函数的另一种写法如
function (x)
^2 + 1
xend
## #14 (generic function with 1 method)
这样就产生了一个无名函数。
利用无名函数可以制作“函数工厂”, 即返回值为函数的函数。 例如:
make_power(α) = x -> x^α
= make_power(2)
f2 = make_power(3)
f3 f2(2), f3(2)] |> show
[## [4, 8]
3.3 链式调用与函数复合
用x |> f
可以表示f(x),
这样,x |> f |> g
就是复合调用g(f(x))
,
称为链式调用。
但是,不允许写成x |> f() |> g()
。
需要的话可以将每一步写成一个无名函数。
例如:
1:5;] |> (x -> x .^ 2) |> sum
[## 55
加点的运算,如g(f.(x))
,
可以写成x .|> f |> g
,
如:
1:5;] .|> (x -> x ^ 2) |> sum
[## 55
例子中的[1:5;]
的写法类似于collect(1:5)
。
链式调用中的无名函数应该用()
保护,
以免发生优先级疑惑。
g(f(x))
也可以用“∘
”运算符表示成g∘f(x)
,
“∘
”的结果仍是函数。
“∘
”输入方法为\circ<TAB>
。
这样的复合运算在有确定的函数名时当然没有必要,
但是如果用变量保存了函数,
以至于预先不知道要复合的函数,
就是需要的。
如:
= [uppercase, lowercase, first]
funcs = ["apple", "banana", "carrot"]
fruits = funcs[1] ∘ funcs[3]
fnew = map(fnew, fruits);
y @show y;
## y = ['A', 'B', 'C']
程序对每个单词取第一个字母并转换成大写。 等效于下面每一行:
map(uppercase∘first, fruits)
∘first).(fruits)
(uppercaseuppercase.(first.(fruits))
.|> first .|> uppercase
fruits ∘first)(xi) for xi in fruits]
[(uppercaseuppercase(first(xi)) for xi in fruits] [
3.3.1 示例:图像点阵下标与xy坐标转换
一个图像可以简单地看成一个\(m \times n\)矩阵, 矩阵元素是像素点, 一般取元素值。 为了对图像进行一些数学变换, 需要将图像看成是直角坐标平面上定义的二元函数, 而矩阵的行列下标与一般的实数值的x、y坐标还是有差别。 下面的函数来自MIT的计算思维导论公开课件, 用于将图像与x、y坐标上的二元函数相互转换。
下面是将图像从平面直角坐标定位转换回到整数行、列下标定位的函数, 输入为图像,一对\((x,y)\)坐标, 输出对应的\((i,j)\)下标。 认为坐标原点在图像正中心,超出范围的点返回白色或黑色。 这是一个标量函数,没有针对整个图像变换, 但输入整个图像。 因为Julia的函数自变量是引用传输而不是复制传输, 所以输入整个图像并不损失效率。
function transform_xy_to_ij(img::AbstractMatrix, x::Float64, y::Float64)
= size(img) # 行、列数
rows, cols = max(cols, rows)
m
# 平移变换
translate(α,β) = ((x, y),) -> [x+α, y+β]
# 坐标互换
swap(x,y) = [y, x]
# 纵坐标颠倒
flipy((x, y)) = [x, -y]
# 伸缩变换
scale(α) = ((x,y),) -> (α*x, α*y)
# 从平面直角坐标到行、列下标的函数
= translate(rows/2, cols/2) ∘ swap ∘ flipy ∘ scale(m/2)
xy_to_ij # 使用函数将输入的一对(x, y)值转换为整数
= floor.(Int, xy_to_ij((x, y)))
i, j # 注意:返回值仅两个整数,不是向量化的。
end
上面程序的主要语句是xy_to_ij
的定义。
该式从右向左解释:
- 先从[-1,1]区间变换到[-m/2, m/2]区间;
- 将y轴颠倒次序,这是因为用行下标代表纵轴, 行下标的增长方向是从上到下方向,与纵轴方向相反;
- 调换x, y,这是因为行下标i代表了上下方向,即y方向, 列下标j代表了左右方向,即x方向;
- 最后将i最小值变为0,j最小值变为0。
下面是从行列下标转换为平面直角坐标系坐标的函数,
输入一对\((i,j)\)下标,
和图像横、纵方向像素点数的最大值pixels
,
输出平面直角坐标系坐标\((x,y)\)。
function transform_ij_to_xy(i::Int, j::Int, pixels)
# 从行、列下标转换为直角平面坐标系坐标的函数
# pixels是横向、纵向的像素点数的最大值
# 用来转换的函数。
= scale(2/pixels) ∘ flipy ∘ swap ∘ translate(-pixels/2,-pixels/2)
ij_to_xy # 上式从右向左解读:记m=pixels,
# 先将坐标范围从$[1,m] \times
ij_to_xy([i,j])
end
上面函数的主要部分是函数ij_to_xy
的定义。
因为有参数pixels
,所以不直接使用此函数,
而是将其包裹在transform_ij_to_xy
中。
ij_to_xy
的定义也充分利用了函数复合,
从右向左解读如下(记pixels
为\(m\)):
- 将坐标范围从\([1,m] \times [1,m]\)平移到\([-m/2, m/2] \times [-m/2, m/2]\);
- 交换行、列关系;
- 将纵轴颠倒;
- 将坐标范围限制在\([-1,1] \times [-1,]\)之间。
还有一个配套的给定了图像和一个\((x,y)\)坐标, 取出对应的像素点的函数。 超出范围则给出一个纯黑像素点。
using Colors
function getpixel(img::AbstractMatrix,i::Int, j::Int)
= size(img)
rows, cols = max(cols,rows)
m black(c::RGB) = RGB(0,0,0)
black(c::RGBA) = RGBA(0,0,0,0.75)
if 1 < i ≤ rows && 1 < j ≤ cols
img[i, j]else
black(img[1,1])
end
end
3.4 可变个数参数与元组实参
在自定义函数的自变量名后面加上三个句点作为后缀,如args...
,
则此函数可以有可变个数的位置参数,
args
为一个元组。
如:
function f_vara1(x, args...)
println("x=", x)
println("其它参数:", args)
end
f_vara1(11, 1, 2, 3)
## x=11
## 其它参数:(1, 2, 3)
有时需要传递给一个多自变量函数的实参保存在了一个变量中,
比如,
函数max()
求各个自变量中最大值,
如
max(1, 3, 1, 4)
## 4
如果要求最大值的数已经在一个元组或数组中如何利用max()
求最大值?
可以用“展开”(splatting)的方法将一个变量中的多个值展开成函数的自变量,
方法是在作为实参的自变量名后面加三个句点后缀,如
= [1, 3, 1, 4]
x max(x...)
## 4
3.5 递归调用
函数定义时允许调用自己, 这使得许多本质上是递推的计算程序变得很简单, 比如,\(n! = n(n-1)!\), 用递归函数可以写成:
function myfact(n)
if n==1
return 1
else
return n*myfact(n-1)
end
end
myfact(5)
## 120
递归程序必须有初始结果, 比如上面程序中\(n=1\)时结果为1; 递归必须能逐步退回到初始结果的地方。
再比如Fibonacci序列, \(F_1 = 1\), \(F_2 = 1\), \(F_n = F_{n-1} + F_{n-2}\), 前几个数是1, 1, 2, 3, 5, 8, 13, 21, 34。 用递归调用写成
function myfib(n)
if n <= 0
return 0
elseif n==1 || n==2
return 1
else
return myfib(n-1) + myfib(n-2)
end
end
show([(n, myfib(n)) for n=0:9])
## [(0, 0), (1, 1), (2, 1), (3, 2), (4, 3),
## (5, 5), (6, 8), (7, 13), (8, 21), (9, 34)]
递归程序只是把用递推表示的问题变得容易变成解决, 很多时候效率并不好。 比如, 为了计算\(F_5\), 需要用到\(F_4\)和\(F_3\), 算\(F_4\)的时候又需要重新计算\(F_3\), 所以\(F_3\)被重复计算了2次, 计算\(F_3\)要计算\(F_2\), \(F_4\)也要计算\(F_2\), 所以\(F_2\)也被重复计算了\(3\)次。 这都是不必要的额外计算, 比如改成如下的正向循环:
function myfib2(n)
local f1, f2, f3
if n <= 0
return 0
elseif n==1 || n==2
return 1
end
= 1
f1 = 1
f2 for i=3:n
= f1 + f2
f3 = f2, f3
f1, f2 end
return f3
end
myfib2.(1:10) |> show
## [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
3.6 闭包
可以在函数内定义内嵌函数并以此内嵌函数为函数返回值, 称这样的内嵌函数为闭包(closure)。 闭包的好处是可以保存定义时的局部变量作为一部分, 产生“有状态的函数”, 类似于面向对象语言中的方法, 而Julia并不支持传统的面向对象语言中那样的类。
例如, 某个函数希望能记住被调用的次数。 传统的方法无法解决问题,比如下面的版本是无效的:
function counter_old()
= 0
n = n+1
n return n
end
println(counter_old(), ", ", counter_old())
## 1, 1
可以用如下的闭包做法:
function make_counter()
= 0
n function counter()
+= 1
n return n
end
end
= make_counter()
my_counter typeof(my_counter)
## var"#counter#22"
println(my_counter(), ", ", my_counter())
## 1, 2
递归函数在反复调用自身以后效率会很低, 比如,前面计算Fibonacci数的递归函数, 因为对每个\(n\)都要多次调用自变量为\(0, 1, 2, \dots\)的情形, 所以进行许多不必要的重复计算。 可以用闭包的方法将已有结果保存, 使得计算效率大大提高:
function makefib()
= Dict(0=>0, 1=>1)
saved function fib(n)
if n ∉ keys(saved)
= fib(n-1) + fib(n-2)
saved[n] end
return saved[n]
end
end
= makefib()
myfibnew show(myfibnew.(0:9))
## [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
3.7 函数式编程
无名函数经常用在map()
, filter()
,reduce()
这样的函数式编程函数中。
3.7.1 map
map(f, x)
将函数f
作用到容器x
的每个元素上,
有时可以用f.(x)
代替,
但有些情况下不能使用广播(加点语法)。
如:
map(x -> x^2 + 1, [1,2,3])
3-element Array{Int64,1}:
2
5
10
在调用map()
函数时,
如果对每个元素执行的操作需要用多行代码完成,
用无名函数就不太方便。
例如,对数组的每个元素,
负数映射到0,
大于100的数映射到100,
其它数值不变,用有名函数可以写成:
function fwins(x)
if x < 0
= 0
y elseif x > 100
= 100
y else
= x
y end
return y
end
fwins.([-1, 0, 80, 120]) |> show
## [0, 0, 80, 100]
也可以写成map(fwins, [-1, 0, 80, 120])
。
如果要用无名函数的格式,
Julia还提供了map函数的一种do块格式,如
map([-1, 0, 80, 120]) do x
if x < 0
= 0
y elseif x > 100
= 100
y else
= x
y end
return y
end
4-element Array{Int64,1}:
0
0
80
100
注意这样调用map()
时圆括号中仅有要处理的数据,
要进行的操作写在do
关键字后面,
操作写成了不带->
符号的无名函数格式。
对于多元函数, 计算是按对应元素进行的,如:
map((x,y) -> 2*x + y, [1, 2, 3], [10, 20, 30]) |> show
## [12, 24, 36]
3.7.2 filter
函数filter(f, x)
中f
是返回布尔值的函数,
称这样的函数为示性函数(indicator functions),
x
是向量,
filter(f, x)
的结果是将f
作用在x
的每个元素上,
输出f
的结果为真值的那些元素组成的数组。如
filter(x -> x>0, [-2, 0, 1,2,3]) |> show
## [1, 2, 3]
3.7.3 reduce
reduce(f, x)
中f
是接受两个自变量的函数,
如加法、乘法,结果是将x
中的元素用f
反复按结合律计算结果。
称这种做法为约化计算。
比如,
求x
的元素和,
除了sum(x)
函数,
也可以写成reduce(+, x)
:
reduce(+, 1:3)
## 6
对于多维数组, 可以指定沿哪一个维度方向进行简化计算
= reshape([1:9;], 3, 3) mat
3×3 Array{Int64,2}:
1 4 7
2 5 8
3 6 9
reduce(+, mat, dims=1)
1×3 Array{Int64,2}:
6 15 24
reduce(+, x, dims=2)
3×1 Array{Int64,2}:
12
15
18
有一些reduce
的操作已经写成了内置函数,
如sum
, prod
, minimum
, maximum
, all
, any
。
可以将map
与reduce
合并为mapreduce()
函数,如:
mapreduce(x -> x ^ 2, +, [1:3;])
## 14
3.7.4 accumulate
类似于sum()
求总和而cumsum()
给出累计求和的所有中间结果,
cumprod()
计算连乘过程中的所有中间结果,
函数accumulate()
可以将二元运算累计地计算并给出每一步的中间结果。
如:
cumsum(1:5) |> show
## [1, 3, 6, 10, 15]
accumulate(+, 1:5) |> show
## [1, 3, 6, 10, 15]
用于多维数组时可以指定在某个维方向上进行累计计算。如:
= reshape([1:9;], 3, 3)
mat accumulate(+, mat, dims=1)
3×3 Array{Int64,2}:
1 4 7
3 9 15
6 15 24
accumulate(+, mat, dims=2)
3×3 Array{Int64,2}:
1 5 12
2 7 15
3 9 18
map()
、filter()
、reduce()
对非数值元素的数组也是适用的。
3.8 异常处理
只要是程序, 就难以避免会有出错的时候。 为了在程序出错的时候造成严重后果, 传统的办法是对程序增加许多判断, 确保输入和处理是处于合法和正常的状态。 这样的做法过于依赖于程序员的经验, 程序代码也过于繁复。
现代程序设计语言增加了“异常处理”功能。 程序出错时, 称为发生了异常(exception), 编程时可以用专门的程序“捕获”这些异常, 并进行处理。 这并不能保证程序不出错, 而是出错时能得到及时的、不至于造成严重后果的处理。
Julia中捕获异常并处理的基本结构是:
try
可能出错的程序
catch 异常类型变量名
异常处理程序end
如
= [2, -2, "a"]
x for xi in x
try
= sqrt(xi)
y println("√", xi, " = ", y)
catch ex
if isa(ex, DomainError)
println("√", xi, ": 平方根函数定义域异常")
else
print("√", xi, ": 平方根函数其它异常")
end
end
end
## √2 = 1.4142135623730951
## √-2: 平方根函数定义域异常
## √a: 平方根函数其它异常
异常处理代码会降低程序效率, 一些常见错误还是应该使用逻辑判断预先排除。
throw()
函数可以当场生成(称为“抛出”)一个异常对象,
如throw(DomainError())
。
Julia中内建了一些异常类型,
详见Julia的手册。
error(msg)
可以直接抛出ErrorException对象,
使得程序停止,
并显示给出的msg
字符串的内容。
try
结构可以有一个finally
部分,
表示不论有没有出错,
都需要在最后运行的语句。
框架如:
try
可能出错的程序
catch 异常类型变量名
异常处理程序
finally
无论如何最后都要执行的程序end