Документация Engee

Документация по 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.rowsFunction

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.columnsFunction

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.SchemaType

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.schemaFunction

Tables.schema(x) => Union{Nothing, Tables.Schema}

Пытается получить схему объекта, возвращаемого Tables.rows или Tables.columns. Если итератор AbstractRow или объект AbstractColumns не может определить свою схему, возвращается nothing. В противном случае возвращается объект Tables.Schema, содержащий имена столбцов и типы, доступные для использования.

# Tables.subsetFunction

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.partitionsFunction

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.partitionerFunction

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.rowtableFunction

Tables.rowtable(x) => Vector{NamedTuple}

Принимает источник входной таблицы и создает вектор (Vector) именованных кортежей (NamedTuple), также известный как «таблица строк». Это своего рода тип таблицы по умолчанию, поскольку она естественным образом соответствует интерфейсу строк Tables.jl, т. е. Vector естественным образом итерирует свои элементы, а NamedTuple соответствует интерфейсу AbstractRow по умолчанию (позволяет индексировать значение по индексу, имени и получать все имена).

Сведения о «ленивом» итераторе строк см. в описании rows и namedtupleiterator.

Не для использования с очень широкими таблицами с количеством столбцов, превышающим 67 тыс. Текущие фундаментальные ограничения компилятора не позволяют строить NamedTuple такого размера.

# Tables.columntableFunction

Tables.columntable(x) => NamedTuple of AbstractVectors

Принимает источник входной таблицы x и возвращает именованный кортеж (NamedTuple) векторов (AbstractVector), также известный как «таблица столбцов». Это своего рода тип таблицы по умолчанию, поскольку она естественным образом соответствует интерфейсу столбцов Tables.jl.

Обратите внимание, что если x является объектом, в котором столбцы хранятся как векторы, проверка того, что эти векторы используют индексацию на основе 1, не выполняется (это должно быть обеспечено при построении x).

Не для использования с очень широкими таблицами с количеством столбцов, превышающим 67 тыс. Текущие фундаментальные ограничения компилятора не позволяют строить NamedTuple такого размера.

# Tables.dictrowtableFunction

Tables.dictrowtable(x) => Tables.DictRowTable

Принимает любой совместимый с Tables.jl источник x и возвращает таблицу DictRowTable, которую можно представить как вектор (Vector) строк OrderedDict, сопоставляющий имена столбцов в виде символов (Symbol) со значениями. Порядок столбцов входной таблицы сохраняется с помощью Tables.schema(::DictRowTable).

Для входных таблиц «без схемы» dictrowtable использует поведение «объединения столбцов», а не выводит схему из первой строки, как Tables.columns. Это означает, что при итерации строк каждое значение из строки объединяется в итоговый набор столбцов. Это особенно полезно, когда строки входной таблицы могут не включать столбцы, если значение отсутствует, вместо того чтобы включать фактическое значение missing, что часто встречается, например, в json. При этом возникают затраты на отслеживание всех видимых значений и вывод окончательных объединенных схем, поэтому рекомендуется использовать только в тех случаях, когда необходимо поведение объединения.

# Tables.dictcolumntableFunction

Tables.dictcolumntable(x) => Tables.DictColumnTable

Принимает любой совместимый с Tables.jl источник x и возвращает таблицу DictColumnTable, которую можно представить как словарь (OrderedDict), сопоставляющий имена столбцов в виде символов (Symbol) с векторами AbstractVector. Порядок столбцов входной таблицы сохраняется с помощью Tables.schema(::DictColumnTable).

Для входных таблиц «без схемы» dictcolumntable использует поведение «объединения столбцов», а не выводит схему из первой строки, как Tables.columns. Это означает, что при итерации строк каждое значение из строки объединяется в итоговый набор столбцов. Это особенно полезно, когда строки входной таблицы могут не включать столбцы, если значение отсутствует, вместо того чтобы включать фактическое значение missing, что часто встречается, например, в json. При этом возникают затраты на отслеживание всех видимых значений и вывод окончательных объединенных схем, поэтому рекомендуется использовать только в тех случаях, когда это необходимо.

# Tables.namedtupleiteratorFunction

Tables.namedtupleiterator(x)

Передает любой источник входных данных таблицы и возвращает итератор NamedTuple.

См. также описание rows и rowtable.

Не для использования с очень широкими таблицами с количеством столбцов, превышающим 67 тыс. Текущие фундаментальные ограничения компилятора не позволяют строить NamedTuple такого размера.

# Tables.datavaluerowsFunction

Tables.datavaluerows(x) => NamedTuple iterator

Принимает любые входные данные таблицы x и возвращает итератор NamedTuple, который заменит отсутствующие значения значениями, заключенными в DataValue. Это позволяет любому табличному типу соответствовать интерфейсу интеграции TableTraits.jl Queryverse, определяя следующее:

IteratorInterfaceExtensions.getiterator(x::MyTable) = Tables.datavaluerows(x)

# Tables.nondatavaluerowsFunction

Tables.nondatavaluerows(x)

Принимает любой источник итератора NamedTuple, совместимый с Queryverse, и преобразует в итератор AbstractRow, совместимый с Tables.jl. Автоматически распакует любой DataValue, заменяя NA на missing. Полезно для преобразования результатов Query.jl обратно в таблицы, не основанные на DataValue.

# Tables.tableFunction

Tables.table(m::AbstractVecOrMat; [header])

Заключает в оболочку AbstractVecOrMat (Matrix, Vector, Adjoint и т. д.) в MatrixTable, что соответствует интерфейсу Tables.jl. (AbstractVector рассматривается как матрица с одним столбцом.) Это позволяет получать доступ к матрице с помощью Tables.rows и Tables.columns. Можно передать итератор header необязательного именованного аргумента, который будет преобразован в Vector{Symbol} для использования в качестве имен столбцов. Обратите внимание, что AbstractVecOrMat не копируется.

# Tables.matrixFunction

Tables.matrix(table; transpose::Bool=false)

Материализует входные данные источника таблицы в виде новой матрицы (Matrix) или в случае с MatrixTable возвращает первоначально заключенную в оболочку матрицу. Если типы элементов столбцов таблицы являются разнородными, они путем продвижения будут приведены к общему типу в материализованной матрице (Matrix). Обратите внимание, что имена столбцов игнорируются в преобразовании. По умолчанию столбцы входной таблицы будут материализованы как соответствующие столбцы матрицы. Передача transpose=true транспонирует входные данные с входными столбцами как строки матрицы, или в случае с MatrixTable применяет permutedims к первоначально заключенной в оболочку матрице.

# Tables.eachcolumnFunction

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.materializerFunction

Tables.materializer(x) => Callable

Для входных данных таблицы возвращает функцию «приемника» или «материализации», которая может принять совместимые с Tables.jl входные данные таблицы и создать экземпляр табличного типа. Это позволяет выполнять рабочие процессы «преобразования», которые принимают входные данные таблицы, применяют преобразования, потенциально преобразуя таблицу в другую форму, и в итоге создают таблицу того же типа, как и у исходных данных. Материализатором по умолчанию является Tables.columntable, который преобразует любые входные данные таблицы в именованный кортеж (NamedTuple) векторов (Vector).

Рекомендуется, чтобы пользователи, реализующие MyType, определяли только materializer(::Type{<:MyType}). materializer(::MyType) затем будет автоматически делегирован этому методу.

# Tables.columnindexFunction

Tables.columnindex(table, name::Symbol)

Возвращает индекс столбца (на основе 1) для столбца по имени (name) в таблице с известной схемой, Возвращает 0, если имя (name) не существует в таблице.

Если заданы имена и символ name, вычисляет индекс (на основе 1) имени в именах.

# Tables.columntypeFunction

Tables.columntype(table, name::Symbol)

Возвращает тип элемента столбца для столбца по имени (name) в таблице с известной схемой, Возвращает Union{}, если имя (name) не существует в таблице.

Если заданы тип кортежа и символ name, вычисляет тип имени в типах кортежей.

# Tables.rowmergeFunction

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.RowType

Tables.Row(row)

Вспомогательный тип, позволяющий заключать в оболочку любой объект интерфейса AbstractRow в выделенной структуре для обеспечения полезного поведения по умолчанию (позволяет использовать любой AbstractRow как NamedTuple):

  • Определен интерфейс индексирования; т. е. row[i] возвратит значение столбца по индексу i, row[nm] возвратит значение столбца для имени столбца nm.

  • Определен интерфейс доступа к свойствам; т. е. row.col1 получит значение для столбца с именем col1.

  • Определен интерфейс итерации; т. е. for x in row будет итерировать каждое значение столбца в строке.

  • Определены методы AbstractDict (get, haskey и т. д.) для проверки и получения значений столбцов.

# Tables.ColumnsType

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 интерфейс преобразования в правильную таблицу прост:

Обязательные методы Определение по умолчанию Краткое описание

Tables.istable(::Type{MyTable})

Объявляет, что тип таблицы реализует интерфейс.

Один из следующих:

Tables.rowaccess(::Type{MyTable})

Объявляет, что тип таблицы определяет метод Tables.rows(::MyTable).

Tables.rows(x::MyTable)

Возвращает совместимый с Tables.AbstractRow итератор из таблицы.

Или:

Tables.columnaccess(::Type{MyTable})

Объявляет, что тип таблицы определяет метод Tables.columns(::MyTable).

Tables.columns(x::MyTable)

Возвращает совместимый с Tables.AbstractColumns объект из таблицы.

Необязательные методы

Tables.schema(x::MyTable)

Tables.schema(x) = nothing

Возвращает объект Tables.Schema из итератора Tables.AbstractRow или объекта Tables.AbstractColumns; или nothing для неизвестной схемы.

Tables.materializer(::Type{MyTable})

Tables.columntable

Объявляет для типа таблицы функцию приемника materializer, которая может сконструировать экземпляр вашего типа из любых входных данных Tables.jl.

Tables.subset(x::MyTable, inds; viewhint)

Возвращает строку или подтаблицу исходной таблицы.

DataAPI.nrow(x::MyTable)

Возвращает количество строк таблицы x.

DataAPI.ncol(x::MyTable)

Возвращает количество столбцов таблицы x.

В зависимости от того, какой тип таблицы определен (Tables.rows или Tables.columns), вы убедитесь, что итератор Tables.AbstractRow или объект Tables.AbstractColumns удовлетворяет соответствующему интерфейсу.

Для получения дополнительных сведений см. эту публикацию, в которой подробно описывается процесс создания таблицы, ориентированной на строки.

Tables.AbstractRow

# Tables.AbstractRowType

Tables.AbstractRow

Абстрактный тип интерфейса, представляющий ожидаемый тип элемента (eltype) итератора, возвращаемый из Tables.rows(table). Tables.rows должен возвращать итератор элементов, соответствующих интерфейсу Tables.AbstractRow. Хотя Tables.AbstractRow является абстрактным типом, который пользовательские типы «столбцов» могут разделить на подтипы для полезного поведения по умолчанию (индексирование, итерация, доступ к свойствам и т. д.), пользователи не должны использовать его для диспетчеризации, поскольку объекты интерфейса Tables.jl не должны выделять подтипы, а только реализовывать обязательные методы интерфейса.

Определение интерфейса:

Обязательные методы Определение по умолчанию Краткое описание

Tables.getcolumn(row, i::Int)

getfield(row, i)

Получает значение столбца по индексу.

Tables.getcolumn(row, nm::Symbol)

getproperty(row, nm)

Получает значение столбца по имени.

Tables.columnnames(row)

propertynames(row)

Возвращает имена столбцов для строки в виде индексируемой коллекции на основе 1.

Необязательные методы

Tables.getcolumn(row, ::Type{T}, i::Int, nm::Symbol)

Tables.getcolumn(row, nm)

Получает значение столбца, учитывая тип элемента столбца T, индекс i и имя столбца 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.AbstractColumnsType

Tables.AbstractColumns

Тип интерфейса, определяемый как упорядоченный набор столбцов, которые поддерживают извлечение отдельных столбцов по имени или индексу. Извлеченный столбец должен быть основанной на 1 индексируемой коллекцией известной длины, т. е. объектом, который поддерживает length(col) и col[i] для любого i = 1:length(col). Tables.columns должен возвращать объект, соответствующий интерфейсу Tables.AbstractColumns. Хотя Tables.AbstractColumns является абстрактным типом, который пользовательские типы «столбцов» могут разделить на подтипы для полезного поведения по умолчанию (индексирование, итерация, доступ к свойствам и т. д.), пользователи не должны использовать его для диспетчеризации, поскольку объекты интерфейса Tables.jl не должны выделять подтипы, а только реализовывать обязательные методы интерфейса.

Определение интерфейса:

Обязательные методы Определение по умолчанию Краткое описание

Tables.getcolumn(table, i::Int)

getfield(table, i)

Получает столбец по индексу.

Tables.getcolumn(table, nm::Symbol)

getproperty(table, nm)

Получает столбец по имени.

Tables.columnnames(table)

propertynames(table)

Возвращает имена столбцов для таблицы в виде индексируемой коллекции на основе 1.

Необязательные методы

Tables.getcolumn(table, ::Type{T}, i::Int, nm::Symbol)

Tables.getcolumn(table, nm)

Получает столбец, учитывая тип элемента столбца T, индекс i и имя столбца 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.isrowtableFunction

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]