Интерфейсы
Большая часть возможностей и расширяемости в Julia основана на наборе неформальных интерфейсов. Расширив несколько специфических методов для пользовательского типа, объекты этого типа не только получают эти функциональности, но и могут использоваться в других методах, написанных для обобщённого построения на этих поведених.
Итерация
| Необходимые методы | Краткое описание | |
|---|---|---|
start(iter) | Возвращает начальное состояние итерации | |
next(iter, state) | Возвращает текущий элемент и следующее состояние | |
done(iter, state) | Проверяет, есть ли ещё элементы | |
| Важные необязательные методы | Значение по умолчанию | Краткое описание |
iteratorsize(IterType) | HasLength() | Один из HasLength(), HasShape(), IsInfinite(), или SizeUnknown() в соответствии с ситуацией |
iteratoreltype(IterType) | HasEltype() | Либо EltypeUnknown(), либо HasEltype() в соответствии с ситуацией |
eltype(IterType) | Any | Тип элементов, возвращаемых next()
|
length(iter) | (неопределённо) | Количество элементов, если известно |
size(iter, [dim...]) | (неопределённо) | Количество элементов в каждом измерении, если известно |
Значение, возвращаемое iteratorsize(IterType)
| Необходимые методы |
|---|---|
HasLength() | length(iter) |
HasShape() |
length(iter) и size(iter, [dim...])
|
IsInfinite() | (нет) |
SizeUnknown() | (нет) |
Значение, возвращаемое iteratoreltype(IterType)
| Необходимые методы |
|---|---|
HasEltype() | eltype(IterType) |
EltypeUnknown() | (нет) |
Последовательная итерация реализуется методами start(), done(), и next(). Вместо изменения объектов во время итерации, Julia предоставляет эти три метода для отслеживания состояния итерации вне объекта.
Метод start(iter) возвращает начальное состояние для итерируемого объекта iter. Это состояние передаётся методу done(iter, state), который проверяет, остались ли ещё элементы, и методу next(iter, state), который возвращает кортеж, содержащий текущий элемент и обновлённое state. Объект state может быть любым и обычно считается деталью реализации, приватной для итерируемого объекта.
Любой объект, определяющий эти три метода, является итерируемым и может быть использован во многих функциях, зависящих от итерации. Его также можно использовать напрямую в цикле for, так как синтаксис:
for i in iter # or "for i = iter"
# body
end
преобразуется в:
state = start(iter)
while !done(iter, state)
(i, state) = next(iter, state)
# body
end
Простой пример — итерируемая последовательность квадратных чисел с определённой длиной:
julia> immutable Squares
count::Int
end
Base.start(::Squares) = 1
Base.next(S::Squares, state) = (state*state, state+1)
Base.done(S::Squares, state) = state > S.count;
Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type
Base.length(S::Squares) = S.count;
Только с определениями start, next, и done тип Squares уже достаточно мощный. Мы можем перебирать все элементы:
julia> for i in Squares(7)
println(i)
end
1
4
9
16
25
36
49
Мы можем использовать многие встроенные методы, работающие с итерируемыми объектами, такие как in(), mean() и std():
julia> 25 in Squares(10) true julia> mean(Squares(100)), std(Squares(100)) (3383.5,3024.355854282583)
Есть ещё несколько методов, которые можно расширить, чтобы предоставить Julia больше информации об этой итерируемой коллекции. Мы знаем, что элементы в последовательности Squares всегда будут Int. Расширив метод eltype(), мы можем предоставить эту информацию Julia и помочь ей генерировать более специализированный код в более сложных методах. Мы также знаем количество элементов в нашей последовательности, поэтому мы можем расширить и метод length().
Теперь, когда мы просим Julia собрать все элементы в массив, она может предварительно выделить массив нужного размера вместо слепого добавления каждого элемента в массив:
julia> collect(Squares(100))' # transposed to save space
1×100 Array{Int64,2}:
1 4 9 16 25 36 49 64 81 100 … 9025 9216 9409 9604 9801 10000
Хотя мы можем полагаться на общие реализации, мы также можем расширить специфические методы, где известен более простой алгоритм. Например, есть формула для вычисления суммы квадратов, поэтому мы можем переопределить общий итеративный вариант более производительным решением:
julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)
sum(Squares(1803))
1955361914
Это очень распространённый паттерн в стандартной библиотеке Julia: небольшой набор необходимых методов определяет неформальный интерфейс, который позволяет множество более сложных поведений. В некоторых случаях типы могут дополнительно специализировать эти дополнительные поведения, когда в их конкретном случае может использоваться более эффективный алгоритм.
Индексирование
| Методы для реализации | Краткое описание |
|---|---|
getindex(X, i) | Доступ к элементу по индексу |
setindex!(X, v, i) | Присвоение значения элементу по индексу |
endof(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
Squares(100)[23]
529
Кроме того, для поддержки синтаксиса S[end], мы должны определить endof() для указания последнего допустимого индекса:
julia> Base.endof(S::Squares) = length(S)
Squares(23)[end]
529
Однако обратите внимание, что выше определено getindex() только с одним целочисленным индексом. Индексирование с чем-либо кроме целочисленного индекса Int вызовет ошибку MethodError, говорящую, что соответствующий метод не найден. Для поддержки индексирования с диапазонами или векторами целых чисел необходимо написать отдельные методы:
julia> Base.getindex(S::Squares, i::Number) = S[convert(Int, i)]
Base.getindex(S::Squares, I) = [S[i] for i in I]
Squares(10)[[3,4.,5]]
3-element Array{Int64,1}:
9
16
25
Хотя это начинает поддерживать больше операций индексирования, поддерживаемых некоторыми встроенными типами, всё ещё не хватает ряда поведений. Эта Squares последовательность начинает всё больше напоминать вектор по мере добавления в неё поведений. Вместо определения всех этих поведений самостоятельно, мы можем официально определить его как подтип AbstractArray.
Абстрактные массивы
| Методы для реализации | Краткое описание | |
|---|---|---|
size(A) | Возвращает кортеж, содержащий размеры A
| |
getindex(A, i::Int) | (если LinearFast) Линейное скалярное индексирование | |
getindex(A, I::Vararg{Int, N}) | (если LinearSlow, где N = ndims(A)) N-мерное скалярное индексирование | |
setindex!(A, v, i::Int) | (если LinearFast) Скалярное присвоение по индексу | |
setindex!(A, v, I::Vararg{Int, N}) | (если LinearSlow, где N = ndims(A)) N-мерное скалярное присвоение по индексу | |
| Необязательные методы | Значение по умолчанию | Краткое описание |
Base.linearindexing(::Type) | Base.LinearSlow() | Возвращает либо Base.LinearFast(), либо Base.LinearSlow(). См. описание ниже. |
getindex(A, I...) | определяется через скалярное getindex()
| Многомерное и нескалярное индексирование |
setindex!(A, I...) | определяется через скалярное setindex!()
| Многомерное и нескалярное присвоение по индексу |
start()/next()/done()
| определяется через скалярное 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::NTuple{Int}) | similar(A, eltype(A), dims) | Возвращает изменяемый массив с тем же типом элементов и размером dims
|
similar(A, ::Type{S}, dims::NTuple{Int}) | Array{S}(dims) | Возвращает изменяемый массив с указанным типом элементов и размером |
| Нестандартные индексы | Значение по умолчанию | Краткое описание |
indices(A) | map(OneTo, size(A)) | Возвращает множество допустимых индексов |
Base.similar(A, ::Type{S}, inds::NTuple{Ind}) | similar(A, S, Base.to_shape(inds)) | Возвращает изменяемый массив с указанными индексами inds (см. ниже) |
Base.similar(T::Union{Type,Function}, inds) | T(Base.to_shape(inds)) | Возвращает массив, аналогичный T с указанными индексами inds (см. ниже) |
Если тип определен как подтип AbstractArray, он наследует очень большой набор богатых возможностей, включая итерацию и многомерную индексацию, построенные на основе доступа к одному элементу. См. документацию массивов и раздел стандартной библиотеки для получения более подробной информации о поддерживаемых методах.
Ключевой частью при определении подтипа AbstractArray является Base.linearindexing(). Поскольку индексация является такой важной частью массива и часто встречается в горячих циклах, важно сделать и индексацию, и присваивание по индексу максимально эффективными. Структуры данных массивов обычно определяются одним из двух способов: либо они наиболее эффективно обращаются к своим элементам с использованием только одного индекса (линейная индексация), либо они изначально обращаются к элементам с индексами, указанными для каждой размерности. Эти два способа идентифицируются Julia как Base.LinearFast() и Base.LinearSlow(). Преобразование линейного индекса в несколько индексных подстрок обычно очень дорого, поэтому это предоставляет механизм на основе свойств для обеспечения эффективного универсального кода для всех типов массивов.
Это различие определяет, какие скалярные методы индексации должен определять тип. LinearFast() массивы простые: просто определите getindex(A::ArrayType, i::Int). Когда массив затем индексируется набором многомерных индексов, getindex(A::AbstractArray, I...)() эффективно преобразует индексы в один линейный индекс и затем вызывает указанный выше метод. LinearSlow() массивы, с другой стороны, требуют определения методов для каждой поддерживаемой размерности с ndims(A) Int индексами. Например, встроенный тип SparseMatrixCSC поддерживает только две размерности, поэтому он просто определяет getindex(A::SparseMatrixCSC, i::Int, j::Int)(). То же самое относится и к setindex!().
Возвращаясь к последовательности квадратов из вышеизложенного, мы можем вместо этого определить её как подтип AbstractArray{Int, 1}:
julia> immutable SquaresVector <: AbstractArray{Int, 1}
count::Int
end
Base.size(S::SquaresVector) = (S.count,)
Base.linearindexing{T<:SquaresVector}(::Type{T}) = Base.LinearFast()
Base.getindex(S::SquaresVector, i::Int) = i*i;
Обратите внимание, что очень важно указать два параметра AbstractArray; первый определяет eltype(), а второй определяет ndims(). Этот супертип и эти три метода — все, что нужно для того, чтобы SquaresVector был итерируемым, индексируемым и полностью функциональным массивом:
julia> s = SquaresVector(7)
7-element SquaresVector:
1
4
9
16
25
36
49
julia> s[s .> 20]
3-element Array{Int64,1}:
25
36
49
julia> s \ rand(7,2)
1×2 Array{Float64,2}:
0.0151876 0.0179393
В качестве более сложного примера давайте определим наш собственный массив типа N-мерного разреженного массива, построенного поверх Dict:
julia> immutable SparseArray{T,N} <: AbstractArray{T,N}
data::Dict{NTuple{N,Int}, T}
dims::NTuple{N,Int}
end
SparseArray{T}(::Type{T}, dims::Int...) = SparseArray(T, dims)
SparseArray{T,N}(::Type{T}, dims::NTuple{N,Int}) = SparseArray{T,N}(Dict{NTuple{N,Int}, T}(), dims)
SparseArray{T,N}
julia> Base.size(A::SparseArray) = A.dims
Base.similar{T}(A::SparseArray, ::Type{T}, dims::Dims) = SparseArray(T, dims)
# Define scalar indexing and indexed assignment
Base.getindex{T,N}(A::SparseArray{T,N}, I::Vararg{Int,N}) = get(A.data, I, zero(T))
Base.setindex!{T,N}(A::SparseArray{T,N}, v, I::Vararg{Int,N}) = (A.data[I] = v)
Обратите внимание, что это LinearSlow массив, поэтому мы должны вручную определить 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> rand!(A)
3×3 SparseArray{Float64,2}:
0.28119 0.0203749 0.0769509
0.209472 0.287702 0.640396
0.251379 0.859512 0.873544
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 может сам быть массивом (например, при индексации по Range). AbstractArray методы обратной совместимости используют similar() для выделения Array соответствующего размера и типа элемента, который заполняется с использованием описанного выше основного метода индексации. Однако при реализации обёртки массива вы часто хотите, чтобы результат также был обернут:
julia> A[1:2,:]
2×3 SparseArray{Float64,2}:
1.0 4.0 7.0
2.0 5.0 8.0
В этом примере это достигается путем определения Base.similar{T}(A::SparseArray, ::Type{T}, dims::Dims) для создания соответствующего обернутого массива. (Обратите внимание, что хотя similar поддерживает 1- и 2-аргументные формы, в большинстве случаев вам нужно специализироваться только на 3-аргументной форме.) Для работы важно, чтобы SparseArray был изменяемым (поддерживал setindex!). similar() также используется для выделения результирующих массивов для арифметических операций над AbstractArrays, например:
julia> A + 4
3×3 SparseArray{Float64,2}:
5.0 8.0 11.0
6.0 9.0 12.0
7.0 10.0 13.0
В дополнение ко всем итерируемым и индексируемым методам из вышеизложенного, эти типы также могут взаимодействовать друг с другом и использовать все методы, определённые в стандартной библиотеке для AbstractArrays:
julia> A[SquaresVector(3)]
3-element SparseArray{Float64,1}:
1.0
4.0
9.0
julia> dot(A[:,1],A[:,2])
32.0
Если вы определяете тип массива, который допускает нетрадиционную индексацию (индексы, которые начинаются не с 1), вы должны специализировать indices. Вы также должны специализировать similar так, чтобы аргумент dims (обычно кортеж размеров Dims ) мог принимать AbstractUnitRange объекты, возможно, типы диапазонов Ind вашего собственного дизайна. Более подробную информацию можно найти в Массивы с пользовательскими индексами.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/interfaces/