介面
Julia 中許多功能和可擴充性來自非正式介面的集合。透過擴充特定方法來處理自訂類型,該類型的物件不僅會接收那些功能,而且還可以在撰寫為通用建構在那些行為之上的其他方法中使用。
反覆運算
必要方法 | 簡要說明 | |
---|---|---|
iterate(iter) | 傳回第一個項目和初始狀態的元組,或如果為空則傳回 nothing | |
iterate(iter, state) | 傳回下一項和下一個狀態的元組,或如果沒有剩餘項目,則傳回nothing | |
重要的選用方法 | 預設定義 | 簡要說明 |
Base.IteratorSize(IterType) | Base.HasLength() | 適當的Base.HasLength() 、Base.HasShape{N}() 、Base.IsInfinite() 或Base.SizeUnknown() 之一 |
Base.IteratorEltype(IterType) | Base.HasEltype() | 適當的Base.EltypeUnknown() 或Base.HasEltype() 之一 |
eltype(IterType) | Any | iterate() 傳回的元組第一個項目的類型 |
length(iter) | (未定義) | 項目的數量(如果已知) |
size(iter, [dim]) | (未定義) | 每個維度中項目的數量(如果已知) |
Base.isdone(iter[, state]) | 遺失 | 迭代器完成的快速路徑提示。應為有狀態迭代器定義,否則isempty(iter) 可能會呼叫iterate(iter[, state]) 並變更迭代器。 |
IteratorSize(IterType) 傳回的值 | 必要的方法 |
---|---|
Base.HasLength() | length(iter) |
Base.HasShape{N}() | length(iter) 和size(iter, [dim]) |
Base.IsInfinite() | (無) |
Base.SizeUnknown() | (無) |
IteratorEltype(IterType) 傳回的值 | 必要的方法 |
---|---|
Base.HasEltype() | eltype(IterType) |
Base.EltypeUnknown() | (無) |
順序迭代由iterate
函數實作。Julia 迭代器不會在迭代物件時變更物件,而是從物件外部追蹤迭代狀態。iterate 的傳回值永遠是值和狀態的元組,或如果沒有剩餘元素,則傳回nothing
。狀態物件會在下次迭代傳回 iterate 函數,通常被視為可迭代物件的實作細節。
定義此函數的任何物件都是可迭代的,且可用於許多依賴於迭代的函數中。由於語法
for item in iter # or "for item = iter"
# body
end
轉換為
next = iterate(iter)
while next !== nothing
(item, state) = next
# body
next = iterate(iter, state)
end
一個簡單的範例是具有定義長度的平方數可迭代序列
julia> struct Squares
count::Int
end
julia> Base.iterate(S::Squares, state=1) = state > S.count ? nothing : (state*state, state+1)
僅使用iterate
定義,Squares
類型已經相當強大。我們可以迭代所有元素
julia> for item in Squares(7)
println(item)
end
1
4
9
16
25
36
49
我們可以使用許多與可迭代物件一起運作的內建方法,例如in
或sum
julia> 25 in Squares(10)
true
julia> sum(Squares(100))
338350
我們可以延伸更多方法,以提供 Julia 有關此可迭代集合的更多資訊。我們知道Squares
序列中的元素將永遠是Int
。透過延伸eltype
方法,我們可以將該資訊提供給 Julia,並協助它在更複雜的方法中建立更專業的程式碼。我們也知道序列中的元素數量,因此我們也可以延伸length
julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type
julia> Base.length(S::Squares) = S.count
現在,當我們要求 Julia 將所有元素collect
到陣列中時,它可以預先配置正確大小的Vector{Int}
,而不是天真地將每個元素push!
到Vector{Any}
中
julia> collect(Squares(4))
4-element Vector{Int64}:
1
4
9
16
雖然我們可以依賴於一般實作,但我們也可以延伸特定方法,在這些方法中我們知道有一個更簡單的演算法。例如,有一個公式可以計算平方和,因此我們可以使用效能更高的解法來覆寫一般迭代版本
julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)
julia> sum(Squares(1803))
1955361914
這是整個 Julia Base 中非常常見的模式:一組小型的必要方法定義一個非正式介面,可啟用許多更花俏的行為。在某些情況下,當類型知道可以在其特定情況下使用更有效率的演算法時,就會希望進一步專業化這些額外行為。
在反向順序中反覆運算集合時,通常允許反覆運算 Iterators.reverse(iterator)
來反覆運算。然而,若要實際支援反向順序反覆運算,反覆運算類型 T
需要為 Iterators.Reverse{T}
實作 iterate
。(給定 r::Iterators.Reverse{T}
,類型為 T
的底層反覆運算為 r.itr
。)在我們的 Squares
範例中,我們將實作 Iterators.Reverse{Squares}
方法
julia> Base.iterate(rS::Iterators.Reverse{Squares}, state=rS.itr.count) = state < 1 ? nothing : (state*state, state-1)
julia> collect(Iterators.reverse(Squares(4)))
4-element Vector{Int64}:
16
9
4
1
索引
要實作的方法 | 簡要說明 |
---|---|
getindex(X, i) | X[i] ,索引元素存取 |
setindex!(X, v, i) | X[i] = v ,索引指派 |
firstindex(X) | 第一個索引,用於 X[begin] |
lastindex(X) | 最後一個索引,用於 X[end] |
對於上述的 Squares
可迭代物件,我們可以輕鬆地透過平方計算序列的第 i
個元素。我們可以將此公開為索引表達式 S[i]
。若要選擇此行為,Squares
只需要定義 getindex
julia> function Base.getindex(S::Squares, i::Int)
1 <= i <= S.count || throw(BoundsError(S, i))
return i*i
end
julia> Squares(100)[23]
529
此外,若要支援語法 S[begin]
和 S[end]
,我們必須定義 firstindex
和 lastindex
,分別指定第一個和最後一個有效的索引
julia> Base.firstindex(S::Squares) = 1
julia> Base.lastindex(S::Squares) = length(S)
julia> Squares(23)[end]
529
例如,對於多維度的 begin
/end
索引,例如 a[3, begin, 7]
,您應該定義 firstindex(a, dim)
和 lastindex(a, dim)
(分別預設為對 axes(a, dim)
呼叫 first
和 last
)。
不過,請注意,上述僅定義了具有單個整數索引的 getindex
。使用非 Int
進行索引會引發 MethodError
,表示沒有匹配的方法。為了支援使用 Int
範圍或向量的索引,必須撰寫個別的方法
julia> Base.getindex(S::Squares, i::Number) = S[convert(Int, i)]
julia> Base.getindex(S::Squares, I) = [S[i] for i in I]
julia> Squares(10)[[3,4.,5]]
3-element Vector{Int64}:
9
16
25
雖然這開始支援更多 內建類型支援的索引運算,但仍有許多行為遺漏。隨著我們新增行為,這個 Squares
序列看起來越來越像一個向量。我們可以正式將它定義為 AbstractArray
的子類型,而不是自己定義所有這些行為。
抽象陣列
要實作的方法 | 簡要說明 | |
---|---|---|
size(A) | 傳回包含 A 維度的元組 | |
getindex(A, i::Int) | (如果為 IndexLinear ) 線性標量索引 | |
getindex(A, I::Vararg{Int, N}) | (如果為 IndexCartesian ,其中 N = ndims(A) ) N 維標量索引 | |
選用方法 | 預設定義 | 簡要說明 |
IndexStyle(::Type) | IndexCartesian() | 傳回 IndexLinear() 或 IndexCartesian() 。請參閱以下說明。 |
setindex!(A, v, i::Int) | (如果為 IndexLinear ) 標量索引指定 | |
setindex!(A, v, I::Vararg{Int, N}) | (如果為 IndexCartesian ,其中 N = ndims(A) ) N 維標量索引指定 | |
getindex(A, I...) | 根據標量 getindex 定義 | 多維和非標量索引 |
setindex!(A, X, I...) | 根據標量 setindex! 定義 | 多維和非標量索引指定 |
iterate | 根據標量 getindex 定義 | 反覆運算 |
length(A) | prod(size(A)) | 元素數量 |
similar(A) | similar(A, eltype(A), size(A)) | 傳回一個可變陣列,具有相同的形狀和元素類型 |
similar(A, ::Type{S}) | similar(A, S, size(A)) | 傳回一個可變陣列,具有相同的形狀和指定的元素類型 |
similar(A, dims::Dims) | similar(A, eltype(A), dims) | 傳回一個可變陣列,具有相同的元素類型和大小 dims |
similar(A, ::Type{S}, dims::Dims) | Array{S}(undef, dims) | 傳回一個可變陣列,具有指定的元素類型和大小 |
非傳統索引 | 預設定義 | 簡要說明 |
axes(A) | map(OneTo, size(A)) | 傳回一個有效的索引 AbstractUnitRange{<:Integer} 的元組。軸應該是它們自己的軸,也就是說 axes.(axes(A),1) == axes(A) 應該成立。 |
similar(A, ::Type{S}, inds) | similar(A, S, Base.to_shape(inds)) | 傳回一個可變陣列,具有指定的索引 inds (見下方) |
similar(T::Union{Type,Function}, inds) | T(Base.to_shape(inds)) | 傳回一個類似於 T 的陣列,具有指定的索引 inds (見下方) |
如果一個類型被定義為 AbstractArray
的子類型,它將繼承一組非常豐富的行為,包括建立在單元素存取之上的迭代和多維索引。請參閱 陣列手冊頁面 和 Julia Base 部分 以取得更多支援的方法。
定義 AbstractArray
子類型的關鍵部分是 IndexStyle
。由於索引是陣列中如此重要的部分,而且經常出現在熱迴圈中,因此讓索引和索引指定盡可能有效率非常重要。陣列資料結構通常以兩種方式之一定義:它最有效率地使用一個索引(線性索引)存取其元素,或者它本質上使用為每個維度指定的索引存取元素。這兩種方式被 Julia 識別為 IndexLinear()
和 IndexCartesian()
。將線性索引轉換為多重索引子腳本通常非常昂貴,因此這提供了一種基於特質的機制,以針對所有陣列類型啟用有效率的通用程式碼。
此區別決定類型必須定義哪些標量索引方法。IndexLinear()
陣列很簡單:只需定義 getindex(A::ArrayType, i::Int)
。當陣列隨後使用多維度索引集進行索引時,後備 getindex(A::AbstractArray, I...)
會有效率地將索引轉換為一個線性索引,然後呼叫上述方法。另一方面,IndexCartesian()
陣列需要為每個支援的維度定義方法,其具有 ndims(A)
Int
索引。例如,SparseArrays
標準函式庫模組中的 SparseMatrixCSC
僅支援兩個維度,因此它僅定義 getindex(A::SparseMatrixCSC, i::Int, j::Int)
。setindex!
也是如此。
回到上述的平方序列,我們可以改將其定義為 AbstractArray{Int, 1}
的子類型
julia> struct SquaresVector <: AbstractArray{Int, 1}
count::Int
end
julia> Base.size(S::SquaresVector) = (S.count,)
julia> Base.IndexStyle(::Type{<:SquaresVector}) = IndexLinear()
julia> Base.getindex(S::SquaresVector, i::Int) = i*i
請注意,指定 AbstractArray
的兩個參數非常重要;第一個定義 eltype
,第二個定義 ndims
。該超類型和這三個方法是 SquaresVector
成為可迭代、可索引且完全功能的陣列所需的全部
julia> s = SquaresVector(4)
4-element SquaresVector:
1
4
9
16
julia> s[s .> 8]
2-element Vector{Int64}:
9
16
julia> s + s
4-element Vector{Int64}:
2
8
18
32
julia> sin.(s)
4-element Vector{Float64}:
0.8414709848078965
-0.7568024953079282
0.4121184852417566
-0.2879033166650653
作為一個更複雜的範例,讓我們在 Dict
的基礎上定義我們自己的玩具 N 維稀疏陣列類型
julia> struct SparseArray{T,N} <: AbstractArray{T,N}
data::Dict{NTuple{N,Int}, T}
dims::NTuple{N,Int}
end
julia> SparseArray(::Type{T}, dims::Int...) where {T} = SparseArray(T, dims);
julia> SparseArray(::Type{T}, dims::NTuple{N,Int}) where {T,N} = SparseArray{T,N}(Dict{NTuple{N,Int}, T}(), dims);
julia> Base.size(A::SparseArray) = A.dims
julia> Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where {T} = SparseArray(T, dims)
julia> Base.getindex(A::SparseArray{T,N}, I::Vararg{Int,N}) where {T,N} = get(A.data, I, zero(T))
julia> Base.setindex!(A::SparseArray{T,N}, v, I::Vararg{Int,N}) where {T,N} = (A.data[I] = v)
請注意,這是一個 IndexCartesian
陣列,因此我們必須手動定義 getindex
和 setindex!
在陣列的維度。與 SquaresVector
不同,我們能夠定義 setindex!
,因此我們可以變異陣列
julia> A = SparseArray(Float64, 3, 3)
3×3 SparseArray{Float64, 2}:
0.0 0.0 0.0
0.0 0.0 0.0
0.0 0.0 0.0
julia> fill!(A, 2)
3×3 SparseArray{Float64, 2}:
2.0 2.0 2.0
2.0 2.0 2.0
2.0 2.0 2.0
julia> A[:] = 1:length(A); A
3×3 SparseArray{Float64, 2}:
1.0 4.0 7.0
2.0 5.0 8.0
3.0 6.0 9.0
對 AbstractArray
進行索引的結果本身可以是一個陣列(例如,當按 AbstractRange
進行索引時)。AbstractArray
後備方法使用 similar
分配適當大小和元素類型的 Array
,該 Array
使用上述描述的基本索引方法填入。但是,在實作陣列封裝器時,您通常希望結果也能被封裝
julia> A[1:2,:]
2×3 SparseArray{Float64, 2}:
1.0 4.0 7.0
2.0 5.0 8.0
在此範例中,透過定義 Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where T
來建立適當的包裝陣列,即可完成此操作。(請注意,儘管 similar
支援 1 個和 2 個引數形式,但大多數情況下,您只需要專門化 3 個引數形式即可。)為使此方法運作,SparseArray
必須是可變的(支援 setindex!
)。為 SparseArray
定義 similar
、getindex
和 setindex!
也能讓您 copy
陣列
julia> copy(A)
3×3 SparseArray{Float64, 2}:
1.0 4.0 7.0
2.0 5.0 8.0
3.0 6.0 9.0
除了上述所有可迭代和可索引的方法外,這些類型還能互相互動,並使用 Julia Base 中為 AbstractArrays
定義的大部分方法
julia> A[SquaresVector(3)]
3-element SparseArray{Float64, 1}:
1.0
4.0
9.0
julia> sum(A)
45.0
如果您正在定義允許非傳統索引(從 1 以外的數字開始的索引)的陣列類型,您應該專門化 axes
。您也應該專門化 similar
,以便 dims
引數(通常是 Dims
大小元組)可以接受 AbstractUnitRange
物件,可能是您自己設計的範圍類型 Ind
。如需更多資訊,請參閱 具有自訂索引的陣列。
跨步陣列
要實作的方法 | 簡要說明 | |
---|---|---|
strides(A) | 傳回每個維度中相鄰元素之間在記憶體中的距離(以元素數為單位),作為元組。如果 A 是 AbstractArray{T,0} ,這應該會傳回一個空元組。 | |
Base.unsafe_convert(::Type{Ptr{T}}, A) | 傳回陣列的原生位址。 | |
Base.elsize(::Type{<:A}) | 傳回陣列中連續元素之間的跨步。 | |
選用方法 | 預設定義 | 簡要說明 |
stride(A, i::Int) | strides(A)[i] | 傳回維度 k 中相鄰元素之間在記憶體中的距離(以元素數為單位)。 |
跨步陣列是 AbstractArray
的子類型,其條目以固定的跨步儲存在記憶體中。只要陣列的元素類型與 BLAS 相容,跨步陣列就能利用 BLAS 和 LAPACK 常式,以執行更有效率的線性代數常式。使用者定義跨步陣列的一個典型範例,就是將標準 Array
包裝在額外的結構中。
警告:如果底層儲存實際上並非跨步,請勿實作這些方法,因為這可能會導致不正確的結果或區段錯誤。
以下是幾個範例,用來展示哪些類型的陣列是跨步的,哪些不是
1:5 # not strided (there is no storage associated with this array.)
Vector(1:5) # is strided with strides (1,)
A = [1 5; 2 6; 3 7; 4 8] # is strided with strides (1,4)
V = view(A, 1:2, :) # is strided with strides (1,4)
V = view(A, 1:2:3, 1:2) # is strided with strides (2,4)
V = view(A, [1,2,4], :) # is not strided, as the spacing between rows is not fixed.
自訂廣播
要實作的方法 | 簡要說明 |
---|---|
Base.BroadcastStyle(::Type{SrcType}) = SrcStyle() | SrcType 的廣播行為 |
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType}) | 輸出容器的配置 |
選用方法 | |
Base.BroadcastStyle(::Style1, ::Style2) = Style12() | 混合樣式的優先順序規則 |
Base.axes(x) | x 的索引宣告,根據 axes(x) 。 |
Base.broadcastable(x) | 將 x 轉換為具有 axes 且支援索引的物件 |
繞過預設機制 | |
Base.copy(bc::Broadcasted{DestStyle}) | broadcast 的自訂實作 |
Base.copyto!(dest, bc::Broadcasted{DestStyle}) | broadcast! 的自訂實作,專門針對 DestStyle |
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing}) | broadcast! 的自訂實作,專門針對 DestType |
Base.Broadcast.broadcasted(f, args...) | 覆寫融合運算式中的預設延遲行為 |
Base.Broadcast.instantiate(bc::Broadcasted{DestStyle}) | 覆寫延遲廣播的軸線計算 |
廣播是由對 broadcast
或 broadcast!
的明確呼叫觸發,或由「點」運算(例如 A .+ b
或 f.(x, y)
)隱含觸發。任何具有 axes
且支援索引的物件都可以作為廣播中的引數,而預設情況下,結果會儲存在 Array
中。這個基本架構可以在三個主要方面進行擴充
- 確保所有引數都支援廣播
- 針對給定的引數集選擇適當的輸出陣列
- 針對給定的引數集選擇有效的實作
並非所有類型都支援 axes
和索引,但許多類型允許在廣播中使用。針對每個要廣播的引數呼叫 Base.broadcastable
函數,允許它傳回支援 axes
和索引的不同內容。預設情況下,這是所有 AbstractArray
和 Number
的身分函數,它們已支援 axes
和索引。
如果一個類型打算作為「0 維度純量」(單一物件)而不是廣播的容器,則應定義下列方法
Base.broadcastable(o::MyType) = Ref(o)
將引數封裝在 0 維度 Ref
容器中。例如,此類封裝方法定義給類型本身、函數、特殊單例,例如 missing
和 nothing
,以及日期。
自訂的陣列類型可以專門化 Base.broadcastable
來定義其形狀,但它們應遵循慣例,即 collect(Base.broadcastable(x)) == collect(x)
。一個值得注意的例外是 AbstractString
;字串為特殊情況,即使它們是其字元的可迭代集合,但它們在廣播目的中仍表現為純量(請參閱 字串 以取得更多資訊)。
後續兩個步驟(選擇輸出陣列和實作)取決於為一組給定的引數確定單一解答。廣播必須採用所有不同類型的引數,並將它們簡化為只有一個輸出陣列和一個實作。廣播將這個單一解答稱為「樣式」。每個可廣播物件都有其自己的偏好樣式,並使用類似提升的系統將這些樣式組合成單一解答,也就是「目標樣式」。
廣播樣式
Base.BroadcastStyle
是所有廣播樣式衍生的抽象類型。當用作函數時,它有兩種可能的形式,單元(單一引數)和二元。單元變體表示您打算實作特定的廣播行為和/或輸出類型,並且不希望依賴預設後備 Broadcast.DefaultArrayStyle
。
若要覆寫這些預設值,您可以為您的物件定義自訂的 BroadcastStyle
struct MyStyle <: Broadcast.BroadcastStyle end
Base.BroadcastStyle(::Type{<:MyType}) = MyStyle()
在某些情況下,不必定義 MyStyle
會比較方便,這種情況下您可以利用其中一個一般廣播包裝器
Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}()
可用於任意類型。- 如果
MyType
是AbstractArray
,則建議使用Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}()
。 - 對於僅支援特定維度的
AbstractArrays
,請建立Broadcast.AbstractArrayStyle{N}
的子類型(請見下方)。
當您的廣播運算涉及多個引數時,個別引數樣式會結合起來,以確定單一的 DestStyle
,用於控制輸出容器的類型。如需更多詳細資訊,請參閱 下方。
選擇適當的輸出陣列
每項廣播運算都會計算廣播樣式,以允許調度和專門化。結果陣列的實際配置由 similar
處理,並使用廣播物件作為其第一個引數。
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})
備用定義為
similar(bc::Broadcasted{DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType} =
similar(Array{ElType}, axes(bc))
不過,如有需要,你可以針對任何或所有這些參數進行專門化。最後一個參數 bc
是(潛在融合)廣播運算的惰性表示,一個 Broadcasted
物件。對於這些目的,包裝器最重要的欄位是 f
和 args
,分別描述函數和參數清單。請注意,參數清單可以(而且通常會)包含其他巢狀 Broadcasted
包裝器。
舉一個完整的範例,假設你建立了一個類型 ArrayAndChar
,用來儲存陣列和單一字元
struct ArrayAndChar{T,N} <: AbstractArray{T,N}
data::Array{T,N}
char::Char
end
Base.size(A::ArrayAndChar) = size(A.data)
Base.getindex(A::ArrayAndChar{T,N}, inds::Vararg{Int,N}) where {T,N} = A.data[inds...]
Base.setindex!(A::ArrayAndChar{T,N}, val, inds::Vararg{Int,N}) where {T,N} = A.data[inds...] = val
Base.showarg(io::IO, A::ArrayAndChar, toplevel) = print(io, typeof(A), " with char '", A.char, "'")
你可能希望廣播保留 char
「元資料」。我們首先定義
Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()
這表示我們也必須定義一個對應的 similar
方法
function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ArrayAndChar}}, ::Type{ElType}) where ElType
# Scan the inputs for the ArrayAndChar:
A = find_aac(bc)
# Use the char field of A to create the output
ArrayAndChar(similar(Array{ElType}, axes(bc)), A.char)
end
"`A = find_aac(As)` returns the first ArrayAndChar among the arguments."
find_aac(bc::Base.Broadcast.Broadcasted) = find_aac(bc.args)
find_aac(args::Tuple) = find_aac(find_aac(args[1]), Base.tail(args))
find_aac(x) = x
find_aac(::Tuple{}) = nothing
find_aac(a::ArrayAndChar, rest) = a
find_aac(::Any, rest) = find_aac(rest)
根據這些定義,可以得到以下行為
julia> a = ArrayAndChar([1 2; 3 4], 'x')
2×2 ArrayAndChar{Int64, 2} with char 'x':
1 2
3 4
julia> a .+ 1
2×2 ArrayAndChar{Int64, 2} with char 'x':
2 3
4 5
julia> a .+ [5,10]
2×2 ArrayAndChar{Int64, 2} with char 'x':
6 7
13 14
使用自訂實作擴充廣播
一般而言,廣播運算會以一個延遲的 Broadcasted
容器表示,其中包含要套用的函數及其參數。這些參數本身可能是更巢狀的 Broadcasted
容器,形成一個要評估的大型表達式樹。Broadcasted
容器的巢狀樹是由隱含的點語法直接建構的;例如,5 .+ 2.*x
會暫時表示為 Broadcasted(+, 5, Broadcasted(*, 2, x))
。這對使用者來說是看不見的,因為它會立即透過呼叫 copy
來實現,但正是這個容器為自訂型別的作者提供了廣播可擴充性的基礎。內建的廣播機制會根據參數來決定結果型別和大小,分配它,然後最後將 Broadcasted
物件的實現複製到其中,並使用預設的 copyto!(::AbstractArray, ::Broadcasted)
方法。內建的後備 broadcast
和 broadcast!
方法會以類似的方式建構一個暫時性的 Broadcasted
運算表示,以便它們可以遵循相同的程式碼路徑。這允許自訂陣列實作提供自己的 copyto!
專門化,以自訂和最佳化廣播。這再次由計算出的廣播樣式決定。這是運算中如此重要的部分,因此它會儲存在 Broadcasted
型別的第一個型別參數中,允許進行分派和專門化。
對於某些類型,用於「融合」嵌套廣播層級運算的機器不可用,或以遞增方式執行會更有效率。在這種情況下,您可能需要或希望將 x .* (x .+ 1)
評估為 broadcast(*, x, broadcast(+, x, 1))
,其中在處理外部運算之前會評估內部運算。此類急切運算直接受到一點間接作用的支援;Julia 沒有直接建構 Broadcasted
物件,而是將融合的運算式 x .* (x .+ 1)
降低為 Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1))
。現在,預設情況下,broadcasted
只會呼叫 Broadcasted
建構函式來建立融合運算式樹的延遲表示法,但您可以選擇針對特定函式和引數組合覆寫它。
舉例來說,內建的 AbstractRange
物件使用此機器來最佳化廣播運算式的部分,這些部分可以純粹根據起點、步長和長度(或終點)急切評估,而不是計算每個單一元素。就像所有其他機器一樣,broadcasted
也會計算並公開其引數的組合廣播樣式,因此,您不必針對 broadcasted(f, args...)
進行專門化,而是可以針對任何樣式、函式和引數組合對 broadcasted(::DestStyle, f, args...)
進行專門化。
例如,下列定義支援範圍的否定
broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r))
延伸原地廣播
可以透過定義適當的 copyto!(dest, bc::Broadcasted)
方法來支援原地廣播。由於您可能想要針對 dest
或 bc
的特定子類型進行專門化,因此,為了避免套件之間的歧義,我們建議採用下列慣例。
如果您希望針對特定樣式 DestStyle
進行專門化,請定義一個方法,如下所示
copyto!(dest, bc::Broadcasted{DestStyle})
選擇性地,使用此表單,您也可以針對 dest
的類型進行專門化。
如果您反而想要針對目標類型 DestType
進行專門化,而不針對 DestStyle
進行專門化,那麼您應該定義一個具有下列簽章的方法
copyto!(dest::DestType, bc::Broadcasted{Nothing})
這利用了 copyto!
的備用實作,將包裝器轉換為 Broadcasted{Nothing}
。因此,針對 DestType
的特化優先權低於針對 DestStyle
特化的函式。
類似地,您可以使用 copy(::Broadcasted)
函式完全覆寫非原位廣播。
處理 Broadcasted
物件
當然,為了實作這樣的 copy
或 copyto!
函式,您必須使用 Broadcasted
包裝器來計算每個元素。有兩種主要方法可以做到這一點
Broadcast.flatten
將潛在的巢狀運算重新計算為單一函式和引數的平面清單。您必須自行實作廣播形狀規則,但在某些情況下這可能會有用。- 迭代
axes(::Broadcasted)
的CartesianIndices
,並使用索引和產生的CartesianIndex
物件來計算結果。
撰寫二元廣播規則
優先權規則由二元 BroadcastStyle
呼叫定義
Base.BroadcastStyle(::Style1, ::Style2) = Style12()
其中 Style12
是您想要為涉及 Style1
和 Style2
引數的輸出選擇的 BroadcastStyle
。例如,
Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0}) = Broadcast.Style{Tuple}()
表示 Tuple
「勝過」零維陣列(輸出容器將會是元組)。值得注意的是,您不需要(也不應該)定義此呼叫的兩個引數順序;定義一個就足夠了,無論使用者以何種順序提供引數。
對於 AbstractArray
類型,定義 BroadcastStyle
會取代後備選項,Broadcast.DefaultArrayStyle
。DefaultArrayStyle
和抽象超類型 AbstractArrayStyle
會將維度儲存為類型參數,以支援具有固定維度需求的特殊陣列類型。
DefaultArrayStyle
會「輸給」任何其他已定義的 AbstractArrayStyle
,原因在於下列方法
BroadcastStyle(a::AbstractArrayStyle{Any}, ::DefaultArrayStyle) = a
BroadcastStyle(a::AbstractArrayStyle{N}, ::DefaultArrayStyle{N}) where N = a
BroadcastStyle(a::AbstractArrayStyle{M}, ::DefaultArrayStyle{N}) where {M,N} =
typeof(a)(Val(max(M, N)))
除非您想為兩個或多個非 DefaultArrayStyle
類型建立優先順序,否則您不需要撰寫二進位 BroadcastStyle
規則。
如果您的陣列類型確實有固定維度需求,則您應該將 AbstractArrayStyle
子類型化。例如,稀疏陣列程式碼有下列定義
struct SparseVecStyle <: Broadcast.AbstractArrayStyle{1} end
struct SparseMatStyle <: Broadcast.AbstractArrayStyle{2} end
Base.BroadcastStyle(::Type{<:SparseVector}) = SparseVecStyle()
Base.BroadcastStyle(::Type{<:SparseMatrixCSC}) = SparseMatStyle()
每當您將 AbstractArrayStyle
子類型化時,您也需要定義結合維度的規則,方法是為您的樣式建立一個建構函數,該函數會採用 Val(N)
參數。例如
SparseVecStyle(::Val{0}) = SparseVecStyle()
SparseVecStyle(::Val{1}) = SparseVecStyle()
SparseVecStyle(::Val{2}) = SparseMatStyle()
SparseVecStyle(::Val{N}) where N = Broadcast.DefaultArrayStyle{N}()
這些規則表示將 SparseVecStyle
與 0 或 1 維陣列結合會產生另一個 SparseVecStyle
,將其與 2 維陣列結合會產生 SparseMatStyle
,而任何更高維度的陣列都會回退到密集任意維度架構。這些規則允許廣播保留稀疏表示,以進行會產生一或二維輸出的運算,但會為任何其他維度產生 Array
。
實例屬性
要實作的方法 | 預設定義 | 簡要說明 |
---|---|---|
propertynames(x::ObjType, private::Bool=false) | fieldnames(typeof(x)) | 傳回物件 x 的屬性 (x.property ) 的元組。如果 private=true ,也傳回預計保持為私有的屬性名稱 |
getproperty(x::ObjType, s::Symbol) | getfield(x, s) | 傳回 x 的屬性 s 。x.s 會呼叫 getproperty(x, :s) 。 |
setproperty!(x::ObjType, s::Symbol, v) | setfield!(x, s, v) | 將 x 的屬性 s 設定為 v 。x.s = v 會呼叫 setproperty!(x, :s, v) 。應該傳回 v 。 |
有時,會希望變更最終使用者與物件欄位的互動方式。與其授予直接存取類型欄位的權限,可以使用超載 object.field
來提供使用者與程式碼之間的額外抽象層。屬性是使用者看到物件的內容,欄位則是物件實際上的內容。
預設情況下,屬性和欄位相同。不過,可以變更此行為。例如,採用 極座標 來表示平面上的一個點
julia> mutable struct Point
r::Float64
ϕ::Float64
end
julia> p = Point(7.0, pi/4)
Point(7.0, 0.7853981633974483)
如上表所述,點存取 p.r
與 getproperty(p, :r)
相同,而 getproperty(p, :r)
預設與 getfield(p, :r)
相同
julia> propertynames(p)
(:r, :ϕ)
julia> getproperty(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)
julia> p.r, p.ϕ
(7.0, 0.7853981633974483)
julia> getfield(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)
不過,我們可能希望使用者不知道 Point
將座標儲存在 r
和 ϕ
(欄位) 中,而是與 x
和 y
(屬性) 進行互動。第一欄中的方法可以定義為新增新功能
julia> Base.propertynames(::Point, private::Bool=false) = private ? (:x, :y, :r, :ϕ) : (:x, :y)
julia> function Base.getproperty(p::Point, s::Symbol)
if s === :x
return getfield(p, :r) * cos(getfield(p, :ϕ))
elseif s === :y
return getfield(p, :r) * sin(getfield(p, :ϕ))
else
# This allows accessing fields with p.r and p.ϕ
return getfield(p, s)
end
end
julia> function Base.setproperty!(p::Point, s::Symbol, f)
if s === :x
y = p.y
setfield!(p, :r, sqrt(f^2 + y^2))
setfield!(p, :ϕ, atan(y, f))
return f
elseif s === :y
x = p.x
setfield!(p, :r, sqrt(x^2 + f^2))
setfield!(p, :ϕ, atan(f, x))
return f
else
# This allow modifying fields with p.r and p.ϕ
return setfield!(p, s, f)
end
end
很重要的一點是,getfield
和 setfield
必須在 getproperty
和 setproperty!
內部使用,而不是使用點語法,因為點語法會使函式遞迴,這可能會導致類型推論問題。現在,我們可以試用新功能
julia> propertynames(p)
(:x, :y)
julia> p.x
4.949747468305833
julia> p.y = 4.0
4.0
julia> p.r
6.363961030678928
最後,值得注意的是,在 Julia 中很少會新增此類實例屬性,而且通常只有在有充分理由時才這麼做。