在Julia中使用多重调度

165 阅读7分钟

Description: https://images.manning.com/360/480/resize/book/e/542293a-5ae6-432b-a2c0-c388b32affd6/Kaminski-MEAP-HI.png

摘自 Bogumil Kaminski的 Julia for Data Analysis一书

这篇文章告诉你如何在Julia中使用多重调度。

如果你是一个数据科学家或任何与大量数据打交道的人,如果你对Julia语言感兴趣,请阅读它。


打75折 用于数据分析的Juliamanning.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函数的定义。下面是你可以实现它的方法。

 

首先注意,我们已经限制了允许的类型为 xk的类型,因此,如果你试图调用该函数,其参数必须符合要求的类型。

 
 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是只读的,因此以后我们将不能用 yy[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.