marlon
marlon

Reputation: 7653

Is there a better way to use multiple OR in dict comprehension?

x = {'a': (2, 3), 'b': (4, 1), 'c': (3, 2), 'd': (1, 0), 'e': (2, 0)} 
x = {
    k: v
    for k, v in x.items()
    if (v[0] >= 2 and v[1] >= 0) or (v[0] >= 1 and v[1] > 1)
}
print(x)

In the 'if' block, I have a lot of logical 'or' enclosed with parenthesis. Is there better way to express it in dict comprehension?

Also, I can omit the () in the 'if' statement to make it more consise. Right?

Upvotes: 2

Views: 101

Answers (4)

cards
cards

Reputation: 4975

I think the question is a bit bad-posed since expect to deal with an uncountable many ors is independent of the comprehension-form.

My suggestion is to use a systematic approach to deal with a chain of (irreducible) or conjunctions based on an auxiliary function.

General idea: form two groups wrt the index of the values of the dictionary, i.e. v0, v1, together with an own collection of unitary comparison operators ( for short UCO). Then evaluate each group with the proper UCO, apply a termwise and to the "product" of both groups and finally reduce with an any.

The key step is hence the construction of the unitary comparison operators UCO. It depends on methodcaller. Notice that are some implicit data which are very important because are invariant of the iterations: the values that you use to make the comparison. These will be classified with dict as following:

  • lb: lower bound
  • lbe: lower bound also equal to it
  • ub: upper bound
  • ube: upper bound also equal to it
  • neq: not equal to
  • eq: equal to and used with a mapper which assigns the corresponding name name of the method. It is not mandatory but could avoid confusions with the order operators, here an example:
lb = 4

2 > lb                 # binary inequality         greater than
int(lb).__lt__(2)      # unitary inequality (UCO)  less than

To deal with the invariance of boundaries I used a decorator (not mandatory).

Here my solution:

from operator import methodcaller, __and__


def systematic_comparison(v0_boundaries, v1_boundaries):
    # v0_boundaries, v1_boundaries: both tuple of dictionaries, ({2: 'lbe'}, {1: 'lbe'})
    def __systematic_comparison(v0, v1):
        # mapper boundary type - order relation
        mapper = {'lb': '__gt__', 'lbe': '__ge__', 'ub': '__lt__', 'ube': '__le__', 'ne': '__ne__', 'eq': '__eq__'}

        # make unitary comparison operators
        v0_comparisons = []
        for d in v0_boundaries:
            k, v = tuple(d.items())[0]
            v0_comparisons.append(methodcaller(mapper[v], k))
        v1_comparisons = []
        for d in v1_boundaries:
            k, v = tuple(d.items())[0]
            v1_comparisons.append(methodcaller(mapper[v], k))

        # evaluate unitary comparisons operator
        v0_checks = [comp(v0) for comp in v0_comparisons]
        v1_checks = [comp(v1) for comp in v1_comparisons]

        # pairwise-and separated by or
        return any(map(__and__, v0_checks, v1_checks))

    return __systematic_comparison


x = {'a': (2, 3), 'b': (4, 1), 'c': (3, 2), 'd': (1, 0), 'e': (2, 0)}
x = {
    k: (v0, v1)
    for k, (v0, v1) in x.items()
    if systematic_comparison(v0_boundaries=({2: 'lbe'}, {1: 'lbe'}), v1_boundaries=({0: 'lbe'}, {1: 'lb'}))(v0, v1)
}
print(x)
#{'a': (2, 3), 'b': (4, 1), 'c': (3, 2), 'e': (2, 0)}

The code can be made a bit more compact like this:

def systematic_comparison(v0_boundaries, v1_boundaries):
    # v0_boundaries, v1_boundaries: both tuple of dictionaries, ({2: 'lbe'}, {1: 'lbe'})
    def __systematic_comparison(v0, v1):
        # mapper boundary type - order relation
        mapper = {'lb': '__gt__', 'lbe': '__ge__', 'ub': '__lt__', 'ube': '__le__', 'ne': '__ne__', 'eq': '__eq__'}

        # make unitary comparison operators
        v_comparisons = []
        for d in v0_boundaries + v1_boundaries: # merge together -> size double
            k, v = tuple(d.items())[0]
            v_comparisons.append(methodcaller(mapper[v], k))
        
        # size of half list of v_comparisons
        n = len(v0_boundaries)

        # groupwise comparison and pairwise-and separated by or
        return any(map(__and__, [comp(v0) for comp in v_comparisons[:n]], [comp(v1) for comp in v_comparisons[n:]]))

    return __systematic_comparison

Here an example of the syntax with 3 or more conditions:

v0_boundaries = ({2: 'lbe'}, {1: 'lbe'}, {8: 'ne'}, ...)
v1_boundaries = ({0: 'lbe'}, {1: 'lb'}, {10: 'eq'}, ...)

systematic_comparison(v0_boundaries, v1_boundaries)

Upvotes: 0

0x263A
0x263A

Reputation: 1859

One option is to write a function that checks for whether or not you want to include the item:

def my_filter(tup):
    if tup[0] >= 2 and tup[1] >= 0:
        return True
    else: #Chain whatever you want as elif
        return tup[0] >= 1 and tup[1] > 1


x = {'a': (2, 3), 'b': (4, 1), 'c': (3, 2), 'd': (1, 0), 'e': (2, 0)} 
y = {
    k: v
    for k, v in x.items()
    if my_filter(v)
}
print(y)

Or you could wrap both conditions with any:

z = {
    k: v
    for k, v in x.items()
    if any(
        [v[0] >= 2 and v[1] >= 0,
         v[0] >= 1 and v[1] > 1]
    )
}
print(z)

Both of these will output:

{'a': (2, 3), 'b': (4, 1), 'c': (3, 2), 'e': (2, 0)}

Upvotes: 0

BrokenBenchmark
BrokenBenchmark

Reputation: 19242

You can unpack the tuple. Keep the parentheses, even though you can technically get away with it; and has a higher precedence than or, but not everyone knows that and this makes it more readable:

x = {k: (fst, snd) for k, (fst, snd) in x.items() if (fst >= 2 and snd >= 0) or (fst >= 1 and snd > 1)}

This outputs:

{'a': (2, 3), 'b': (4, 1), 'c': (3, 2), 'e': (2, 0)}

Upvotes: 3

Marcus Müller
Marcus Müller

Reputation: 36352

In the 'if' block, I have a lot of logical 'or' enclosed with parenthesis. Is there better way to express it in dict comprehension?

No. Your both "main" conditions have one constraint that is a superset of the same constraint of the other side, and one that is incompatible.

Three boolean operators is the minimum you can do.

Upvotes: 1

Related Questions