Spec-Zone.ru › Julia 1.11

Интерфейсы

Большая часть мощности и расширяемости в Julia происходит от набора неформальных интерфейсов. Расширяя несколько определенных методов для работы с пользовательским типом, объекты этого типа не только получают эти функциональности, но и могут использоваться в других методах, которые написаны для обобщенного построения на основе этих поведений.

Итерация

Существует два метода, которые всегда требуются:

Требуемый метод Краткое описание
iterate(iter) Возвращает либо кортеж из первого элемента и начального состояния, либо nothing, если коллекция пуста
iterate(iter, state) Возвращает либо кортеж из следующего элемента и следующего состояния, либо nothing если элементов больше нет

Есть несколько дополнительных методов, которые следует определять в некоторых ситуациях. Обратите внимание, что вы всегда должны определять по крайней мере один из Base.IteratorSize(IterType) и length(iter), потому что по умолчанию Base.IteratorSize(IterType) определено как Base.HasLength().

Метод Когда следует определять этот метод? Определение по умолчанию Краткое описание
Base.IteratorSize(IterType) Если значение по умолчанию не подходит Base.HasLength() Один из Base.HasLength(), Base.HasShape{N}(), Base.IsInfinite(), или Base.SizeUnknown(), соответственно
length(iter) Если Base.IteratorSize() возвращает Base.HasLength() или Base.HasShape{N}() (неопределено) Количество элементов, если известно
size(iter, [dim]) Если Base.IteratorSize() возвращает Base.HasShape{N}() (неопределено) Количество элементов в каждом измерении, если известно
Base.IteratorEltype(IterType) Если значение по умолчанию не подходит Base.HasEltype() Либо Base.EltypeUnknown(), либо Base.HasEltype(), соответственно
eltype(IterType) Если значение по умолчанию не подходит Any Тип первого элемента кортежа, возвращаемого iterate()
Base.isdone(iter, [state]) Обязательно должно быть определено, если итератор имеет состояние missing Быстрый путь определения завершения итератора. Если для состоятельного итератора не определено, то функции, которые проверяют завершение, например, isempty() и zip(), могут изменить итератор и привести к ошибочному поведению!

Последовательная итерация реализована функцией 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 все элементы в массив, она может предварительно выделить массив нужного размера вместо наивного push! каждого элемента в массив Vector{Any}:

julia> collect(Squares(4))
4-element Vector{Int64}:
  1
  4
  9
 16

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

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

julia> sum(Squares(1803))
1955361914

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

Часто бывает полезно позволить итерацию по коллекции в обратном порядке, итерируя по Iterators.reverse(iterator). Однако, чтобы фактически поддерживать итерацию в обратном порядке, типу итератора T нужно реализовать 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) Индексированный доступ, нескалярные i должны выделять копию
setindex!(X, v, i) Индексированное присваивание
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). Когда массив затем индексируется с помощью набора многомерных индексов, метод fallback 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). Методы fallback 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)))

Вам не нужно писать правила бинарной 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 и, как правило, должно делаться только в том случае, если есть веская причина.

Округление

Методы для реализации Определение по умолчанию Краткое описание
round(x::ObjType, r::RoundingMode) нет Округлить x и вернуть результат. Если возможно, округлить должно вернуть объект того же типа, что и x
round(T::Type, x::ObjType, r::RoundingMode) convert(T, round(x, r)) Округлить x, вернув результат как T

Для поддержки округления нового типа обычно достаточно определить единственный метод round(x::ObjType, r::RoundingMode). Переданный режим округления определяет, в каком направлении должно быть округлено значение. Наиболее часто используемые режимы округления — RoundNearest, RoundToZero, RoundDown, и RoundUp, поскольку эти режимы округления используются в определениях метода с одним аргументом round, и trunc, floor, и ceil, соответственно.

В некоторых случаях можно определить метод round с тремя аргументами, который более точен или производителен, чем метод с двумя аргументами, за которым следует преобразование. В этом случае допустимо определить метод с тремя аргументами помимо метода с двумя аргументами. Если округлённый результат невозможно представить как объект типа T, то метод с тремя аргументами должен сгенерировать InexactError.

Например, если у нас есть тип Interval , который представляет собой диапазон возможных значений, аналогично https://github.com/JuliaPhysics/Measurements.jl, мы можем определить округление для этого типа следующим образом

julia> struct Interval{T}
           min::T
           max::T
       end

julia> Base.round(x::Interval, r::RoundingMode) = Interval(round(x.min, r), round(x.max, r))

julia> x = Interval(1.7, 2.2)
Interval{Float64}(1.7, 2.2)

julia> round(x)
Interval{Float64}(2.0, 2.0)

julia> floor(x)
Interval{Float64}(1.0, 2.0)

julia> ceil(x)
Interval{Float64}(2.0, 3.0)

julia> trunc(x)
Interval{Float64}(1.0, 2.0)

© 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/manual/interfaces/

Spec-Zone.ru

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