Pidro
Pidro

Reputation: 153

Python: How to use ast.literal_eval() for sets in set?

Sets are not hashable as they are mutable. But is there a way to still use literal_eval on {1, 2, {3, 4}}? I just want to know that the outer structure is a set, I don't care about the inner type, but sets within sets are possible inputs.

Update:

The input is read from a file as string.

Upvotes: 2

Views: 2856

Answers (2)

Serge Ballesta
Serge Ballesta

Reputation: 148910

You can hack ast.literal_eval to make it return a frozenset when it sees a set. Here is how to do:

  • search where the library for your Python installation is
  • it contains the file ast.py that contains the function literal_eval
  • copy that function (using a different name) in you own module and change it to import all the relevant names from the ast module
  • in the line processing a Set, replace the generation of a set with a frozenset

You can then use it to safely parse literal sets containing sets. For my Python 3.5, I used:

def frozen_literal_eval(node_or_string):
    """
    Safely evaluate an expression node or a string containing a Python
    expression.  The string or node provided may only consist of the following
    Python literal structures: strings, bytes, numbers, tuples, lists, dicts,
    sets, booleans, and None.

    SPECIAL: This version uses frozensets instead of sets
    """
    # SPECIAL: import names from ast module
    from ast import parse, Expression, Str, Bytes, Num, Tuple, List, Set, Dict
    from ast import NameConstant, UnaryOp, UAdd, USub, BinOp, Add, Sub
    # END SPECIAL
    if isinstance(node_or_string, str):
        node_or_string = parse(node_or_string, mode='eval')
    if isinstance(node_or_string, Expression):
        node_or_string = node_or_string.body
    def _convert(node):
        if isinstance(node, (Str, Bytes)):
            return node.s
        elif isinstance(node, Num):
            return node.n
        elif isinstance(node, Tuple):
            return tuple(map(_convert, node.elts))
        elif isinstance(node, List):
            return list(map(_convert, node.elts))
        elif isinstance(node, Set):
            #SPECIAL: returns a frozenset
            return frozenset(map(_convert, node.elts))
            # END SPECIAL
        elif isinstance(node, Dict):
            return dict((_convert(k), _convert(v)) for k, v
                        in zip(node.keys, node.values))
        elif isinstance(node, NameConstant):
            return node.value
        elif isinstance(node, UnaryOp) and \
             isinstance(node.op, (UAdd, USub)) and \
             isinstance(node.operand, (Num, UnaryOp, BinOp)):
            operand = _convert(node.operand)
            if isinstance(node.op, UAdd):
                return + operand
            else:
                return - operand
        elif isinstance(node, BinOp) and \
             isinstance(node.op, (Add, Sub)) and \
             isinstance(node.right, (Num, UnaryOp, BinOp)) and \
             isinstance(node.left, (Num, UnaryOp, BinOp)):
            left = _convert(node.left)
            right = _convert(node.right)
            if isinstance(node.op, Add):
                return left + right
            else:
                return left - right
        raise ValueError('malformed node or string: ' + repr(node))
    return _convert(node_or_string)

I could the use:

>>> s = '{ 1, 2, {3, 4}}'
>>> frozen_literal_eval(s)
frozenset({1, 2, frozenset({3, 4})})

Upvotes: 2

Moinuddin Quadri
Moinuddin Quadri

Reputation: 48077

Answer to Question 1:

Are sets within sets are possible input?

You may use frozenset to create a nested set object:

Return a new set or frozenset object whose elements are taken from iterable. The elements of a set must be hashable. To represent sets of sets, the inner sets must be frozenset objects. If iterable is not specified, a new empty set is returned.

For example:

>>> frozen_set = frozenset({3, 4})

>>> my_set = {1, 2, frozen_set}
>>> my_set
{frozenset({3, 4}), 2, 1}

Answer to Question 2:

Is there a way to still use literal_eval on {1, 2, {3, 4}}?

No, there is no way as there is no ast.literal_eval for frozenset iterable. There was a proposal in 2008 by Raymond Hettinger for frozenset literal. Guido even pronounced his agreement but then changed his mind. Check this mail for more insights.

But you may use eval instead as:

>>> my_str = '{frozenset({3, 4}), 2, 1}'

>>> eval(my_str)
{1, 2, frozenset({3, 4})}

Before using eval, please also read: Why should exec() and eval() be avoided?

Upvotes: 1

Related Questions