Spec-Zone.ru › Julia 1.10

Интерфейсы

Большая часть возможностей и расширяемости в 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} допустимых индексов. Оси должны быть своими осями, т.е. 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 индексов. Например, 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). Методы резервного копирования 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(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 собственной конструкции. Для получения дополнительной информации см. Массивы с пользовательскими индексами.

Массивы со сдвигом

Методы для реализации Краткое описание
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 пересчитывает потенциально вложенную операцию в одну функцию и плоский список аргументов. Вы несёте ответственность за реализацию правил трансляции форм сами, но это может быть полезно в ограниченных ситуациях.
  • Итерация по CartesianIndices axes(::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)))

Вам не нужно писать правила бинарной трансляции, если вы не хотите установить приоритет для двух или более типов, не являющихся 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, что его комбинация с двумерным массивом даёт SparseMatStyle, а всё с большей размерностью возвращается к плотному фреймворку произвольной размерности. Эти правила позволяют трансляции сохранить разреженное представление для операций, которые приводят к выходам размерности 1 или 2, но производят 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.10/manual/interfaces/

Spec-Zone.ru

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