5

Is it possible to have 'optional' keys in a dict literal, rather than add them in in if statements?

Like so:

a = True
b = False
c = True
d = False

obj = {
    "do_a": "args for a" if a,
    "do_b": "args for b" if b,
    "do_c": "args for c" if c,
    "do_d": "args for d" if d,
}

#expect:
obj == {
    "do_a": "args for a",
    "do_c": "args for c",
}

Edit for context: I know how to do the logic :) I just want to avoid if statements because my object is a big block of data that represents declarative logic, so moving stuff around is a bit like "spaghetti coding" something that isn't meant to be procedural at all. I want the value of the object to "look like what it means" as a query.

It's actually an elasticsearch query, so it will look like this:

{
    "query": {
        "bool": {
            "must": [
                 <FILTER A>,
                 <FILTER B>,  # would like to make this filter optional
                 <FILTER C>,
                 {
                     "more nested stuff" : [ ... ]
                 }
             ],
             "other options": [ ... ]
        },
        "other options": [ ... ]
    },
    "other options": [ ... ]
}

and my possibly-dubious goal was to make it look like a query that you could look at and understand the shape of it, without having to trace through ifs. ie, without having "filters": [f for f in filters if f.enabled] because then you have to go and look for the filters, which are all optional-constants anyway here

JamEnergy
  • 720
  • 8
  • 21

4 Answers4

10

I think the answer is 'no', as stated by other answers, but here is the closest I've gotten so far...

It's slightly on the 'abhorrent' side of 'wtf' though

a = True
b = False
c = True
d = False

obj = {
    **({"do_a": "args for a"} if a else {}),
    **({"do_b": "args for b"} if b else {}),
    **({"do_c": "args for c"} if c else {}),
    **({"do_d": "args for d"} if d else {}),
}

#expect:
assert(obj == {
        "do_a": "args for a",
        "do_c": "args for c",
    })

or if you want to put the optional-ness in some function:

def maybe(dictionary, condition, default=None):
    return dictionary if condition else default or {}

obj = {
    **maybe({"do_a": "args for a"}, a),
    **maybe({"do_b": "args for b"}, b),
    **maybe({"do_c": "args for c"}, c),
    **maybe({"do_d": "args for d"}, d),
}

The problem with this kind of code is that the conditions get further and further from the results (imagine that we end up passing large dicts to the first param in maybe).

JamEnergy
  • 720
  • 8
  • 21
5

Start by defining your lists of keys, arguments, and boolean variables:

keys = ["do_a", "do_b", ...]
args = ["args for a", "args for b", ...]
mask = [a, b, ...]

Now, construct obj using the mask list to determine what keys are inserted:

obj = {k : a for k, a, m in zip(keys, args, mask) if m}

Alternatively,

obj = {}
for k, a, m in zip(keys, args, mask):
    if m:
        obj[k] = a
cs95
  • 379,657
  • 97
  • 704
  • 746
5

No, you can't have "optional values" in a literal. The results of the expressions in the literal are always inserted into the result.

However I would argue that an explicit if statement would probably be better to follow anyway:

a = True
b = False
c = True
d = False

obj = {}
if a: obj["do_a"] = "args for a"
if b: obj["do_b"] = "args for b"
if c: obj["do_c"] = "args for c"
if d: obj["do_d"] = "args for d"

Just to mention a few alternatives in case you really don't like the ifs:

  • You could also use a different value in case the argument is "false"-ey and then filter the dictionary:

    _to_remove = object()
    
    obj = {
        "do_a": "args for a" if a else _to_remove,
        "do_b": "args for b" if b else _to_remove,
        "do_c": "args for c" if c else _to_remove,
        "do_d": "args for d" if d else _to_remove,
    }
    
    obj = {key: value for key, value in obj.items() if value is not _to_remove}
    
  • Or use itertools.compress and the dict builtin:

    key_value_pairs = [
        ("do_a", "args for a"), 
        ("do_b", "args for b"), 
        ("do_c", "args for c"), 
        ("do_d", "args for d")
    ]
    
    from itertools import compress
    
    obj = dict(compress(key_value_pairs, [a, b, c, d]))
    
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • Building a dictionary and then removing those keys seems a bit excessive, and indeed may be worse when there are more to remove than keep. – cs95 Jun 30 '18 at 22:21
  • 1
    Agreed - but I don't think it's really excessive because at some point all the keys, values and conditions have to exist. If they are in a list or a dict doesn't make _much_ difference. And in the end you always end up with a dictionary that contains the desired items and you need some sort of loop for that (or repeated ifs). In case this is really the bottleneck (be it speed or memory) then it may be significant that dicts allocate slightly more space than similar lists/tuples and that it does more `if`s. But note that this only affects my second approach I'd really go with the first. – MSeifert Jun 30 '18 at 22:28
  • 1
    I realize that I phrased this question asking for a boolean as the answer, so I suppose your response is the only candidate that fits the signature so far, since the others return suggestions rather than stating whether or not the thing is possible - your one starts with a "No". – JamEnergy Jul 01 '18 at 18:36
2

Here is a different approach. Given your same definitions of:

a = True
b = False
c = True
d = False

You can then construct your literals as three member tuples:

li=[
    ("do_a", "args for a",a),
    ("do_b", "args for b",b),
    ("do_c", "args for c",c),
    ("do_d", "args for d",d)
]

This is equivalent to using zip with three lists of literals but perhaps easier for human eyes to understand the intent with shorter lists.

And then construct your dict conditionally as so:

>>> dict([(k,v) for k,v,f in li if f])
{'do_c': 'args for c', 'do_a': 'args for a'}

With the clarification in the post, you can use a function as a dict value and simply call the function as the dict is created (or as you update the dict) :

def filter_a():
    # some expensive function...
    return "<FILTER A>"

def filter_b():
    return "<FILTER B>"  

def filter_c():
    return "<FILTER C>"    

def filter_d():
    return "<FILTER D>" 

li=[
    ("do_a", filter_a, a),
    ("do_b", filter_b, b),
    ("do_c", filter_c, c),
    ("do_d", filter_d, d)
]

Then only the related filter functions are called as constructed:

>>> dict((k,v()) for k,v,f in li if f)
{'do_c': '<FILTER C>', 'do_a': '<FILTER A>'}

Then B and D are never called.

Better still, write the logic so that FILTER X is a form of generator and returns data only as needed.

dawg
  • 98,345
  • 23
  • 131
  • 206
  • I like the generator idea, I'm going to see if I can find a way of composing such things such that the resultant syntax doesn't obscure the "shape" of my object. Also wondering if I can use the double splat strategy for creating dicts... sort of like {**({things} if X else {})} – JamEnergy Jul 01 '18 at 18:29