Spec-Zone.ru › Julia 0.6

Интерфейсы

Большая часть возможностей и расширяемости в 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> struct Squares
           count::Int
       end

julia> Base.start(::Squares) = 1

julia> Base.next(S::Squares, state) = (state*state, state+1)

julia> Base.done(S::Squares, state) = state > S.count

julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type

julia> 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))
3383.5

julia> std(Squares(100))
3024.355854282583

Есть ещё несколько методов, которые мы можем расширить, чтобы предоставить Julia больше информации об этой итерируемой коллекции. Мы знаем, что элементы в последовательности Squares всегда будут Int. Расширив метод eltype(), мы можем предоставить эту информацию Julia и помочь ей создавать более специализированный код в более сложных методах. Мы также знаем количество элементов в нашей последовательности, поэтому можем расширить метод length().

Теперь, когда мы попросим Julia collect() все элементы в массив, она сможет предварительно выделить массив нужного размера вместо слепого push! каждого элемента в массив Vector{Any}:

julia> collect(Squares(10))' # transposed to save space
1×10 RowVector{Int64,Array{Int64,1}}:
 1  4  9  16  25  36  49  64  81  100

Хотя мы можем полагаться на общие реализации, мы также можем расширить определённые методы, когда знаем более простой алгоритм. Например, есть формула для вычисления суммы квадратов, поэтому мы можем переопределить общий итеративный метод более эффективным решением:

julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)

julia> sum(Squares(1803))
1955361914

Это очень распространённый шаблон во всей стандартной библиотеке Julia: небольшой набор необходимых методов определяет неформальный интерфейс, который позволяет использовать множество более сложных свойств. В некоторых случаях типы могут дополнительно специализировать эти дополнительные свойства, когда знают, что в их конкретном случае можно использовать более эффективный алгоритм.

Индексирование

Методы для реализации Краткое описание
getindex(X, i) X[i], доступ к элементу по индексу
setindex!(X, v, i) X[i] = v, присваивание значения по индексу
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

julia> Squares(100)[23]
529

Кроме того, чтобы поддерживать синтаксис S[end], необходимо определить endof() для указания последнего допустимого индекса:

julia> Base.endof(S::Squares) = length(S)

julia> Squares(23)[end]
529

Обратите внимание, что выше описано определение getindex() только для целого индекса. Индексирование с чем-либо, кроме целого индекса, вызовет ошибку MethodError о том, что подходящий метод не найден. Для поддержки индексирования с диапазонами или векторами целых чисел необходимо написать отдельные методы:

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 Array{Int64,1}:
  9
 16
 25

Хотя это начинает поддерживать больше операций индексирования, которые поддерживаются некоторыми встроенными типами, всё ещё не хватает нескольких свойств. Эта последовательность Squares начинает всё больше походить на вектор, поскольку мы добавили к ней свойства. Вместо того, чтобы определять все эти свойства сами, мы можем официально определить её как подтип AbstractArray.

Абстрактные массивы

Методы для реализации Краткое описание
size(A) Возвращает кортеж, содержащий размеры A
getindex(A, i::Int) (если IndexLinear) Линейная скалярная индексация
getindex(A, I::Vararg{Int, N}) (если IndexCartesian, где N = ndims(A)) N-мерная скалярная индексация
setindex!(A, v, i::Int) (если IndexLinear) Скалярное присваивание по индексу
setindex!(A, v, I::Vararg{Int, N}) (если IndexCartesian, где N = ndims(A)) N-мерное скалярное присваивание по индексу
Необязательные методы Определение по умолчанию Краткое описание
IndexStyle(::Type) IndexCartesian() Возвращает либо IndexLinear(), либо IndexCartesian(). Смотрите описание ниже.
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 является IndexStyle. Поскольку индексирование является важной частью массива и часто встречается в горячих циклах, важно сделать и индексирование, и присваивание по индексу максимально эффективными. Структуры данных массивов обычно определяются одним из двух способов: либо они наиболее эффективно обращаются к элементам с помощью одного индекса (линейное индексирование), либо они изначально обращаются к элементам с индексами, указанными для каждой размерности. Эти два способа идентифицируются в Julia как IndexLinear() и IndexCartesian(). Преобразование линейного индекса в несколько индексных подстрок обычно очень дорого, поэтому это предоставляет механизм на основе свойств для обеспечения эффективного универсального кода для всех типов массивов.

Это различие определяет, какие скалярные методы индексирования должен определять тип. Массивы IndexLinear() просты: просто определите getindex(A::ArrayType, i::Int). Когда к массиву обращаются с многомерным набором индексов, вспомогательный метод getindex(A::AbstractArray, I...)() эффективно преобразует индексы в один линейный индекс, а затем вызывает указанный выше метод. В то же время, для массивов IndexCartesian() необходимо определить методы для каждой поддерживаемой размерности с ndims(A) Int индексами. Например, встроенный тип 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(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 \ [1 2; 3 4; 5 6; 7 8; 9 10; 11 12; 13 14]
1×2 Array{Float64,2}:
 0.305389  0.335329

julia> s ⋅ s # dot(s, s)
4676

В качестве более сложного примера давайте определим собственный массив типа N-мерного разрежённого массива, построенный поверх Dict:

julia> struct SparseArray{T,N} <: AbstractArray{T,N}
           data::Dict{NTuple{N,Int}, T}
           dims::NTuple{N,Int}
       end

julia> SparseArray{T}(::Type{T}, dims::Int...) = SparseArray(T, dims);

julia> SparseArray{T,N}(::Type{T}, dims::NTuple{N,Int}) = 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 сам по себе может быть массивом (например, при индексировании по 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(), getindex() и setindex!() для SparseArray также позволяет 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

Помимо всех итерируемых и индексируемых методов из вышесказанного, эти типы также могут взаимодействовать друг с другом и использовать большинство методов, определённых в стандартной библиотеке для 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 (обычно кортеж размера) мог принимать 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.6/manual/interfaces/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API