What is the equivalent of Python's ast.literal_eval() in Julia?

我的未来我决定 提交于 2019-12-04 19:31:38


Is there anything in Julia which is equivalent to Python's literal_eval provided by the package ast (Abstract Syntax Tree)?

A summary of its (literal_eval) description:

This function only evaluates Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None, and can be used for safely evaluating strings from untrusted sources without the need to parse the values oneself. It is not capable of evaluating arbitrarily complex expressions, for example involving operators or indexing.


There is no equivalent, although you could potentially write one fairly easily by parsing code and then recursively ensuring that you only have certain syntactic forms in the resulting expression before evaluating it. However, unlike Python where a lot of basic types and their syntax and behavior are built in and unchangeable, Julia's "built in" types are just user-defined types that happen to be defined before the system starts up. Let's explore what happens, for example, when you use vector literal syntax:

julia> :([1,2,3]) |> dump
  head: Symbol vect
  args: Array{Any}((3,))
    1: Int64 1
    2: Int64 2
    3: Int64 3
  typ: Any

julia> f() = [1,2,3]
f (generic function with 2 methods)

julia> @code_lowered f()
        return (Base.vect)(1, 2, 3)

julia> methods(Base.vect)
# 3 methods for generic function "vect":
vect() in Base at array.jl:63
vect(X::T...) where T in Base at array.jl:64
vect(X...) in Base at array.jl:67

So [1,2,3] is just a syntactic form that is lowered as a call to the Base.vect function, i.e. Base.vect(1,2,3). Now, we might in the future make it possible to "seal" some functions so that one can't add any submethods or overwrite their behavior in any way, but currently modifying the behavior of Base.vect for some set of arguments is entirely possible:

julia> function Base.vect(a::Int, b::Int, c::Int)
           return invoke(Base.vect, Tuple{Any,Any,Any}, a, b, c)

julia> [1,2,3]
3-element Array{Int64,1}:

Since an array literal is overloadable in Julia it's not really a purely literal syntax. Of course, I don't recommend doing what I just did – "SURPRISE!" is not something you want to see in the middle of your program – but it is possible and therefore the syntax is not "safe" in the sense of this question. Some other constructs which are expressed with literals in Python or JavaScript (or most scripting languages), are explicitly function calls in Julia, such as constructing dictionaries:

julia> Dict(:foo => 1, :bar => 2, :baz => 42)
Dict{Symbol,Int64} with 3 entries:
  :baz => 42
  :bar => 2
  :foo => 1

This is just a function call to the Dict type with three pair object arguments, not a literal syntax at all. The a => b pair syntax itself is also just a special syntax for a function call to the => operator, which is an alias for the Pair type:

julia> dump(:(a => b))
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol =>
    2: Symbol a
    3: Symbol b
  typ: Any

julia> :foo => 1.23

julia> =>

julia> Pair(:foo, 1.23)

What about integer literals? Surely those are safe! Well, yes and no. Small integer literals are currently safe, since they are converted in the parser directly to Int values, without any overloadable entry points (that could change in the future, however, allowing user code to opt into different behaviors for integer literals). Large enough integer literals, however, are lowered to macro calls, for example:

julia> :(18446744073709551616)
:(@int128_str "18446744073709551616")

An integer literal that is too big for the Int64 type is lowered as macro call with a string argument containing the integer digits, allowing the macro to parse the string and return an appropriate integer object – in this case an Int128 value – to be spliced into the abstract syntax tree. But you can define new behaviors for these macros:

julia> macro int128_str(s)
           warn("BIG SUPRISE!")

julia> 18446744073709551616

Essentially, there is no meaningful "safe literal subset" of Julia. Philosophically, Julia is very different from Python: instead of building in a fixed set of types with special capabilities that are inaccessible to user-defined types, Julia includes powerful enough mechanisms in the language that the language can be built from within itself – a process known as "bootstrapping". These powerful language mechanisms are just as available to Julia programmers as they are to the programmers of Julia. This is where much of Julia's flexibility and power comes from. But with great power comes great responsibility and all that... so don't actually do any of the things I've done in this answer unless you have a really good reason :)

To get back to your original problem, the best one could do to create a parser for safe literal object construction using Julia syntax would be to implement a parser for a subset of Julia, giving literals their usual meaning in a way that cannot be overloaded. This safe syntax subset could include numeric literals, string literals, array literals, and Dict constructors, for example. But it would probably be more practical to just use JSON syntax and parse it using Julia's JSON package.


I know I'm late here, but Meta.parse does the job:

 julia> eval(Meta.parse("[1,2,3]"))
 3-element Array{Int64,1}:

Specifically, Meta.parse turns your string into an Expr which eval then turns into a useable data structure. Definitely works in Julia 1.0. https://discourse.julialang.org/t/how-to-convert-a-string-into-an-expression/11160