Julia & Avro.jl :元组问题

Julia & Avro.jl : Issue with tuples

我正在尝试让 Avro 在 Julia 中工作,但遇到了一些实际问题。对于我的应用程序来说,重要的是我使用面向行的数据格式,我可以在生成时逐行附加分层数据结构。

Avro 似乎很合适。但我在 Julia 遇到了问题。我在 Python 测试中工作,但我需要在 Julia 中,因为主要代码在 julia 中。

这是我的简化测试示例,它显示了我的问题。第一个有效,其余的无效。任何帮助,将不胜感激。第二个给出了错误的答案。其余给出错误。

import Avro
v1=Dict("RUTHERFORD"  => 7, "DURHAM" => 11)
buf=Avro.write(v1)
Avro.read(buf,typeof(v1))

输出:

Dict{String, Int64} with 2 entries:
  "DURHAM"     => 11
  "RUTHERFORD" => 7

示例 2:

@show v3=Dict((5,2)  => 7, (5,4) => 11)
@show typeof(v3)
buf=Avro.write(v3)
Avro.read(buf,typeof(v3))

输出:

v3 = Dict((5, 2) => 7, (5, 4) => 11) = Dict((5, 2) => 7, (5, 4) => 11)
typeof(v3) = Dict{Tuple{Int64, Int64}, Int64}
Dict{Tuple{Int64, Int64}, Int64} with 1 entry:
  (40, 53) => 11

示例 3:

@show v2=Dict(("jcm",2)  => 7, ("sem",4) => 11)
@show typeof(v2)
buf=Avro.write(v2)
v2o=Avro.read(buf,typeof(v2))

输出:

v2 = Dict(("jcm", 2) => 7, ("sem", 4) => 11) = Dict(("sem", 4) => 11, ("jcm", 2) => 7)
typeof(v2) = Dict{Tuple{String, Int64}, Int64}
MethodError: Cannot `convert` an object of type Char to an object of type String
Closest candidates are:
  convert(::Type{String}, ::String) at essentials.jl:210
  convert(::Type{T}, ::T) where T<:AbstractString at strings/basic.jl:231
  convert(::Type{T}, ::AbstractString) where T<:AbstractString at strings/basic.jl:232
  ...

Stacktrace:
  [1] _totuple
    @ ./tuple.jl:316 [inlined]
  [2] Tuple{String, Int64}(itr::String)
    @ Base ./tuple.jl:303
  [3] construct(T::Type, args::String; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310
  [4] construct(T::Type, args::String)
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310
  [5] construct(::Type{Tuple{String, Int64}}, ptr::Ptr{UInt8}, len::Int64; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:435
  [6] construct(::Type{Tuple{String, Int64}}, ptr::Ptr{UInt8}, len::Int64)
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:435
  [7] readvalue(B::Avro.Binary, #unused#::Avro.StringType, #unused#::Type{Tuple{String, Int64}}, buf::Vector{UInt8}, pos::Int64, len::Int64, opts::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/binary.jl:247
  [8] readvalue(B::Avro.Binary, MT::Avro.MapType, #unused#::Type{Dict{Tuple{String, Int64}, Int64}}, buf::Vector{UInt8}, pos::Int64, buflen::Int64, opts::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/maps.jl:63
  [9] read(buf::Vector{UInt8}, ::Type{Dict{Tuple{String, Int64}, Int64}}; schema::Avro.MapType, jsonencoding::Bool, kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/binary.jl:58
 [10] read(buf::Vector{UInt8}, ::Type{Dict{Tuple{String, Int64}, Int64}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/binary.jl:58
 [11] top-level scope
    @ In[209]:5
 [12] eval
    @ ./boot.jl:360 [inlined]
 [13] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
    @ Base ./loading.jl:1094

最后一个例子:

v=Dict(("RUTHERFORD", "05A", "371619611022065")   => 7, ("DURHAM", "28","jcm") => 11)
buf=Avro.write(v)
vo=Avro.read(buf,typeof(v))

输出:

MethodError: Cannot `convert` an object of type Char to an object of type String
Closest candidates are:
  convert(::Type{String}, ::String) at essentials.jl:210
  convert(::Type{T}, ::T) where T<:AbstractString at strings/basic.jl:231
  convert(::Type{T}, ::AbstractString) where T<:AbstractString at strings/basic.jl:232
  ...

Stacktrace:
  [1] _totuple
    @ ./tuple.jl:316 [inlined]
  [2] Tuple{String, String, String}(itr::String)
    @ Base ./tuple.jl:303
  [3] construct(T::Type, args::String; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310
  [4] construct(T::Type, args::String)
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:310
  [5] construct(::Type{Tuple{String, String, String}}, ptr::Ptr{UInt8}, len::Int64; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:435
  [6] construct(::Type{Tuple{String, String, String}}, ptr::Ptr{UInt8}, len::Int64)
    @ StructTypes ~/.julia/packages/StructTypes/NJXhA/src/StructTypes.jl:435
  [7] readvalue(B::Avro.Binary, #unused#::Avro.StringType, #unused#::Type{Tuple{String, String, String}}, buf::Vector{UInt8}, pos::Int64, len::Int64, opts::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/binary.jl:247
  [8] readvalue(B::Avro.Binary, MT::Avro.MapType, #unused#::Type{Dict{Tuple{String, String, String}, Int64}}, buf::Vector{UInt8}, pos::Int64, buflen::Int64, opts::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/maps.jl:63
  [9] read(buf::Vector{UInt8}, ::Type{Dict{Tuple{String, String, String}, Int64}}; schema::Avro.MapType, jsonencoding::Bool, kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/binary.jl:58
 [10] read(buf::Vector{UInt8}, ::Type{Dict{Tuple{String, String, String}, Int64}})
    @ Avro ~/.julia/packages/Avro/JEoRa/src/types/binary.jl:58
 [11] top-level scope
    @ In[210]:3
 [12] eval
    @ ./boot.jl:360 [inlined]
 [13] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
    @ Base ./loading.jl:1094

出了什么问题?

根据 Avro specification:

Map keys are assumed to be strings.

这个假设被硬编码到 Avro.jl 中:无论 Dict 键的实际类型是什么,代码 forces the key to be a String. Avro.jl does not bother to check that the key is actually a subtype of String because as long as the type can be converted to a String via the Base.string method,代码都会将该字符串表示形式写入缓冲区。这正是您使用元组键编写 Dict 时发生的情况:

v = Dict((1,2) => 3)
buf = Avro.write(v)
Char.(buf)

这会将 buf 中的字节解码为 ASCII/Unicode 字符并将它们打印到 REPL。您应该在其中看到元组 (1,2) 的字符串表示形式编码为 "(1, 2)":

11-element Vector{Char}:
 '\x01': ASCII/Unicode U+0001 (category Cc: Other, control)
 '\x10': ASCII/Unicode U+0010 (category Cc: Other, control)
 '\f': ASCII/Unicode U+000C (category Cc: Other, control)
 '(': ASCII/Unicode U+0028 (category Ps: Punctuation, open)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
 ',': ASCII/Unicode U+002C (category Po: Punctuation, other)
 ' ': ASCII/Unicode U+0020 (category Zs: Separator, space)
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 ')': ASCII/Unicode U+0029 (category Pe: Punctuation, close)
 '\x06': ASCII/Unicode U+0006 (category Cc: Other, control)
 '[=11=]': ASCII/Unicode U+0000 (category Cc: Other, control)

当您尝试将该键读回元组时出现问题。读取 Map 元素的键时,Avro.jl 将 try to read whatever is in the buffer as a String and stuff it into whatever type the key is. If the type is a Tuple of N types that can be constructed from UInt8 values (eltype(buf)), then the next N UInt8 values in the buffer will be used to create the key:

Avro.read(buf, typeof(v))
# Dict{Tuple{Int64, Int64}, Int64} with 1 entry:
#   (40, 49) => 3

为什么是 40 和 49?因为这些分别是 Chars '(''1' 的 Int64 表示:

Char(40)
# '(': ASCII/Unicode U+0028 (category Ps: Punctuation, open)
Char(49)
# '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)

请注意,这就是为什么您的第二个示例只读取 Dict 中的一个元素,即使写入了两个元素。被解析为键的双元素 Tuple 仅读取字符串表示形式的第一个字符,在您的示例中它们都是 '(''5' 。 Dict 不能有重复的键,所以第二个值简单地覆盖第一个。

如何修复

避免使用非字符串作为键

因为 Avro 规范明确指出 Map 的键被假定为字符串,您可能应该遵循规范并避免使用非字符串作为键。在我看来,Avro.jl 不应该让用户使用不是 AbstractString 子类型的键来编写 Dict。也许这是一个设计选择,或者这可能是一个错误,但为了以防万一,可能值得在项目页面上提交问题。

使用自定义类型作为键

如果您真的非常想使用 String 以外的东西作为键,Avro.jl 在使用 Base.string 将 Map 序列化为缓冲区时,总是会将键转换为 String方法。在反序列化期间,如果代码将键识别为结构,它将尝试将序列化的字符串传递给结构的构造函数。因此,您所要做的就是定义一个带有构造函数的自定义结构,该构造函数采用 String 并使其执行正确的操作(并可选择重载 Base.string 方法)。这是一个例子:

struct XY
    x::Int64
    y::Int64
end
function XY(s::String)
    # parse the default string representation of an XY value
    # very inefficient: for demonstration purposes only
    m = match(r"XY\((\d+), (\d+)\)", s)
    XY(parse.(Int64, m.captures)...)
end

v2 = Dict(XY(1,2) => 3)
buf2 = Avro.write(v2)
Avro.read(buf2, typeof(v2)
# Dict{XY, Int64} with 1 entry:
#   XY(1, 2) => 3

编写自己的元组构造方法

如果你真的,真的,真的想要使用元组作为键,你可以利用 StructType.StringType 并定义你自己的 StructType.construct方法。因为 Avro.jl 使用不安全的指针版本,所以您无法为您的元组定义相同的版本。这是一个尴尬的例子:

function StructTypes.construct(::Type{Tuple{Int64,Int64}}, ptr::Ptr{UInt8}, len::Int; kw...)
    arr = unsafe_wrap(Vector{UInt8}, ptr, len)
    s = join(Char.(arr))
    m = findall(r"\d+", s)
    (parse(Int64, s[m[1]]), parse(Int64, s[m[2]]))
end
Avro.read(buf, typeof(v))
# Dict{Tuple{Int64, Int64}, Int64} with 1 entry:
#   (1, 2) => 3

对于好奇:为什么 Avro.jl 得到正确的值,即使密钥被错误地解析?

在 Avro 的二进制编码方案中,strings are serialized with their lengths stored at the beginning of the string。这允许 Avro.jl 将字符串键的已知长度传递给基于指针的 StructTypes.construct 方法,该方法将 Array{UInt8,1} 传递给 Tuple 构造函数。关于 Julia 的一个有趣事实是,元组的基于可迭代对象的构造函数只会从可迭代对象中读取构造元组所需数量的元素,然后停止。示例:

Tuple{Int64, Int64}([1,2,3,4])
# (1, 2)

所以Avro.jl将一个6元素的Array{UInt8,1} (['(', '1', ',', ' ', '2', ')'])传递给Tuple{Int64,Int64}的构造函数,它依次只读取前两个元素,然后returns Avro.jl 的元组用作 Map 元素的键。 Avro.jl 然后跳到它知道字符串结束的地方(记住:它将字符串的长度存储在缓冲区中)并开始在那里读取 Map 元素的值。 Avro.jl 知道该值应该是一个 Int64,并且它知道如何解析一个 Int64,因此它会读取适当的值。整洁!