Интерфейсы
Большая часть возможностей и расширяемости в 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]) |
missing |
Указание для быстрого пути завершения итератора. Должно быть определено для состоятельных итераторов, иначе 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, так как синтаксис:
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 все элементы в массив, она может предварительно выделить массив нужного размера вместо того, чтобы по умолчанию добавлять каждый элемент в массив:
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 должен реализовывать iterate для Iterators.Reverse{T}. (Учитывая 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) (по умолчанию они вызывают first и last соответственно для axes(a, dim)).
Обратите внимание, что выше описано только определение 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} допустимых индексов |
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 индексами. Например, SparseMatrixCSC из стандартного модуля SparseArrays, поддерживает только две размерности, поэтому она просто определяет 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
В качестве более сложного примера давайте определим наш собственный пример N-мерного разреженного массива, построенного на основе Dict:
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). Методы-обработчики падения для индексирования используют 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(A::SparseArray, ::Type{T}, dims::Dims) where T для создания соответствующего обернутого массива. (Обратите внимание, что хотя 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
В дополнение ко всем итерируемым и индексируемым методам из вышеприведенного, эти типы также могут взаимодействовать друг с другом и использовать большинство методов, определенных в 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 (обычно кортеж размеров) мог принимать AbstractUnitRange объекты, возможно, типы диапазонов Ind вашего собственного дизайна. Более подробную информацию см. в документации по массивам со специальными индексами.
Массивы с шагами (Strided Arrays)
| Методы для реализации | Краткое описание | |
|---|---|---|
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; строки специально обрабатываются как скаляры для целей вещания, даже если они являются итерируемыми коллекциями своих символов (см. Strings для получения более подробной информации).
Следующие два шага (выбор массива вывода и реализации) зависят от определения единого ответа для данного набора аргументов. Вещание должно принять все различные типы своих аргументов и свести их к одному массиву вывода и одной реализации. Вещание называет этот единственный ответ "стилем". Каждый транслируемый объект имеет свой предпочтительный стиль, и используется система продвижения для объединения этих стилей в единый ответ — "целевой стиль".
Стили вещания
Base.BroadcastStyle — это абстрактный тип, от которого производятся все стили вещания. При использовании в качестве функции он имеет две возможные формы: унарную (с одним аргументом) и бинарную. Унарная форма указывает, что вы хотите реализовать определённое поведение вещания и/или тип вывода и не хотите полагаться на стандартную резервную Broadcast.DefaultArrayStyle.
Чтобы переопределить эти значения по умолчанию, вы можете определить пользовательский BroadcastStyle для своего объекта:
struct MyStyle <: Broadcast.BroadcastStyle end
Base.BroadcastStyle(::Type{<:MyType}) = MyStyle()
В некоторых случаях может быть удобнее не определять MyStyle, в этом случае вы можете использовать один из общих оболочек вещания:
-
Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}()может использоваться для произвольных типов. -
Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}()предпочтительнее, еслиMyTypeявляетсяAbstractArray. - Для
AbstractArrays, которые поддерживают только определённую размерность, создайте подтипBroadcast.AbstractArrayStyle{N}(см. ниже).
Когда ваша операция вещания включает несколько аргументов, индивидуальные стили аргументов объединяются для определения одного DestStyle, который контролирует тип контейнера вывода. Для получения более подробной информации см. ниже.
Выбор подходящего массива вывода
Стиль вещания вычисляется для каждой операции вещания, чтобы обеспечить диспетчеризацию и специализацию. Фактическое выделение массива результата обрабатывается similar, используя объект Broadcasted в качестве первого аргумента.
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)), где внутренняя операция оценивается до обработки внешней операции. Этот вид жадной операции напрямую поддерживается небольшим отступлением; вместо прямого создания объектов Broadcasted, Julia понижает объединённое выражение 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пересчитывает потенциально вложенную операцию в одну функцию и плоский список аргументов. Вы отвечаете за реализацию правил формы широковещательной рассылки самостоятельно, но это может быть полезно в ограниченных ситуациях. - Итерация по
CartesianIndicesaxes(::Broadcasted)и использование индексации с полученнымCartesianIndexобъектом для вычисления результата.
Написание правил бинарной широковещательной рассылки
Правила приоритета определяются бинарными BroadcastStyle вызовами:
Base.BroadcastStyle(::Style1, ::Style2) = Style12()
где Style12 - это BroadcastStyle , которое вы хотите выбрать для выходов, включающих аргументы Style1 и Style2. Например,
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)))
Вам не нужно писать правила бинарной BroadcastStyle рассылки, если вы не хотите установить приоритет для двух или более не-DefaultArrayStyle типов.
Если ваш тип массива имеет фиксированные требования к размерности, то вы должны быть подтипом 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.property) объекта x. Если private=true, также возвращает имена свойств, предназначенные для сохранения как приватные |
getproperty(x::ObjType, s::Symbol) |
getfield(x, s) |
Возвращает свойство s объекта x. x.s вызывает getproperty(x, :s). |
setproperty!(x::ObjType, s::Symbol, v) |
setfield!(x, s, v) |
Устанавливает свойство s объекта x в значение 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), который по умолчанию эквивалентен 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 довольно редко делается и, как правило, должно выполняться только в том случае, если для этого есть веская причина.
© 2009–2023 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.9/manual/interfaces/