| 摘自 Bogumil Kaminski的 Julia for Data Analysis一书 这篇文章告诉你如何在Julia中使用多重调度。 如果你是一个数据科学家或任何与大量数据打交道的人,如果你对Julia语言感兴趣,请阅读它。 |
打75折 用于数据分析的Julia在manning.com结账时,在折扣代码框中输入fcckaminski 即可享受25%的折扣。
让我们来学习如何定义具有不同方法的函数,并将这些知识应用于一个名为 winsorized_mean.
为一个函数定义方法的规则
幸运的是,如果你了解 Julia 类型系统的工作原理,定义方法是相对容易的。你只需在函数的参数后添加类型限制即可 ::.如果省略了类型说明的部分,那么 Julia 就会假定值为 Any类型是允许的。
假设我们想创建一个函数 fun取一个单一的位置参数,其行为如下。
- 如果它被传递了一个数字,它应该打印
"a number was passed"除非它是一个Float64类型的值,在这种情况下,我们希望"a Float64 value"打印。 - 在所有其他情况下,我们希望打印
"unsupported type".
下面是一个例子,你可以通过为一个函数定义三个方法来实现这种行为 fun.
在上面的例子中,请注意,例如 1是一个 Number(因为它是 Int),但它不是 Float64,所以最具体的匹配方法是 fun(x::Number).
方法的模糊性问题
在为一个函数定义多个方法时,你必须记住的是要避免方法的模糊性。当Julia编译器无法决定对于一组给定的参数应该选择哪种方法时,就会发生这种情况。通过例子来理解这个问题是最容易的。假设你想定义一个接受两个位置参数的 bar 函数。如果其中任何一个是数字,它应该通知你。这里是实现这样一个函数的第一次尝试。
正如你所看到的,所有的工作都很顺利,直到我们想调用 bar通过传递一个数字作为它的第一个和第二个参数。在这种情况下,Julia 抱怨说它不知道哪个方法应该被调用,因为其中有两个可能被选中。幸运的是,我们得到了一个如何解决这种情况的提示。我们需要定义一个额外的方法来解决这个模糊的问题。
julia> bar(x::Number, y::Number) = "both arguments are numbers"
foo (generic function with 4 methods)
julia> bar(1, 2)
"both arguments are numbers"
julia> methods(bar)
# 4 methods for generic function "foo":
[1] bar(x::Number, y::Number) in Main at REPL[8]:1
[2] bar(x::Number, y) in Main at REPL[2]:1
[3] bar(x, y::Number) in Main at REPL[3]:1
[4] bar(x, y) in Main at REPL[1]:1
为什么多重调度是有用的?
了解Julia中的方法是如何工作的,是你应该掌握的基本知识的一部分。正如你在上面的例子中看到的,它允许用户根据函数的任何位置参数的类型来区分函数的行为。结合灵活的类型层次系统,多重调度允许 Julia 程序员编写高度灵活和可重用的代码。请注意,通过在一个合适的抽象层次上指定类型,用户不必考虑每一个可能被传递给函数的具体类型,同时仍然保留对接受何种值的控制。例如,如果你定义了自己的 Number子类型,就像Decimals.jl(https://github.com/JuliaMath/Decimals.jl)包所做的那样,它的特点是支持任意精度的十进制浮点计算的类型,你不必重写你的代码。即使原来的代码不是专门针对这个用例而开发的,所有的代码也只是在新的类型下工作。
改进后的winsorized mean的实现
我们准备改进我们的 winsorized_mean函数的定义。下面是你可以实现它的方法。
首先注意,我们已经限制了允许的类型为 x和 k的类型,因此,如果你试图调用该函数,其参数必须符合要求的类型。
julia> winsorized_mean([8, 3, 1, 5, 7], 1)
5.0
julia> winsorized_mean(1:10, 2)
5.5
julia> winsorized_mean(1:10, "a")
ERROR: MethodError: no method matching winsorized_mean(::UnitRange{Int64}, ::String)
Closest candidates are:
winsorized_mean(::AbstractVector{T} where T, ::Integer) at REPL[6]:1
julia> winsorized_mean(10, 1)
ERROR: MethodError: no method matching winsorized_mean(::Int64, ::Int64)
Closest candidates are:
winsorized_mean(::AbstractVector{T} where T, ::Integer) at REPL[6]:1
此外,我们可以看到代码中有几处地方使它变得稳健。首先,我们检查传递的参数是否一致,也就是说,如果 k为负数或过大,则是无效的,在这种情况下,我们通过调用 throw函数,并以 ArgumentError作为其参数。看看如果我们传递错误的 k:
接下来,在对向量进行排序之前,将存储在向量中的数据复制一份。 x向量中的数据进行分类。为了达到这个目的,我们使用 collect函数,该函数接收任何可迭代的集合,并返回一个存储相同数值的对象,该对象有一个 Vector类型的对象。我们将这个向量传递给 sort!函数来进行就地排序。
你可能会问为什么使用 collect函数来分配一个新的 Vector是需要的。原因是,例如像 1:10是只读的,因此以后我们将不能用 y用 y[i] = y[k + 1]和 y[end - i + 1] = y[end - k].此外,在一般情况下,Julia可以支持数组中非基于1的索引(见github.com/JuliaArrays…。然而。 Vector使用基于1的索引。总之,使用 collect函数将任何集合或一般 AbstractVector变成一个标准的 Vector类型,它是可变的,并且使用基于 1 的索引。
最后请注意,我们没有手动执行for循环,而是使用了 sum函数,它更简单也更健壮。
在方法中添加参数类型注释是否能提高其执行速度?
为函数参数添加类型注解可以使 Julia 代码更容易阅读,也更安全。用户经常问的一个自然问题是,它是否提高了代码的执行速度。
如果你有某个函数的单一方法,那么添加类型注释并不能提高代码执行速度。原因是,当某个函数被调用时,Julia 编译器知道你传递给它的参数类型,并使用这些信息生成本地机器代码。换句话说:类型限制信息并不影响代码的生成。
然而,如果你为某个函数定义了多个方法,情况就不同了。原因是,类型限制会影响到方法的调度。那么,每个方法都可以有一个不同的实现,使用一个针对给定类型的值进行优化的算法。使用多重调度允许 Julia 编译器挑选最适合你的数据的实现。
让我用例子来解释一下。考虑一下 sort函数,我们在第二章中介绍过。通过调用 methods(sort)你可以了解到它在 Base Julia 中定义了五个不同的方法(如果你加载了一些 Julia 包,可能还有更多)。有一个用于排序向量的一般方法,其签名为 sort(v::AbstractVector; kws...)和一个专门用于排序范围的方法,如1:3,其签名是 sort(r::AbstractUnitRange).
有这种专门的方法有什么好处呢?答案是,第二个方法被定义为 sort(r::AbstractUnitRange) = r.因为我们知道 AbstractUnitRange的对象已经被排序了(它们是增量等于1的数值范围),所以我们可以直接返回传递的数值。在这种情况下,利用方法签名中的类型限制可以大大改善 sort显著提高操作性能。
现在就这些了。谢谢你的阅读。
The postUsing Multiple Dispatch in Juliaappeared first onManning.
