Документация по Tables.jl
Это руководство содержит документацию по мощным интерфейсам таблиц в пакете Tables.jl. Обратите внимание, что пакет и, соответственно, документация ориентированы на разработчиков пакетов и библиотек, которые планируют реализовывать и использовать интерфейсы. С другой стороны, пользователям выгодно использовать другие пакеты, которые обеспечивают эффективный доступ к табличным данным в различных форматах или рабочих процессах. Хотя разобраться в интерфейсах и функциональности рекомендуется всем заинтересованным лицам, обратите внимание, что большинству пользователей не нужно использовать Tables.jl напрямую.
Поэтому вы легко можете открыть новую проблему, даже если это просто вопрос, или пообщаться с нами в канале #data чата Slak, если у вас есть вопросы, возникли проблемы или требуются уточнения. Кроме того, список пакетов, поддерживающих интерфейс Tables.jl, можно найти на странице INTEGRATIONS.md.
Обратитесь к документации по TableOperations.jl, чтобы узнать о таких распространенных операциях с таблицами, как select
, transform
, filter
и map
.
Использование интерфейса (т. е. источников, совместимых с Tables.jl)
Начнем с обсуждения использования функций интерфейса Tables.jl, поскольку это может помочь контекстуализировать их реализацию для пользовательских типов таблиц.
На высоком уровне Tables.jl предоставляет два мощных API для предсказуемого доступа к данным из любого табличного источника:
# построчный доступ к данным входной таблицы `x`
# функция Tables.rows должна возвращать итератор строк
rows = Tables.rows(x)
# мы можем итерировать каждую строку
for row in rows
# пример получения всех значений в строке
# не волнуйтесь, есть и другие способы более эффективной обработки строк
rowvalues = [Tables.getcolumn(row, col) for col in Tables.columnnames(row)]
end
# доступ к данным входной таблицы `x` по столбцам
# функция Tables.columns возвращает объект, в котором можно получить доступ к отдельным полным столбцам
columns = Tables.columns(x)
# итерация каждого имени столбца в таблице
for col in Tables.columnnames(columns)
# получение всего столбца по имени столбца
# столбец является индексируемой коллекцией
# известной длины (т. е. поддерживает
# `length(column)` и `column[i]`)
column = Tables.getcolumn(columns, col)
end
Итак, мы видим здесь две высокоуровневые функции — Tables.rows
и Tables.columns
.
#
Tables.rows
— Function
Tables.rows(x) => Row iterator
Построчно обращается к данным источника входной таблицы x
, возвращая совместимый с AbstractRow
итератор. Обратите внимание, что даже если источник входной таблицы по своей природе ориентирован на столбцы, в Tables.jl определено эффективное универсальное определение функции Tables.rows
для возврата итератора представлений строк в столбцы входной таблицы.
Тип Tables.Schema
итератора AbstractRow
можно запросить с помощью Tables.schema(rows)
, в результате чего может быть возвращено nothing
, если схема неизвестна. Имена столбцов всегда можно запросить, вызвав Tables.columnnames(row)
для отдельной строки, а к значениям строк можно получить доступ, вызвав Tables.getcolumn(row, i::Int )
или Tables.getcolumn(row, nm::Symbol)
с индексом или именем столбца, соответственно.
См. также описание rowtable
и namedtupleiterator
.
#
Tables.columns
— Function
Tables.columns(x) => AbstractColumns-compatible object
Обращается к данным источника входной таблицы x
, возвращая совместимый с AbstractColumns
объект, который позволяет получать все столбцы по имени или индексу. Извлеченный столбец представляет собой индексируемый объект на основе 1, который имеет известную длину, т. е. поддерживает length(col)
и col[i]
для любого i = 1:length(col)
. Обратите внимание, что даже если источник входной таблицы по своей природе ориентирован на строки, в Tables.jl определено эффективное универсальное определение Tables.columns
для построения совместимого с AbstractColumns
объекта на основе входных строк.
Tables.Schema
объекта AbstractColumns
можно запросить с помощью Tables.schema(columns)
, в результате чего может быть возвращено nothing
, если схема неизвестна. Имена столбцов всегда можно запросить, вызвав Tables.columnnames(columns)
, а к отдельным столбцам можно получить доступ, вызвав Tables.getcolumn(columns, i::Int )
или Tables.getcolumn(columns, nm::Symbol)
с индексом или именем столбца, соответственно.
Обратите внимание, что если x
является объектом, в котором столбцы хранятся как векторы, проверка того, что эти векторы используют индексацию на основе 1, не выполняется (это должно быть обеспечено при построении x
).
Принимая во внимание эти два мощных метода доступа к данным, рассмотрим реальные, хотя и несколько упрощенные версии того, как пакеты на самом деле используют эти методы.
Использование Tables.rows
Для начала рассмотрим пакет SQLite.jl и то, как он использует интерфейс Tables.jl для загрузки универсальных табличных данных в реляционную таблицу SQLite. Вот код:
function load!(table, db::SQLite.DB, tablename)
# получение строк входной таблицы
rows = Tables.rows(table)
# запрос схемы данных
sch = Tables.schema(rows)
# создание таблицы с использованием имени таблицы и схемы из входной таблицы
createtable!(db, tablename, sch)
# построение оператора insert
params = chop(repeat("?,", length(sch.names)))
stmt = Stmt(db, "INSERT INTO $tablename VALUES ($params)")
# запуск транзакции для вставки строк
transaction(db) do
# итерация строк во входной таблице
for row in rows
# Tables.jl предоставляет служебную функцию
# Tables.eachcolumn, которая позволяет
# применять функцию к каждому значению столбца в строке
# она вызывается со схемой и строкой и применяет
# пользовательскую функцию к значению столбца `val`, индексу `i`
# и имени столбца `nm`. Здесь мы привязываем значения строк
# к параметризованному оператору SQL INSERT, а затем
# вызываем `sqlite3_step` для выполнения оператора INSERT.
Tables.eachcolumn(sch, row) do val, i, nm
bind!(stmt, i, val)
end
sqlite3_step(stmt.handle)
sqlite3_reset(stmt.handle)
end
end
return
end
Это довольно простое использование: вызывается функция Tables.rows
для источника входной таблицы, и поскольку нам нужна схема для настройки таблицы базы данных, мы запрашиваем ее с помощью функции Tables.schema
. Затем мы итерируем строки таблицы с помощью for row in rows
и используем вспомогательную функцию Tables.eachcolumn
для применения функции к каждому значению в строке. Обратите внимание, что мы вообще не вызывали функции Tables.columnnames
или Tables.getcolumn
, поскольку они используются самой функцией Tables.eachcolumn
. Функция Tables.eachcolumn
оптимизирована для обеспечения стабильности типов и даже подстановки констант для индекса, имени и типа столбцов в некоторых случаях, чтобы гарантировать эффективное использование значений строк.
Однако следует учитывать случай «неизвестной схемы»; т. е. что если бы вызов Tables.schema
вернул nothing
(это может произойти в случае экзотических источников таблиц, таких как «лениво» сопоставляемые преобразования строк в таблице):
function load!(sch::Nothing, rows, db::SQLite.DB, tablename)
# sch имеет значение nothing === неизвестная схема
# запуск итерации строк входной таблицы
state = iterate(rows)
state === nothing && return
row, st = state
# запрос имен столбцов первой строки
names = Tables.columnnames(row)
# частичное построение Tables.Schema, по крайней мере путем передачи
# ей имен столбцов
sch = Tables.Schema(names, nothing)
# создание таблицы при необходимости
createtable!(db, tablename, sch)
# построение оператора insert
params = chop(repeat("?,", length(names)))
stmt = Stmt(db, "INSERT INTO $nm VALUES ($params)")
# запуск транзакции для вставки строк
transaction(db) do
while true
# как и раньше, мы можем использовать `Tables.eachcolumn`
# даже с частично созданной Tables.Schema
# для применения функции к каждому значению в строке
Tables.eachcolumn(sch, row) do val, i, nm
bind!(stmt, i, val)
end
sqlite3_step(stmt.handle)
sqlite3_reset(stmt.handle)
# итерация строк до завершения
state = iterate(rows, st)
state === nothing && break
row, st = state
end
end
return name
end
Реализованная здесь стратегия заключается в том, чтобы начать итерацию входного источника и, используя первую строку в качестве ориентира, создать объект Tables.Schema
, содержащий только имена столбцов, которые можно затем передать функции Tables.eachcolumn
, чтобы применить функцию bind!
к каждому значению строки.
Использование Tables.columns
Теперь рассмотрим случай использования Tables.columns
. Следующий код взят из реализации DataFrames.jl Tables.jl:
getvector(x::AbstractVector) = x
getvector(x) = collect(x)
# обратите внимание, что copycols в этом определении игнорируется (Tables.CopiedColumns предполагает, что копии уже сделаны)
fromcolumns(x::Tables.CopiedColumns, names; copycols::Bool=true) =
DataFrame(AbstractVector[getvector(Tables.getcolumn(x, nm) for nm in names],
Index(names),
copycols=false)
fromcolumns(x; copycols::Bool=true) =
DataFrame(AbstractVector[getvector(Tables.getcolumn(x, nm) for nm in names],
Index(names),
copycols=copycols)
function DataFrame(x; copycols::Bool=true)
# получение столбцов из источника входной таблицы
cols = Tables.columns(x)
# получение имен столбцов в виде Vector{Symbol}, что требуется
# основному конструктору DataFrame
names = collect(Symbol, Tables.columnnames(cols))
return fromcolumns(cols, names; copycols=copycols)
end
Итак, у нас есть универсальный конструктор DataFrame
, который принимает единственный нетипизированный аргумент, вызывает для него функцию Tables.columns
, а затем — функцию Tables.columnnames
для получения имен столбцов. Затем он передает совместимый с Tables.AbstractColumns
объект внутренней функции fromcolumns
, которая диспетчеризирует особый вид объекта Tables.AbstractColumns
, называемый Tables.CopiedColumns
, заключающий в оболочку любой совместимый с Tables.AbstractColumns
объект, копии столбцов которого уже были сделаны, и поэтому он безопасен для потребителя колонок (это происходит потому, что DataFrames.jl по умолчанию делает копии всех колонок при построении). В обоих случаях отдельные столбцы собираются в Vector{AbstractVector}
путем вызова Tables.getcolumn(x, nm)
для каждого имени столбца. И последнее замечание — вызов getvector
для каждого столбца, что обеспечивает материализацию каждого столбца в виде AbstractVector
, как того требует конструктор DataFrame.
Обратите внимание, что при использовании строк и столбцов не нужно было беспокоиться о естественной ориентации входных данных. Мы просто вызывали Tables.rows
или Tables.columns
, что было наиболее естественно для конкретного случая использования таблицы, зная, что это будет просто работать™️.
Утилиты Tables.jl
Прежде чем перейти к реализации интерфейсов Tables.jl, сделаем небольшой перерыв, чтобы отметить некоторые служебные функции, предоставляемые Tables.jl:
#
Tables.Schema
— Type
Tables.Schema(names, types)
Создает объект Tables.Schema
, содержащий имена и типы столбцов для итератора AbstractRow
, возвращаемого из Tables.rows
, или объекта AbstractColumns
, возвращаемого из Tables.columns
. Tables.Schema
имеет двойное назначение: предоставление пользователям удобного интерфейса для запроса этих свойств, а также предоставление «структурного» типа для генерации кода.
Чтобы получить схему таблицы, можно вызвать Tables.schema
для результата Tables.rows
или Tables.columns
, но при этом следует учитывать, что таблица может вернуть nothing
, указывая, что имена ее столбцов и (или) типы элементов столбцов неизвестны (обычно их нельзя вывести). Аналогично признаку Base.EltypeUnknown()
для итераторов, когда вызывается Base.IteratorEltype
. Пользователи должны учитывать случай Tables.schema(tbl) => nothing
, используя свойства результатов Tables.rows(x)
и Tables.columns(x)
напрямую.
Чтобы получить доступ к именам, можно просто вызвать sch.names
для возврата коллекции символов (Tuple
или Vector
). Для доступа к типам элементов столбцов можно аналогичным образом вызвать sch.types
для возврата коллекции типов (как и (Int64, Float64, String)
).
Фактическое определение типа выглядит следующим образом:
struct Schema{names, types}
storednames::Union{Nothing, Vector{Symbol}}
storedtypes::Union{Nothing, Vector{Type}}
end
Где names
является кортежем Symbol
или nothing
, а types
— типом кортежа типов (как Tuple{Int64, Float64, String}
) или nothing
. Кодирование имен и типов в качестве параметров типа позволяет оптимально использовать тип в генерируемых функциях и других случаях оптимизации, но пользователи должны учитывать, что когда names
и (или) types
являются значением nothing
, имена и (или) типы хранятся в полях storednames
и storedtypes
. Это необходимо для учета очень широких таблиц с десятками тысяч столбцов, где кодирование имен или типов в качестве параметров типа становится непосильной задачей для компилятора. Поэтому, хотя оптимизация может быть написана для типизированных параметров типа names
/types
, пользователям также следует рассмотреть возможность работы с очень широкими таблицами путем специализации Tables.Schema{nothing, nothing}
.
#
Tables.schema
— Function
Tables.schema(x) => Union{Nothing, Tables.Schema}
Пытается получить схему объекта, возвращаемого Tables.rows
или Tables.columns
. Если итератор AbstractRow
или объект AbstractColumns
не может определить свою схему, возвращается nothing
. В противном случае возвращается объект Tables.Schema
, содержащий имена столбцов и типы, доступные для использования.
#
Tables.subset
— Function
Tables.subset(x, inds; viewhint=nothing)
Возвращает одну или несколько строк из таблицы x
в соответствии с позицией (-ями), указанной (-ыми) в inds
:
-
Если
inds
— одно нелогическое целое значение, возвращает объект строки. -
Если
inds
— вектор нелогических целых значений, вектор логических значений или:
, возвращает подмножество исходной таблицы в соответствии с индексами. В этом случае возвращаемый тип не обязательно совпадает с исходным типом таблицы.
Если переданы другие типы inds
, отличные от указанных выше, поведение будет неопределенным.
Аргумент viewhint
пытается определить, является ли возвращаемый объект представлением исходной таблицы или независимой копией:
-
Если
viewhint=nothing
(по умолчанию), реализация для конкретного типа таблицы может самостоятельно решать, что возвращать — копию или представление. -
Если
viewhint=true
, возвращается представление, а еслиviewhint=false
, возвращается копия. Это относится как к возврату строки, так и к возврату таблицы.
Любая специализированная реализация subset
должна поддерживать аргумент viewhint=nothing
. Поддержка viewhint=true
или viewhint=false
необязательна (т. е. реализации могут игнорировать именованный аргумент и возвращать представление или копию независимо от значения viewhint
).
#
Tables.partitions
— Function
Tables.partitions(x)
Запрашивает итератор «таблицы» из x
. Каждый итерируемый элемент должен быть «таблицей» в том смысле, что можно вызвать Tables.rows
или Tables.columns
, чтобы получить итератор строк или коллекцию столбцов. Все итерируемые элементы должны иметь идентичную схему, чтобы пользователи могли вызывать Tables.schema(first_element)
для первого итерируемого элемента и знать, что каждая последующая итерация будет соответствовать той же схеме. Определение по умолчанию имеет следующий вид:
Tables.partitions(x) = (x,)
Таким образом, любые входные данные считаются одной «таблицей». Это означает, что пользователи могут смело вызывать Tables.partitions
в любом месте, где они сейчас вызывают Tables.columns
или Tables.rows
, и получать их итератор. Иными словами, функции приемника могут использовать Tables.partitions
независимо от того, передает ли пользователь секционируемую таблицу или нет, поскольку по умолчанию одни входные данные рассматриваются как одна несекционированная таблица.
Tables.partitioner(itr)
представляет собой вспомогательную оболочку для создания секций таблиц из любого итератора таблиц, что позволяет заключать в оболочку вектор (Vector
) или итератор таблиц как допустимые секции, поскольку по умолчанию они будут рассматриваться как одна таблица.
Второй вспомогательный метод имеет определение следующего вида:
Tables.partitions(x...) = x
Это позволяет передавать таблицы с переменным числом аргументов, и они будут рассматриваться как отдельные разделы. Функции приемника могут допускать входные данные таблиц с переменными числом аргументов и передавать их в partitions
.
Ради удобства определены методы Tables.partitions(x::Iterators.PartitionIterator) = x
и Tables.partitions(x::Tables.Partitioner) = x
для случаев, когда пользователь выполнит секционирование с помощью функций Iterators.partition
или Tables.partitioner
.
#
Tables.partitioner
— Function
Tables.partitioner(f, itr)
Tables.partitioner(x)
Вспомогательные методы для создания итераторов таблиц. Первый метод принимает функцию «материализатора» f
и итератора itr
и вызовет Tables.LazyTable(f, x) for x in itr
для каждой итерации. Это позволяет отложить материализацию таблицы до вызова Tables.columns
или Tables.rows
для объекта LazyTable
(который вызовет f(x)
). Так можно реализовать общий необходимый шаблон материализации и обработки таблицы в удаленном процессе или потоке, например:
for tbl in Tables.partitions(Tables.partitioner(CSV.File, list_of_csv_files))
Threads.@spawn begin
cols = Tables.columns(tbl)
# выполнение операций со столбцами
end
end
Второй метод используется потому, что по умолчанию Tables.partition(x)
рассматривает x
как одну несекционированную таблицу. Этот метод позволяет пользователям легко заключать в оболочку вектор (Vector
) или генератор таблиц в виде секций таблицы, чтобы передать их функциям приемника, способным использовать Tables.partitions
.
#
Tables.rowtable
— Function
Tables.rowtable(x) => Vector{NamedTuple}
Принимает источник входной таблицы и создает вектор (Vector
) именованных кортежей (NamedTuple
), также известный как «таблица строк». Это своего рода тип таблицы по умолчанию, поскольку она естественным образом соответствует интерфейсу строк Tables.jl, т. е. Vector
естественным образом итерирует свои элементы, а NamedTuple
соответствует интерфейсу AbstractRow
по умолчанию (позволяет индексировать значение по индексу, имени и получать все имена).
Сведения о «ленивом» итераторе строк см. в описании rows
и namedtupleiterator
.
Не для использования с очень широкими таблицами с количеством столбцов, превышающим 67 тыс. Текущие фундаментальные ограничения компилятора не позволяют строить NamedTuple
такого размера.
#
Tables.columntable
— Function
Tables.columntable(x) => NamedTuple of AbstractVectors
Принимает источник входной таблицы x
и возвращает именованный кортеж (NamedTuple
) векторов (AbstractVector
), также известный как «таблица столбцов». Это своего рода тип таблицы по умолчанию, поскольку она естественным образом соответствует интерфейсу столбцов Tables.jl.
Обратите внимание, что если x
является объектом, в котором столбцы хранятся как векторы, проверка того, что эти векторы используют индексацию на основе 1, не выполняется (это должно быть обеспечено при построении x
).
Не для использования с очень широкими таблицами с количеством столбцов, превышающим 67 тыс. Текущие фундаментальные ограничения компилятора не позволяют строить NamedTuple
такого размера.
#
Tables.dictrowtable
— Function
Tables.dictrowtable(x) => Tables.DictRowTable
Принимает любой совместимый с Tables.jl источник x
и возвращает таблицу DictRowTable
, которую можно представить как вектор (Vector
) строк OrderedDict
, сопоставляющий имена столбцов в виде символов (Symbol
) со значениями. Порядок столбцов входной таблицы сохраняется с помощью Tables.schema(::DictRowTable)
.
Для входных таблиц «без схемы» dictrowtable
использует поведение «объединения столбцов», а не выводит схему из первой строки, как Tables.columns
. Это означает, что при итерации строк каждое значение из строки объединяется в итоговый набор столбцов. Это особенно полезно, когда строки входной таблицы могут не включать столбцы, если значение отсутствует, вместо того чтобы включать фактическое значение missing
, что часто встречается, например, в json. При этом возникают затраты на отслеживание всех видимых значений и вывод окончательных объединенных схем, поэтому рекомендуется использовать только в тех случаях, когда необходимо поведение объединения.
#
Tables.dictcolumntable
— Function
Tables.dictcolumntable(x) => Tables.DictColumnTable
Принимает любой совместимый с Tables.jl источник x
и возвращает таблицу DictColumnTable
, которую можно представить как словарь (OrderedDict
), сопоставляющий имена столбцов в виде символов (Symbol
) с векторами AbstractVector
. Порядок столбцов входной таблицы сохраняется с помощью Tables.schema(::DictColumnTable)
.
Для входных таблиц «без схемы» dictcolumntable
использует поведение «объединения столбцов», а не выводит схему из первой строки, как Tables.columns
. Это означает, что при итерации строк каждое значение из строки объединяется в итоговый набор столбцов. Это особенно полезно, когда строки входной таблицы могут не включать столбцы, если значение отсутствует, вместо того чтобы включать фактическое значение missing
, что часто встречается, например, в json. При этом возникают затраты на отслеживание всех видимых значений и вывод окончательных объединенных схем, поэтому рекомендуется использовать только в тех случаях, когда это необходимо.
#
Tables.namedtupleiterator
— Function
Tables.namedtupleiterator(x)
Передает любой источник входных данных таблицы и возвращает итератор NamedTuple
.
Не для использования с очень широкими таблицами с количеством столбцов, превышающим 67 тыс. Текущие фундаментальные ограничения компилятора не позволяют строить NamedTuple
такого размера.
#
Tables.datavaluerows
— Function
Tables.datavaluerows(x) => NamedTuple iterator
Принимает любые входные данные таблицы x
и возвращает итератор NamedTuple
, который заменит отсутствующие значения значениями, заключенными в DataValue
. Это позволяет любому табличному типу соответствовать интерфейсу интеграции TableTraits.jl Queryverse, определяя следующее:
IteratorInterfaceExtensions.getiterator(x::MyTable) = Tables.datavaluerows(x)
#
Tables.nondatavaluerows
— Function
Tables.nondatavaluerows(x)
Принимает любой источник итератора NamedTuple
, совместимый с Queryverse, и преобразует в итератор AbstractRow
, совместимый с Tables.jl. Автоматически распакует любой DataValue
, заменяя NA
на missing
. Полезно для преобразования результатов Query.jl обратно в таблицы, не основанные на DataValue
.
#
Tables.table
— Function
Tables.table(m::AbstractVecOrMat; [header])
Заключает в оболочку AbstractVecOrMat
(Matrix
, Vector
, Adjoint
и т. д.) в MatrixTable
, что соответствует интерфейсу Tables.jl. (AbstractVector
рассматривается как матрица с одним столбцом.) Это позволяет получать доступ к матрице с помощью Tables.rows
и Tables.columns
. Можно передать итератор header
необязательного именованного аргумента, который будет преобразован в Vector{Symbol}
для использования в качестве имен столбцов. Обратите внимание, что AbstractVecOrMat
не копируется.
#
Tables.matrix
— Function
Tables.matrix(table; transpose::Bool=false)
Материализует входные данные источника таблицы в виде новой матрицы (Matrix
) или в случае с MatrixTable
возвращает первоначально заключенную в оболочку матрицу. Если типы элементов столбцов таблицы являются разнородными, они путем продвижения будут приведены к общему типу в материализованной матрице (Matrix
). Обратите внимание, что имена столбцов игнорируются в преобразовании. По умолчанию столбцы входной таблицы будут материализованы как соответствующие столбцы матрицы. Передача transpose=true
транспонирует входные данные с входными столбцами как строки матрицы, или в случае с MatrixTable
применяет permutedims
к первоначально заключенной в оболочку матрице.
#
Tables.eachcolumn
— Function
Tables.eachcolumn(f, sch::Tables.Schema{names, types}, x::Union{Tables.AbstractRow, Tables.AbstractColumns})
Tables.eachcolumn(f, sch::Tables.Schema{names, nothing}, x::Union{Tables.AbstractRow, Tables.AbstractColumns})
Принимает функцию f
, схему таблицы sch
, x
, который является объектом, соответствующим интерфейсам AbstractRow
или AbstractColumns
. генерирует вызовы для получения значения каждого столбца (Tables.getcolumn(x, nm)
), а затем вызывает f(val, index, name)
, где f
является предоставленной пользователем функцией, val
— это значение столбца (AbstractRow
) или весь столбец (AbstractColumns
), index
— это индекс столбца в виде Int
, а name
— это имя столбца в виде Symbol
.
Ниже приведен пример использования Tables.eachcolumn
.
rows = Tables.rows(tbl)
sch = Tables.schema(rows)
if sch === nothing
state = iterate(rows)
state === nothing && return
row, st = state
sch = Tables.schema(Tables.columnnames(row), nothing)
while state !== nothing
Tables.eachcolumn(sch, row) do val, i, nm
bind!(stmt, i, val)
end
state = iterate(rows, st)
state === nothing && return
row, st = state
end
else
for row in rows
Tables.eachcolumn(sch, row) do val, i, nm
bind!(stmt, i, val)
end
end
end
Обратите внимание, что в этом примере мы учитываем, что входная таблица может вернуть nothing
из Tables.schema(rows)
. В этом случае мы начинаем итерацию строк и строим частичную схему, используя имена столбцов из первой строки sch = Tables.schema(Tables.columnnames(row), nothing)
, которую можно передать функции Tables.eachcolumn
.
#
Tables.materializer
— Function
Tables.materializer(x) => Callable
Для входных данных таблицы возвращает функцию «приемника» или «материализации», которая может принять совместимые с Tables.jl входные данные таблицы и создать экземпляр табличного типа. Это позволяет выполнять рабочие процессы «преобразования», которые принимают входные данные таблицы, применяют преобразования, потенциально преобразуя таблицу в другую форму, и в итоге создают таблицу того же типа, как и у исходных данных. Материализатором по умолчанию является Tables.columntable
, который преобразует любые входные данные таблицы в именованный кортеж (NamedTuple
) векторов (Vector
).
Рекомендуется, чтобы пользователи, реализующие MyType
, определяли только materializer(::Type{<:MyType})
. materializer(::MyType)
затем будет автоматически делегирован этому методу.
#
Tables.columnindex
— Function
Tables.columnindex(table, name::Symbol)
Возвращает индекс столбца (на основе 1) для столбца по имени (name
) в таблице с известной схемой, Возвращает 0, если имя (name
) не существует в таблице.
Если заданы имена и символ name
, вычисляет индекс (на основе 1) имени в именах.
#
Tables.columntype
— Function
Tables.columntype(table, name::Symbol)
Возвращает тип элемента столбца для столбца по имени (name
) в таблице с известной схемой, Возвращает Union{}, если имя (name
) не существует в таблице.
Если заданы тип кортежа и символ name
, вычисляет тип имени в типах кортежей.
#
Tables.rowmerge
— Function
rowmerge(row, other_rows...)
rowmerge(row; fields_to_merge...)
Возвращает NamedTuple
, объединяя row
(совместимое с AbstractRow
значение) с other_rows
(одним или несколькими совместимыми с AbstractRow
значениями) с помощью Base.merge
. Эта функция аналогична Base.merge(::NamedTuple, ::NamedTuple...)
, но принимает совместимые с AbstractRow
значения, а не NamedTuple
.
Определен вспомогательный метод rowmerge(row; fields_to_merge...) = rowmerge(row, fields_to_merge)
, который позволяет указывать fields_to_merge
в качестве именованных аргументов.
#
Tables.Row
— Type
Tables.Row(row)
Вспомогательный тип, позволяющий заключать в оболочку любой объект интерфейса AbstractRow
в выделенной структуре для обеспечения полезного поведения по умолчанию (позволяет использовать любой AbstractRow
как NamedTuple
):
-
Определен интерфейс индексирования; т. е.
row[i]
возвратит значение столбца по индексуi
,row[nm]
возвратит значение столбца для имени столбцаnm
. -
Определен интерфейс доступа к свойствам; т. е.
row.col1
получит значение для столбца с именемcol1
. -
Определен интерфейс итерации; т. е.
for x in row
будет итерировать каждое значение столбца в строке. -
Определены методы
AbstractDict
(get
,haskey
и т. д.) для проверки и получения значений столбцов.
#
Tables.Columns
— Type
Tables.Columns(tbl)
Вспомогательный тип, вызывающий Tables.columns
для входной таблицы (tbl
) и заключающий в оболочку результирующий интерфейс AbstractColumns
в выделенной структуре для обеспечения полезного поведения по умолчанию (позволяет использовать любой AbstractColumns
как NamedTuple
Vectors
):
-
Определен интерфейс индексирования; т. е.
row[i]
возвратит столбец по индексуi
,row[nm]
возвратит столбец для имени столбцаnm
. -
Определен интерфейс доступа к свойствам; т. е.
row.col1
получит значение для столбца с именемcol1
. -
Определен интерфейс итерации; т. е.
for x in row
будет итерировать каждый столбец в строке. -
Определены методы
AbstractDict
(get
,haskey
и т. д.) для проверки и получения столбцов.
Обратите внимание, что Tables.Columns
вызывает Tables.columns
внутренним образом для указанного аргумента таблицы. Tables.Columns
можно использовать для диспетчеризации, если это необходимо.
Реализация интерфейса (т. е. получение источника Tables.jl)
Узнав, как можно использовать интерфейс Tables.jl, рассмотрим, как его реализовать, то есть как сделать пользовательский тип допустимым для потребителей Tables.jl.
Для типа MyTable
интерфейс преобразования в правильную таблицу прост:
Обязательные методы | Определение по умолчанию | Краткое описание |
---|---|---|
|
Объявляет, что тип таблицы реализует интерфейс. |
|
Один из следующих: |
||
|
Объявляет, что тип таблицы определяет метод |
|
|
Возвращает совместимый с |
|
Или: |
||
|
Объявляет, что тип таблицы определяет метод |
|
|
Возвращает совместимый с |
|
Необязательные методы |
||
|
|
Возвращает объект |
|
|
Объявляет для типа таблицы функцию приемника materializer, которая может сконструировать экземпляр вашего типа из любых входных данных Tables.jl. |
|
Возвращает строку или подтаблицу исходной таблицы. |
|
|
Возвращает количество строк таблицы |
|
|
Возвращает количество столбцов таблицы |
В зависимости от того, какой тип таблицы определен (Tables.rows
или Tables.columns
), вы убедитесь, что итератор Tables.AbstractRow
или объект Tables.AbstractColumns
удовлетворяет соответствующему интерфейсу.
Для получения дополнительных сведений см. эту публикацию, в которой подробно описывается процесс создания таблицы, ориентированной на строки.
Tables.AbstractRow
#
Tables.AbstractRow
— Type
Tables.AbstractRow
Абстрактный тип интерфейса, представляющий ожидаемый тип элемента (eltype
) итератора, возвращаемый из Tables.rows(table)
. Tables.rows
должен возвращать итератор элементов, соответствующих интерфейсу Tables.AbstractRow
. Хотя Tables.AbstractRow
является абстрактным типом, который пользовательские типы «столбцов» могут разделить на подтипы для полезного поведения по умолчанию (индексирование, итерация, доступ к свойствам и т. д.), пользователи не должны использовать его для диспетчеризации, поскольку объекты интерфейса Tables.jl не должны выделять подтипы, а только реализовывать обязательные методы интерфейса.
Определение интерфейса:
Обязательные методы | Определение по умолчанию | Краткое описание |
---|---|---|
|
getfield(row, i) |
Получает значение столбца по индексу. |
|
getproperty(row, nm) |
Получает значение столбца по имени. |
|
propertynames(row) |
Возвращает имена столбцов для строки в виде индексируемой коллекции на основе 1. |
Необязательные методы |
||
|
Tables.getcolumn(row, nm) |
Получает значение столбца, учитывая тип элемента столбца |
Обратите внимание, что подтипы Tables.AbstractRow
должны перегружать все обязательные методы, перечисленные выше, а не полагаться на определения этих методов по умолчанию.
Хотя пользовательские типы строк не обязаны разделять Tables.AbstractRow
на подтипы, такое выделение дает следующие преимущества:
-
Определен интерфейс индексирования (с использованием
getcolumn
); т. е.row[i]
возвратит значение столбца по индексуi
. -
Определен интерфейс доступа к свойствам (с использованием
columnnames
иgetcolumn
); т. е.row.col1
получит значение для столбца с именемcol1
. -
Определен интерфейс итерации; т. е.
for x in row
будет итерировать каждое значение столбца в строке. -
Определены методы
AbstractDict
(get
,haskey
и т. д.) для проверки и получения значений столбцов. -
Метод по умолчанию
show
.
Это позволяет пользовательскому типу строк действовать практически аналогично встроенному объекту NamedTuple
.
Tables.AbstractColumns
#
Tables.AbstractColumns
— Type
Tables.AbstractColumns
Тип интерфейса, определяемый как упорядоченный набор столбцов, которые поддерживают извлечение отдельных столбцов по имени или индексу. Извлеченный столбец должен быть основанной на 1 индексируемой коллекцией известной длины, т. е. объектом, который поддерживает length(col)
и col[i]
для любого i = 1:length(col)
. Tables.columns
должен возвращать объект, соответствующий интерфейсу Tables.AbstractColumns
. Хотя Tables.AbstractColumns
является абстрактным типом, который пользовательские типы «столбцов» могут разделить на подтипы для полезного поведения по умолчанию (индексирование, итерация, доступ к свойствам и т. д.), пользователи не должны использовать его для диспетчеризации, поскольку объекты интерфейса Tables.jl не должны выделять подтипы, а только реализовывать обязательные методы интерфейса.
Определение интерфейса:
Обязательные методы | Определение по умолчанию | Краткое описание |
---|---|---|
|
getfield(table, i) |
Получает столбец по индексу. |
|
getproperty(table, nm) |
Получает столбец по имени. |
|
propertynames(table) |
Возвращает имена столбцов для таблицы в виде индексируемой коллекции на основе 1. |
Необязательные методы |
||
|
Tables.getcolumn(table, nm) |
Получает столбец, учитывая тип элемента столбца |
Обратите внимание, что подтипы Tables.AbstractColumns
должны перегружать все обязательные методы, перечисленные выше, а не полагаться на определения этих методов по умолчанию.
Хотя пользовательские типы строк не обязаны иметь подтип Tables.AbstractColumns
, их использование дает следующие преимущества.
-
Определен интерфейс индексирования (с использованием
getcolumn
); т. е.tbl[i]
возвратит столбец по индексуi
. -
Определен интерфейс доступа к свойствам (с использованием
columnnames
иgetcolumn
); т. е.tbl.col1
получит столбец с именемcol1
. -
Определен интерфейс итерации; т. е.
for col in table
будет итерировать каждый столбец в таблице. -
Определены методы
AbstractDict
(get
,haskey
и т. д.) для проверки и получения столбцов. -
Метод по умолчанию
show
.
Это позволяет пользовательскому типу таблицы действовать практически аналогично встроенному объекту NamedTuple
векторов.
Пример реализации
В качестве расширенного примера рассмотрим код, определенный в Tables.jl для обработки AbstractVecOrMat
в качестве таблиц.
Сначала определим особый тип MatrixTable
, который будет заключать AbstractVecOrMat
в оболочку и позволит легко перегружать интерфейс Tables.jl.
struct MatrixTable{T <: AbstractVecOrMat} <: Tables.AbstractColumns
names::Vector{Symbol}
lookup::Dict{Symbol, Int}
matrix::T
end
# объявление MatrixTable как таблицы
Tables.istable(::Type{<:MatrixTable}) = true
# методы getter, чтобы избежать конфликта getproperty
names(m::MatrixTable) = getfield(m, :names)
matrix(m::MatrixTable) = getfield(m, :matrix)
lookup(m::MatrixTable) = getfield(m, :lookup)
# схема — это имена и типы столбцов
Tables.schema(m::MatrixTable{T}) where {T} = Tables.Schema(names(m), fill(eltype(T), size(matrix(m), 2)))
Здесь мы определили функцию Tables.istable
для всех типов MatrixTable
, указывающую, что они реализуют интерфейсы Tables.jl. Мы также определили функцию Tables.schema
путем извлечения хранящихся имен столбцов, и поскольку AbstractVecOrMat
имеет один eltype
, мы повторяем эту процедуру для каждого столбца (вызов fill
). Обратите внимание, что определять функцию Tables.schema
для таблиц необязательно. По умолчанию возвращается nothing
, и потребители Tables.jl должны учитывать как известные, так и неизвестные случаи схем. Возвращение схемы, когда это возможно, позволяет потребителям получить определенные оптимизации, когда они могут знать типы всех столбцов заранее (и если количество столбцов не слишком велико), чтобы генерировать более эффективный код.
Итак, в этом примере мы хотим, чтобы тип MatrixTable
реализовывал оба метода Tables.rows
и Tables.columns
, т. е. он будет возвращать себя из этих функций. И вот как мы сделаем MatrixTable
допустимым объектом Tables.AbstractColumns
:
# интерфейс столбцов
Tables.columnaccess(::Type{<:MatrixTable}) = true
Tables.columns(m::MatrixTable) = m
# обязательные методы объекта Tables.AbstractColumns
Tables.getcolumn(m::MatrixTable, ::Type{T}, col::Int, nm::Symbol) where {T} = matrix(m)[:, col]
Tables.getcolumn(m::MatrixTable, nm::Symbol) = matrix(m)[:, lookup(m)[nm]]
Tables.getcolumn(m::MatrixTable, i::Int) = matrix(m)[:, i]
Tables.columnnames(m::MatrixTable) = names(m)
Мы определяем columnaccess
для типа, затем columns
просто возвращает сам MatrixTable
, а затем мы определяем три метода getcolumn
и columnnames
. Обратите внимание на использование lookup
Dict
, который сопоставляет имя столбца с индексом столбца, чтобы мы могли определить, какой столбец нужно вернуть из матрицы. Мы также храним имена столбцов в поле names
, поэтому реализация columnnames
тривиальна. Вот и все! Буквально все! Теперь это можно записать в CSV-файл, сохранить в SQLite или другую базу данных, преобразовать в таблицу DataFrame или JuliaDB и т. д. Довольно интересно.
А теперь перейдем к реализации Tables.rows
:
# объявим, что любая MatrixTable определяет собственный метод `Tables.rows`
rowaccess(::Type{<:MatrixTable}) = true
# просто возвращает себя, что означает, что MatrixTable должна итерировать объекты, совместимые с `Tables.AbstractRow
rows(m::MatrixTable) = m
# интерфейс итерации, как минимум, требует `eltype`, `length` и `iterate`
# для `MatrixTable` `eltype` мы предоставим пользовательский тип строки
Base.eltype(m::MatrixTable{T}) where {T} = MatrixRow{T}
Base.length(m::MatrixTable) = size(matrix(m), 1)
Base.iterate(m::MatrixTable, st=1) = st > length(m) ? nothing : (MatrixRow(st, m), st + 1)
# пользовательский тип строки; действует как представление для строки AbstractVecOrMat
struct MatrixRow{T} <: Tables.AbstractRow
row::Int
source::MatrixTable{T}
end
# необходимые методы интерфейса `Tables.AbstractRow` (те же, что и для объекта `Tables.AbstractColumns` ранее)
# но на этот раз для нашего пользовательского типа строки
getcolumn(m::MatrixRow, ::Type, col::Int, nm::Symbol) =
getfield(getfield(m, :source), :matrix)[getfield(m, :row), col]
getcolumn(m::MatrixRow, i::Int) =
getfield(getfield(m, :source), :matrix)[getfield(m, :row), i]
getcolumn(m::MatrixRow, nm::Symbol) =
getfield(getfield(m, :source), :matrix)[getfield(m, :row), getfield(getfield(m, :source), :lookup)[nm]]
columnnames(m::MatrixRow) = names(getfield(m, :source))
Здесь мы начинаем с определения функций Tables.rowaccess
и Tables.rows
, а затем методов интерфейса итерации, поскольку мы объявили, что сам тип MatrixTable
является итератором совместимых с Tables.AbstractRow
объектов. Для eltype
мы говорим, что MatrixTable
итерирует наш собственный пользовательский тип строки MatrixRow
. MatrixRow
выделяет подтипы Tables.AbstractRow
, что обеспечивает реализацию интерфейсов для нескольких полезных поведений (индексирование, итерация, доступ к свойствам и т. д.). По сути, так наш пользовательский тип MatrixRow
становится более удобным для работы.
Реализация интерфейса Tables.AbstractRow
проста и очень похожа на предыдущую реализацию Tables.AbstractColumns
(т. е. те же методы для getcolumn
и columnnames
).
Вот и все. Теперь тип MatrixTable
является полноценным, допустимым источником Tables.jl и может использоваться во всей экосистеме. Очевидно, что объем кода здесь невелик. Но опять же, фактические реализации интерфейса Tables.jl обычно довольно просты, учитывая другие поведения, которые уже определены для типов таблиц (т. е. для типов таблиц обычно уже определена функция getcolumn
).
Tables.isrowtable
Одним из вариантов для некоторых типов таблиц является определение функции Tables.isrowtable
для автоматического соответствия интерфейсу Tables.jl. Это может быть удобно для «естественных» типов таблиц, в которых уже выполняется итерация строк.
#
Tables.isrowtable
— Function
Tables.isrowtable(x) => Bool
Для удобства некоторые объекты таблиц, которые естественным образом «ориентированы на строки», могут определять Tables.isrowtable(::Type{TableType}) = true
, чтобы упростить соответствие интерфейсу Tables.jl. Ниже приведены требования для определения isrowtable
.
-
Tables.rows(x) === x
, т. е. сам объект таблицы является итераторомRow
. -
Если объект таблицы является изменяемым, он должен поддерживать:
-
push!(x, row)
: allow pushing a single row onto table -
append!(x, rows)
: allow appending set of rows onto table
-
-
Если объект таблицы является индексируемым, он должен поддерживать:
-
x[i] = row
: allow replacing of a row with another row by index
-
Объект таблицы, определяющий функцию Tables.isrowtable
, будет иметь автоматически определенные определения для Tables.istable
, Tables.rowaccess
и Tables.rows
.
Тестирование реализаций Tables.jl
Возникает вопрос о том, какие стратегии лучше всего использовать для тестирования реализации Tables.jl. Продолжая работу с примером MatrixTable
, рассмотрим несколько полезных способов проверки того, что все работает так, как ожидалось.
mat = [1 4.0 "7"; 2 5.0 "8"; 3 6.0 "9"]
Сначала определим матричный литерал с тремя столбцами различных разнотипизированных значений.
# сначала создадим MatrixTable на основе входных данных матрицы
mattbl = Tables.table(mat)
# проверим, что MatrixTable `istable`
@test Tables.istable(typeof(mattbl))
# проверим, что она определяет доступ к строкам
@test Tables.rowaccess(typeof(mattbl))
@test Tables.rows(mattbl) === mattbl
# проверим, что она определяет доступ к столбцам
@test Tables.columnaccess(typeof(mattbl))
@test Tables.columns(mattbl) === mattbl
# проверим, что доступ к первому «столбцу» матрицы можно получить по имени столбца
@test mattbl.Column1 == [1,2,3]
# проверим методы интерфейса `Tables.AbstractColumns`
@test Tables.getcolumn(mattbl, :Column1) == [1,2,3]
@test Tables.getcolumn(mattbl, 1) == [1,2,3]
@test Tables.columnnames(mattbl) == [:Column1, :Column2, :Column3]
# выполним итерацию MatrixTable, чтобы получить первую строку MatrixRow
matrow = first(mattbl)
@test eltype(mattbl) == typeof(matrow)
# теперь можно проверить методы интерфейса `Tables.AbstractColumns`в MatrixRow
@test matrow.Column1 == 1
@test Tables.getcolumn(matrow, :Column1) == 1
@test Tables.getcolumn(matrow, 1) == 1
@test propertynames(mattbl) == propertynames(matrow) == [:Column1, :Column2, :Column3]
Итак, похоже, тип MatrixTable
выглядит неплохо. Он делает все, что мы ожидаем от доступа к его строкам или столбцам с помощью методов API Tables.jl. Тестировать такой источник таблиц довольно просто, поскольку мы просто проверяем, что методы интерфейса делают то, что ожидается.
Хотя в руководстве мы не рассматривали функцию «приемника» для матриц, на самом деле существует функция Tables.matrix
, которая позволяет преобразовать любой входной источник таблицы в обычный объект Julia Matrix
.
Наличие реализаций «источника» и «приемника» Tables.jl (то есть типа, который является совместимым с Tables.jl источником, а также способом потребления других таблиц), позволяет выполнить дополнительное «круговое» тестирование.
rt = [(a=1, b=4.0, c="7"), (a=2, b=5.0, c="8"), (a=3, b=6.0, c="9")]
ct = (a=[1,2,3], b=[4.0, 5.0, 6.0])
В дополнение к нашему объекту mat
мы можем определить пару простых «таблиц». В данном случае rt
— это своего рода стандартная «таблица строк», как Vector
из NamedTuple
, а ct
— это стандартная «таблица столбцов», как NamedTuple
из Vector
. Обратите внимание, что они содержат в основном те же данные, что и матричный литерал ранее, но в немного разных форматах хранения. Эти таблицы «строк» и «столбцов» по умолчанию поддерживаются в Tables.jl благодаря их естественным представлениям таблиц и, следовательно, могут быть отличными инструментами для тестирования табличных интеграций.
# преобразуем таблицу строк в обычный объект матрицы Julia
mat = Tables.matrix(rt)
# проверим, что матрица получилась такой, как ожидалось
@test mat[:, 1] == [1, 2, 3]
@test size(mat) == (3, 3)
@test eltype(mat) == Any
# таким образом, мы успешно создали таблицу, ориентированную на строки,
# теперь попробуем с таблицей, ориентированной на столбцы
mat2 = Tables.matrix(ct)
@test eltype(mat2) == Float64
@test mat2[:, 1] == ct.a
# возьмем входные значения матрицы и создадим на их основе таблицу столбцов
tbl = Tables.table(mat) |> columntable
@test keys(tbl) == (:Column1, :Column2, :Column3)
@test tbl.Column1 == [1, 2, 3]
# сделаем то же самое для таблицы строк
tbl2 = Tables.table(mat2) |> rowtable
@test length(tbl2) == 3
@test map(x->x.Column1, tbl2) == [1.0, 2.0, 3.0]