参数传递模式
Julia的参数传递是“共享传递”(pass by sharing),
这样可以省去复制的开销。
如果参数是标量的数值、字符串、元组(tuple)这样的非可变类型,
参数原来的值不会被修改;
如果参数是数组这样的可变(mutable)数据类型,
则函数内修改了这些参数保存的值,传入的参数也会被修改。
例如,非可变类型不会被修改:
function f(n)
println("Inside f() before changing, n=", n)
n = -1
println("Inside f() after changing, n=", n)
return
end
function test()
n = 1
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)
x[i] *= 2
end
end
xx = [1, 2, 3]
double!(xx)
@show xx;
## xx = [2, 4, 6]
Julia函数的命名习惯是,
如果函数会修改自变量的值,
将函数名末尾加上一个叹号后缀。
在上面的例子中,
函数double!()
改变了数组自变量x
的元素的值。
如果直接对自变量赋值,
则相当于在函数中重新绑定x
,
不会对输入的原始数组造成影响,如:
function double_wrong(x)
x = 2 .* x
return x
end
xx = [1, 2, 3]
double_wrong(xx)
@show xx;
## xx = [2, 4, 6]
无名函数
Julia的函数也是所谓“第一类对象”(first class objects),
可以将函数本身绑定在一个变量上,
函数名并非必须,
允许有无名函数。
无名函数在“函数式编程”(functional programming)范式中有重要作用。
无名函数格式是: 参数表 -> 返回值表达式
,
其中参数表即自变量表,
没有自变量时参数表写()
,
只有一个自变量时可以不写圆括号而只写自变量名,
有多个自变量时将自变量表写成一个元组格式。
比如,函数
f(x)=x2+1又可以写成
x -> x^2 + 1
## #12 (generic function with 1 method)
无名函数的另一种写法如
function (x)
x^2 + 1
end
## #14 (generic function with 1 method)
这样就产生了一个无名函数。
利用无名函数可以制作“函数工厂”,
即返回值为函数的函数。
例如:
make_power(α) = x -> x^α
f2 = make_power(2)
f3 = make_power(3)
[f2(2), f3(2)] |> show
## [4, 8]
链式调用与函数复合
用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>
。
这样的复合运算在有确定的函数名时当然没有必要,
但是如果用变量保存了函数,
以至于预先不知道要复合的函数,
就是需要的。
如:
funcs = [uppercase, lowercase, first]
fruits = ["apple", "banana", "carrot"]
fnew = funcs[1] ∘ funcs[3]
y = map(fnew, fruits);
@show y;
## y = ['A', 'B', 'C']
程序对每个单词取第一个字母并转换成大写。
等效于下面每一行:
map(uppercase∘first, fruits)
(uppercase∘first).(fruits)
uppercase.(first.(fruits))
fruits .|> first .|> uppercase
[(uppercase∘first)(xi) for xi in fruits]
[uppercase(first(xi)) for xi in fruits]
示例:图像点阵下标与xy坐标转换
一个图像可以简单地看成一个
m×n矩阵,
矩阵元素是像素点,
一般取元素值。
为了对图像进行一些数学变换,
需要将图像看成是直角坐标平面上定义的二元函数,
而矩阵的行列下标与一般的实数值的x、y坐标还是有差别。
下面的函数来自MIT的计算思维导论公开课件,
用于将图像与x、y坐标上的二元函数相互转换。
下面是将图像从平面直角坐标定位转换回到整数行、列下标定位的函数,
输入为图像,一对
(x,y)坐标,
输出对应的
(i,j)下标。
认为坐标原点在图像正中心,超出范围的点返回白色或黑色。
这是一个标量函数,没有针对整个图像变换,
但输入整个图像。
因为Julia的函数自变量是引用传输而不是复制传输,
所以输入整个图像并不损失效率。
function transform_xy_to_ij(img::AbstractMatrix, x::Float64, y::Float64)
rows, cols = size(img) # 行、列数
m = max(cols, rows)
# 平移变换
translate(α,β) = ((x, y),) -> [x+α, y+β]
# 坐标互换
swap(x,y) = [y, x]
# 纵坐标颠倒
flipy((x, y)) = [x, -y]
# 伸缩变换
scale(α) = ((x,y),) -> (α*x, α*y)
# 从平面直角坐标到行、列下标的函数
xy_to_ij = translate(rows/2, cols/2) ∘ swap ∘ flipy ∘ scale(m/2)
# 使用函数将输入的一对(x, y)值转换为整数
i, j = floor.(Int, xy_to_ij((x, y)))
# 注意:返回值仅两个整数,不是向量化的。
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是横向、纵向的像素点数的最大值
# 用来转换的函数。
ij_to_xy = scale(2/pixels) ∘ flipy ∘ swap ∘ translate(-pixels/2,-pixels/2)
# 上式从右向左解读:记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]×[1,m]
平移到 [−m/2,m/2]×[−m/2,m/2]
;
- 交换行、列关系;
- 将纵轴颠倒;
- 将坐标范围限制在 [−1,1]×[−1,]
之间。
还有一个配套的给定了图像和一个
(x,y)坐标,
取出对应的像素点的函数。
超出范围则给出一个纯黑像素点。
using Colors
function getpixel(img::AbstractMatrix,i::Int, j::Int)
rows, cols = size(img)
m = max(cols,rows)
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
可变个数参数与元组实参
在自定义函数的自变量名后面加上三个句点作为后缀,如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()
求最大值?
可以用“展开”(splatting)的方法将一个变量中的多个值展开成函数的自变量,
方法是在作为实参的自变量名后面加三个句点后缀,如
x = [1, 3, 1, 4]
max(x...)
## 4
递归调用
函数定义时允许调用自己,
这使得许多本质上是递推的计算程序变得很简单,
比如,
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序列,
F1=1,
F2=1,
Fn=Fn−1+Fn−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)]
递归程序只是把用递推表示的问题变得容易变成解决,
很多时候效率并不好。
比如,
为了计算
F5,
需要用到
F4和
F3,
算
F4的时候又需要重新计算
F3,
所以
F3被重复计算了2次,
计算
F3要计算
F2,
F4也要计算
F2,
所以
F2也被重复计算了
3次。
这都是不必要的额外计算,
比如改成如下的正向循环:
function myfib2(n)
local f1, f2, f3
if n <= 0
return 0
elseif n==1 || n==2
return 1
end
f1 = 1
f2 = 1
for i=3:n
f3 = f1 + f2
f1, f2 = f2, f3
end
return f3
end
myfib2.(1:10) |> show
## [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
闭包
可以在函数内定义内嵌函数并以此内嵌函数为函数返回值,
称这样的内嵌函数为闭包(closure)。
闭包的好处是可以保存定义时的局部变量作为一部分,
产生“有状态的函数”,
类似于面向对象语言中的方法,
而Julia并不支持传统的面向对象语言中那样的类。
例如,
某个函数希望能记住被调用的次数。
传统的方法无法解决问题,比如下面的版本是无效的:
function counter_old()
n = 0
n = n+1
return n
end
println(counter_old(), ", ", counter_old())
## 1, 1
可以用如下的闭包做法:
function make_counter()
n = 0
function counter()
n += 1
return n
end
end
my_counter = make_counter()
typeof(my_counter)
## var"#counter#22"
println(my_counter(), ", ", my_counter())
## 1, 2
递归函数在反复调用自身以后效率会很低,
比如,前面计算Fibonacci数的递归函数,
因为对每个
n都要多次调用自变量为
0,1,2,…的情形,
所以进行许多不必要的重复计算。
可以用闭包的方法将已有结果保存,
使得计算效率大大提高:
function makefib()
saved = Dict(0=>0, 1=>1)
function fib(n)
if n ∉ keys(saved)
saved[n] = fib(n-1) + fib(n-2)
end
return saved[n]
end
end
myfibnew = makefib()
show(myfibnew.(0:9))
## [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函数式编程
无名函数经常用在map()
, filter()
,reduce()
这样的函数式编程函数中。
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
y = 0
elseif x > 100
y = 100
else
y = x
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
y = 0
elseif x > 100
y = 100
else
y = x
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]
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]
reduce
reduce(f, x)
中f
是接受两个自变量的函数,
如加法、乘法,结果是将x
中的元素用f
反复按结合律计算结果。
称这种做法为约化计算。
比如,
求x
的元素和,
除了sum(x)
函数,
也可以写成reduce(+, x)
:
对于多维数组,
可以指定沿哪一个维度方向进行简化计算
mat = reshape([1:9;], 3, 3)
3×3 Array{Int64,2}:
1 4 7
2 5 8
3 6 9
1×3 Array{Int64,2}:
6 15 24
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
accumulate
类似于sum()
求总和而cumsum()
给出累计求和的所有中间结果,cumprod()
计算连乘过程中的所有中间结果,
函数accumulate()
可以将二元运算累计地计算并给出每一步的中间结果。
如:
cumsum(1:5) |> show
## [1, 3, 6, 10, 15]
accumulate(+, 1:5) |> show
## [1, 3, 6, 10, 15]
用于多维数组时可以指定在某个维方向上进行累计计算。如:
mat = reshape([1:9;], 3, 3)
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()
对非数值元素的数组也是适用的。
异常处理
只要是程序,
就难以避免会有出错的时候。
为了在程序出错的时候造成严重后果,
传统的办法是对程序增加许多判断,
确保输入和处理是处于合法和正常的状态。
这样的做法过于依赖于程序员的经验,
程序代码也过于繁复。
现代程序设计语言增加了“异常处理”功能。
程序出错时,
称为发生了异常(exception),
编程时可以用专门的程序“捕获”这些异常,
并进行处理。
这并不能保证程序不出错,
而是出错时能得到及时的、不至于造成严重后果的处理。
Julia中捕获异常并处理的基本结构是:
try
可能出错的程序
catch 异常类型变量名
异常处理程序
end
如
x = [2, -2, "a"]
for xi in x
try
y = sqrt(xi)
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
韭菜热线原创版权所有,发布者:风生水起,转载请注明出处:https://www.9crx.com/75088.html