Интерфейсы
Большая часть возможностей и расширяемости в Julia обусловлена набором неформальных интерфейсов. При расширении нескольких определенных методов для работы с пользовательским типом объекты этого типа не только получают соответствующие функциональные возможности, но и могут использоваться в других методах, написанных для формирования моделей поведения.
Итерация
Обязательные методы | Краткое описание | |
---|---|---|
|
Возвращает либо кортеж из первого элемента и начального состояния, либо |
|
|
Возвращает либо кортеж из следующего элемента и следующего состояния, либо |
|
Важные необязательные методы |
Определение по умолчанию |
Краткое описание |
|
|
Один из |
|
|
Либо |
|
|
Тип первой записи кортежа, возвращаемого |
|
(не определено) |
Количество элементов, если известно |
|
(не определено) |
Количество элементов в каждом измерении, если известно |
|
|
Указание быстрого пути для завершения итератора. Должен быть определен для итераторов с сохранением состояния, иначе |
Значение, возвращаемое IteratorSize(IterType) |
Обязательные методы |
---|---|
|
|
|
|
|
(нет) |
|
(нет) |
Значение, возвращаемое IteratorEltype(IterType) |
Обязательные методы |
---|---|
|
|
|
(нет) |
Последовательная итерация реализуется с помощью функции iterate
. Вместо того чтобы изменять объекты по мере их итерации, итераторы Julia могут отслеживать состояние итерации вне объекта. Возвращаемое значение итерации всегда является либо кортежем из значения и состояния, либо nothing
, если элементов не осталось. Объект состояния будет передан обратно в функцию итерации при следующей итерации и обычно рассматривается как деталь реализации, частная для итерируемого объекта.
Любой объект, который определяет эта функция, является итерируемым и может быть использован во многих функциях, где применяется итерация. Его также можно использовать непосредственно в цикле for
, поскольку синтаксис
for item in iter # или "for item = iter"
# тело
end
преобразовывается в
next = iterate(iter)
while next !== nothing
(item, state) = next
# тело
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 # Обратите внимание, что это определено для типа
julia> Base.length(S::Squares) = S.count
Теперь когда в Julia требуется собрать все элементы в массив с помощью метода collect
, Julia может предварительно выделить Vector{Int}
нужного размера вместо того, чтобы отправлять каждый элемент в 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
Индексирование
Реализуемые методы | Краткое описание |
---|---|
|
|
|
|
|
Первый индекс, используемый в |
|
Последний индекс, используемый в |
Для приведенного выше итерируемого объекта 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
.
Абстрактные массивы
Реализуемые методы | Краткое описание | |
---|---|---|
|
Возвращает кортеж, содержащий измерения |
|
|
(если |
|
|
(если |
|
Необязательные методы |
Определение по умолчанию |
Краткое описание |
|
|
Возвращает |
|
(если |
|
|
(если |
|
|
определяется с точки зрения скалярной функции |
|
|
определяется с точки зрения скалярной функции |
|
|
определяется с точки зрения скалярной функции |
Итерация |
|
|
Количество элементов |
|
|
Возвращает изменяемый массив с той же формой и типом элементов |
|
|
Возвращает изменяемый массив с той же формой и указанным типом элементов |
|
|
Возвращает изменяемый массив с тем же типом элементов и измерениями размера |
|
|
Возвращает изменяемый массив с указанным типом элементов и размером |
Нетрадиционные индексы |
Определение по умолчанию |
Краткое описание |
|
|
Возвращает кортеж |
|
|
Возвращает изменяемый массив с указанными индексами |
|
|
Возвращает массив, аналогичный |
Если тип определен как подтип 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
поддерживает одно- и двухаргументные формы, в большинстве случаев нужно специализировать только трехаргументную форму.) Для этого важно, чтобы 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
(обычно кортеж размеров Dims
) мог принимать объекты AbstractUnitRange
, возможно, диапазонные типы Ind
вашей собственной разработки. Дополнительные сведения см. в главе Массивы с пользовательскими индексами.
Массивы с заданным шагом
Реализуемые методы | Краткое описание | |
---|---|---|
|
Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в каждом измерении в виде кортежа. Если |
|
|
Возвращает собственный адрес массива. |
|
|
Возвращает шаг между последовательными элементами в массиве. |
|
Необязательные методы |
Определение по умолчанию |
Краткое описание |
|
|
Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в измерении k. |
Массив с заданным шагом — это подтип массива AbstractArray
, записи которого хранятся в памяти с фиксированными шагами. При условии, что тип элемента массива совместим с BLAS, массив с заданным шагом может использовать подпрограммы BLAS и LAPACK для более эффективных процедур линейной алгебры. Стандартным примером определяемого пользователем массива с заданным шагом является массив, который заключает стандартный массив Array
в дополнительную структуру.
Предупреждение. Не реализуйте эти методы, если базовое хранилище не имеет заданных шагов, так как это может привести к неправильным результатам или ошибкам сегментации.
Ниже приведены примеры типов массивов с заданными шагами и без них.
1:5 # без заданного шага (хранилище, связанное с этим массивом, отсутствует)
Vector(1:5) # с заданными шагами (1,)
A = [1 5; 2 6; 3 7; 4 8] # с заданными шагами (1, 4)
V = view(A, 1:2, :) # с заданными шагами (1, 4)
V = view(A, 1:2:3, 1:2) # с заданными шагами (2, 4)
V = view(A, [1,2,4], :) # без заданного шага, так как расстояние между строками не является фиксированным.
Настройка трансляции
Реализуемые методы | Краткое описание |
---|---|
|
Проведение трансляции типа |
|
Выделяет выходной контейнер |
Необязательные методы |
|
|
Правила приоритета при смешивании стилей |
|
Объявляет индексы |
|
Преобразует |
Обход механизмов по умолчанию |
|
|
Пользовательская реализация |
|
Пользовательская реализация |
|
Пользовательская реализация |
|
Переопределяет отложенное поведение по умолчанию в выражении с объединением |
|
Переопределяет вычисления осей отложенной трансляции |
Трансляция активируется явным вызовом 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
. Строки имеют специальный регистр, чтобы действовать как скаляры для целей трансляции, хотя они являются итерируемыми коллекциями своих символов (подробнее см. в разделе Строки).
Следующие два шага (выбор выходного массива и реализация) зависят от определения единого решения для заданного набора аргументов. Трансляция должна принимать все разнообразные типы своих аргументов и сводить их к одному выходному массиву и одной реализации. В трансляции это единое решение называется «стилем». Каждый транслируемый объект имеет свой собственный предпочитаемый стиль, а для объединения этих стилей в единое решение — «стиль назначения» — используется система, аналогичная продвижению.
Стили трансляции
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
, где транслируемый объект используется в качестве первого аргумента.
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
# Сканирует входные данные для ArrayAndChar:
A = find_aac(bc)
# Использует поле символов A для создания вывода
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)
# вывод
find_aac (generic function with 6 methods)
Из этих определений вытекает следующее поведение.
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
с нуль- или одномерными массивами выдает еще один SparseVecStyle
, сочетание с двумерным массивом выдает SparseMatStyle
, а все, что имеет более высокую размерность, возвращается в плотную структуру произвольной размерности. Эти правила позволяют трансляции сохранять разреженное представление для операций, результатом выполнения которых являются одно- или двумерные выходные данные, но которые выдают Array
для любой другой размерности.
Свойства экземпляра
Реализуемые методы | Определение по умолчанию | Краткое описание |
---|---|---|
|
|
Возвращает кортеж свойств ( |
|
|
Возвращает свойство |
|
|
Присваивает свойству |
Иногда желательно изменить способ взаимодействия конечного пользователя с полями объекта. Вместо предоставления прямого доступа к полям типа можно ввести дополнительный уровень абстракции между пользователем и кодом, перегрузив 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
# Позволяет обращаться к полям в форме p.r и 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
# Позволяет изменять поля в форме p.r и p.ϕ
return setfield!(p, s, f)
end
end
Важно использовать внутри getproperty
и setproperty!
вызовы getfield
и setfield
вместо точечного синтаксиса, так как точечный синтаксис сделает функции рекурсивными, что может привести к проблемам с выводом типов. Теперь можно испытать новую функциональность:
julia> propertynames(p)
(:x, :y)
julia> p.x
4.949747468305833
julia> p.y = 4.0
4.0
julia> p.r
6.363961030678928
В заключение стоит отметить, что в Julia свойства экземпляра довольно редко добавляются таким образом. Как правило, для этого должна быть веская причина.