朱莉娅:自定义类型

在本文中,我们将考虑向Julia程序中添加自定义数据类型,并重载标准函数,以方便使用新类型。


什么是自定义类型


一个可怕的秘密-Julia中“用户”和“内置”数据类型之间的界线实际上不存在。 在任何Julia程序中,您都可以定义自己的数据类型,并且在编写代码或执行此代码的速度方面,使用内置数据与使用内置数据几乎没有什么不同(您好,Python)。 输入数据类型可以是原始数据或复合数据。 后者也分为抽象和具体类型。


此外,我们将考虑复合混凝土类型的示例,因为 内存中的特定对象是特定类型,抽象对象仅用于构建层次结构。


复合类型由关键字structmutable struct定义。 在第一种情况下,将创建一个不可变的类型:


 #    x  y struct Point x y end 

Point类型的对象Point不可变的,即 一旦创建的对象中的字段值不能更改。


 julia> p = Point(2, 3) Point(2, 3) julia> px 2 julia> py 3 julia> px = 4 ERROR: setfield! immutable struct of type Point cannot be changed 

但是您可以创建一个可变类型:


 mutable struct MutablePoint x y end julia> mp = MutablePoint(3, 4) MutablePoint(3, 4) julia> mp.y = 2 2 julia> mp MutablePoint(3, 2) 

为了提高效率,可以将类型注释添加到结构字段和结构本身。 例如:


 struct ParametricPoint{T<:Real} x::T y::T end 

这意味着类型ParametricPoint由类型T参数化,并且字段xy现在应该是相同类型TT 该行为将如下所示:


 julia> ParametricPoint(1, 1) ParametricPoint{Int64}(1, 1) julia> ParametricPoint{Float64}(1, 1) ParametricPoint{Float64}(1.0, 1.0) julia> ParametricPoint{Rational}(1, 1) ParametricPoint{Rational}(1//1, 1//1) # :        T julia> ParametricPoint{Int}(1, 3.4) ERROR: InexactError: Int64(3.4) # :    julia> ParametricPoint(1, 1.0) ERROR: MethodError: no method matching Point(::Int64, ::Float64) # :  T      julia> ParametricPoint{Any}(1,1) ERROR: TypeError: in Point, in T, expected T<:Real, got Type{Any} 

接下来,让我们看一个稍微复杂一些的容器类型示例,以查看标准库中用于处理此类类型的方法。


两路排队


我们将从DequeNode类型的节点创建一个双向队列,该队列将包含数据记录以及指向该队列的上一个和下一个元素的链接。 DequeNode类型必须是可变的,因为 链接将需要重写。


 mutable struct DequeNode{T} data::T prev::DequeNode{T} next::DequeNode{T} #   -       function DequeNode{T}() where T node = new{T}() node.prev = node.next = node return node end #     function DequeNode{T}(x) where T node = new{T}() node.data = x node.prev = node.next = node return node end end 

在这种情况下,需要内部构造函数,因为默认情况下,在构造对象时,您需要将所有字段的值与参数一起传递。 由于类型是自引用的,因此标准构造函数无法创建此类型的第一个对象。 因此,我们使用特殊的构造函数来创建引用自身的队列节点。


队列本身可以表示为不包含任何数据的虚拟“输入”节点。 它的next链接将指向队列的第一个元素,而上next则指向最后一个。 对于空队列,两个链接都指向输入节点。 这样的组织简化了添加和删除节点的过程。


 struct Deque{T} entry::DequeNode{T} end Deque{T}() where T = Deque{T}(DequeNode{T}()) Deque() = Deque{Any}() 

队列本身的结构不再需要内部构造函数。


新增项目


队列的标准过程是在开始和结束处添加元素,并从开始和结束处提取元素。 对于这些过程,标准库已经具有一些函数,您只需要向这些函数添加用于其他类型的方法:


  • push!(collection, element) -向集合中添加元素(对于有序集合-最后)
  • pushfirst!(collection, element) -将元素添加到有序集合的开头
  • pop!(collection) -从集合中删除项目(对于有序集合-从最后开始)
  • popfirst!(collection) -从有序集合中删除第一项

所有这些功能都在Base模块中,您需要向其添加新方法。 另外,我们将编写一个函数来检查集合是否不为空Base.isempty(collection)


 Base.isempty(deq::Deque) = deq.entry.prev ≡ deq.entry function Base.push!(deq::Deque{T}, elt) where T tail = deq.entry.prev new_item = DequeNode{T}(elt) new_item.prev, new_item.next = tail, deq.entry tail.next = deq.entry.prev = new_item return deq end function Base.pushfirst!(deq::Deque{T}, elt) where T head = deq.entry.next new_item = DequeNode{T}(elt) new_item.prev, new_item.next = deq.entry, head head.prev = deq.entry.next = new_item return deq end function Base.pop!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) last = deq.entry.prev last.prev.next = deq.entry deq.entry.prev = last.prev return last.data end function Base.popfirst!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) first = deq.entry.next first.next.prev = deq.entry deq.entry.next = first.next return first.data end 

push!()函数使得基于任意集合编写队列构造函数几乎变得微不足道:


 function Deque(itr) d = Deque{eltype(itr)}() for elt in itr push!(d, elt) end return d end #        Deque(args...) = Deque(args) 

现在,如果我们只需要添加和删除操作,并且不需要像peek()这样的函数(请查看队列开头或结尾的元素而不删除它),则几乎可以将新类型用作队列。 对于元素的非破坏性访问,在容器上定义迭代函数很方便。


迭代


Julia支持遍历容器元素的迭代器的概念。 对于迭代,您需要定义一个函数iterate(container, state) ,其中state确定迭代器的当前状态。 特别地, for x in collection构造是类似以下内容的语法糖:


 let nextstate = iterate(collection) while nextstate ≢ nothing (x, state) = nextstate <- > nextstate = iterate(collection, state) end end 

iterate()函数将容器和“迭代器状态”作为参数,并从容器元素和下一个迭代器状态返回元组,如果容器已结束则不返回任何内容。
对于队列,将迭代达到的队列节点作为“状态”是合乎逻辑的:


 function Base.iterate(deq::Deque{T}, state::DequeNode{T}=deq.entry) where T nextstate = state.next nextstate ≡ deq.entry ? nothing : (nextstate.data, nextstate) end 

现在可以使用for循环迭代队列的元素:


 julia> for x in Deque(1:10) println(x); end 1 2 3 4 5 6 7 8 9 10 

最大的优点是标准Julia库的许多功能都是使用通用接口(尤其是迭代)编写的。 因此,将自动确定是否自动检查元素是否属于容器:


 julia> 5 ∈ Deque(3:10) true julia> 100 ∈ Deque(-5:5) false 

我们还发现,返回集合的第一个元素的first()方法开始起作用。 为了完成图片,我们补充了last()方法以获得最后一个元素并以相反的顺序进行迭代:


 function Base.iterate(r::Base.Iterators.Reverse{Deque{T}}, state::DequeNode{T}=r.itr.entry) where T nextstate = state.prev nextstate ≡ r.itr.entry ? nothing : (nextstate.data, nextstate) end function Base.last(deq::Deque) isempty(deq) && throw(ArgumentError("deque must be non-empty")) deq.entry.prev.data end julia> for x in Iterators.reverse(Deque(1:10)) println(x); end 10 9 8 7 6 5 4 3 2 1 

因此,队列接口作为数据结构现已完全定义


运作方式功能名称
在末尾插入push!(deque, element)
插入到开始pushfirst!(deque, element)
从一开始就删除popfirst!(deque) (返回已删除的项目)
从头去除pop!(deque) (返回已删除的项目)
查看第一项first(deque)
查看最后一个项目last(deque)

打印输出结构


尽管队列可以完全用作数据结构,但在打印时,它仍然代表无形的混乱:


 julia> Deque() Deque{Any}(DequeNode{Any}(#undef, DequeNode{Any}(#= circular reference @-1 =#), DequeNode{Any}(#= circular reference @-1 =#))) 

添加一种以更人性化的形式打印队列的方法。 结构到终端的输出由Base.show()函数控制,我们将对其进行重载:


 function Base.show(io::IO, deq::Deque{T}) where T print(io, "Deque{", T, "}(") next = iterate(deq) if next ≢ nothing item, state = next show(io, item) next = iterate(deq, state) while next ≢ nothing item, state = next print(io, ", ") show(io, item) next = iterate(deq, state) end end print(io, ")") end #        julia> Deque(1:5) Deque{Int64}(1, 2, 3, 4, 5) #       julia> Deque(Deque(), Deque(), Deque()) Deque{Deque{Any}}(Deque{Any}(), Deque{Any}(), Deque{Any}()) 

按索引访问和更改


原则上,队列也可以用作按索引访问的容器。 通过索引检索和更改值由getindex()setindex!() 。 我们为队列实现了必要的方法。


 function Base.getindex(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else return next[1] end end function Base.setindex!(deq::Deque, val, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.data = val return val end end 

将插入元素的功能添加到任意位置insert!()并从任意位置删除元素也很有用:


 function Base.insert!(deq::Deque{T}, i::Integer, val) where T i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing push!(deq, val) else record = next[2] new_node = DequeNode{T}(val) new_node.prev, new_node.next = record.prev, record record.prev = record.prev.next = new_node deq end end function Base.deleteat!(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.prev.next, record.next.prev = record.next, record.prev end end 

其他


如前所述,标准库的许多函数都是使用迭代器编写的,这使它们可以自动使用定义了iterate()函数的任何类型。 特别是,可以使用诸如map()collect()函数以及诸如sum()minimum() sum()归约方法。
例如,可以这样编写查找队列长度的函数:


 Base.length(deq::Deque) = sum(x->1, deq) julia> length(Deque(1, 1, 2, 3, 5, 8, 13)) 7 

结论


如您所见,在Julia中创建自己的数据类型并使用它来组织方便的工作非常简单。 由于在标准库中使用了通用编程,因此通常可以定义几个基本功能,并且许多从属功能将自动运行。 因此,通过定义push!(container, element)以添加单个元素,您可以发现push!(container, elements...)将可以添加任意数量的参数。 定义了迭代器后,通常,我们会自动获取标准库的所有功能以用于抽象容器。


骇客入侵!

Source: https://habr.com/ru/post/zh-CN463857/


All Articles