4
\$\begingroup\$

Suppose we have variables, num and let, that may be (possibly nested) list, tuple, or None. The combination is to be such that:

  1. Neither == None -> num + let
  2. num == None -> let
  3. let == None -> num
  4. Both == None -> None
  5. Return type is list or None
  6. 1-5 should hold for any number of variables, including one, and >2
  7. Be a one-liner (one statement)

My approach, with test:

def join(_vars):
    return [x for v in _vars if v for x in v] or None

num = ((1, 2), None, [1, (2, 3)], None)
let = (('a', 'b'), ['a', ('b', 'c')], None, None)
vars_all = (let, num)

for i, _vars in enumerate(zip(*vars_all)):
    print(f"Case {i + 1}:", join(_vars))
Case 1: ['a', 'b', 1, 2]
Case 2: ['a', ('b', 'c')]
Case 3: [1, (2, 3)]
Case 4: None

Any better way to accomplish this? E.g. shorter, or more readable, or more intuitive than nested comprehensions.

(For readability I'd opt for less abstract naming: [x for var in _vars if bool(var) for x in var]. Parentheses should also help, though unsure if can insert without changing results.)

\$\endgroup\$
2
  • \$\begingroup\$ Your SE handle sounds like a Final Fantasy boss. \$\endgroup\$ – Reinderien Apr 27 '20 at 13:59
  • \$\begingroup\$ @Reinderien The sage of the six paths, from Naruto. Unfamiliar with FF, but which boss, I wonder? \$\endgroup\$ – OverLordGoldDragon Apr 28 '20 at 15:00
4
\$\begingroup\$

As you mentioned, the nested comprehension is not very intuitive. I had to write it out for understanding.

I am also not sure I understood your requirements correctly. Please correct any wrong assumptions. These are the attempts:

from itertools import chain

def join(vars_):
    filtered = (element for element in vars_ if element)
    return chain.from_iterable(filtered)


num = ((1, 2), None, [1, (2, 3)], None)
let = (('a', 'b'), ['a', ('b', 'c')], None, None)
vars_all = (let, num)

for i, vars_ in enumerate(zip(*vars_all), start=1):
    print(f"Case {i}:", join(vars_))

It returns

Case 1: <itertools.chain object at 0x00000221C73F3940>
Case 2: <itertools.chain object at 0x00000221C73F3940>
Case 3: <itertools.chain object at 0x00000221C73F3940>
Case 4: <itertools.chain object at 0x00000221C73F3940>

So not what you really asked for. However, the chain object behaves like a normal list in most circumstances. It supports the Sequence interface (but not the MutableSequence interface):

from collections.abc import MutableSequence, Sequence
from itertools import chain


def join(vars_):
    filtered = (element for element in vars_ if element)
    return chain.from_iterable(filtered)


num = ((1, 2), None, [1, (2, 3)], None)
let = (("a", "b"), ["a", ("b", "c")], None, None)
vars_all = (let, num)

for i, vars_ in enumerate(zip(*vars_all), start=1):
    print(f"Case {i}:", list(join(vars_)))
    print("\tIs Sequence:", isinstance(vars_, Sequence))
    print("\tIs MutableSequence:", isinstance(vars_, MutableSequence))
    print(
        "\tIs MutableSequence (cast to list):", isinstance(list(vars_), MutableSequence)
    )

will print

Case 1: ['a', 'b', 1, 2]
        Is Sequence: True
        Is MutableSequence: False
        Is MutableSequence (cast to list): True
Case 2: ['a', ('b', 'c')]
        Is Sequence: True
        Is MutableSequence: False
        Is MutableSequence (cast to list): True
Case 3: [1, (2, 3)]
        Is Sequence: True
        Is MutableSequence: False
        Is MutableSequence (cast to list): True
Case 4: []
        Is Sequence: True
        Is MutableSequence: False
        Is MutableSequence (cast to list): True

So if a caller requires a list, they can cast to that type specifically. It is perhaps more pythonic to not do that in the function already (duck-typing).

Another point to note is that if both elements are None, an empty iterable is returned. This will still behave similar to None in boolean contexts. It allows for simpler code and maybe more predictable returns. See if that still fulfills your requirements. If not, this final suggestion will fulfill your tests as specified:

from itertools import chain


def join(vars_):
    if not any(vars_):
        return None
    filtered = (element for element in vars_ if element)
    return list(chain.from_iterable(filtered))


num = ((1, 2), None, [1, (2, 3)], None)
let = (("a", "b"), ["a", ("b", "c")], None, None)
vars_all = (let, num)

for i, vars_ in enumerate(zip(*vars_all), start=1):
    print(f"Case {i}:", join(vars_))

will return

Case 1: ['a', 'b', 1, 2]
Case 2: ['a', ('b', 'c')]
Case 3: [1, (2, 3)]
Case 4: None

for Python 3.8.1. I hope you agree that this is more readable, intuitive and also debuggable. The early return makes it easier to understand. The additional itertools import is very cheap.

Finally, a one-liner version of the above is

def join(vars_):
    return list(chain.from_iterable(elem for elem in vars_ if elem)) or None

Lastly, two notes:

  1. notice the start keyword for enumerate
  2. leading underscores denote (private) implementation details; use trailing underscores for variable names that otherwise shadow built-ins; better to avoid these all-together, though
\$\endgroup\$
4
  • 1
    \$\begingroup\$ I'll look at your solution more closely a bit later; it looks like you can meet the one-line requirement for the last solution via list(chain.from_iterable(elem for elem in vars_ if elem)) or None. Currently my impression is, it's about which Python the user is more comfortable with - a library or a nested comprehension (I for one never used itertools.chain). As for the requirement, if two lines offer far superior readability (and spare an import), I'll take it - but the idea is reusability without making it into a function, which treads on "don't repeat yourself" more the longer it is \$\endgroup\$ – OverLordGoldDragon Apr 28 '20 at 14:58
  • \$\begingroup\$ For your last point, I argue that even a one-liner is, if repeated at all, not following DRY. In that case, I would even suggest a lambda if you really want to avoid def. But a truly named function will do wonders for code readability and maintainability. Also, I think the "beauty" about chain is its name... it is a very simple operation, where chain says everything the user needs to know. itertools is part of the standard library, so in my view can be considered part of the core language ("batteries included"). \$\endgroup\$ – Alex Povel Apr 28 '20 at 15:44
  • \$\begingroup\$ 1. not any(x not in (None, [], {}) for x in arr) is an expression that can be repeated, but we don't consider it 'violating' a coding practice since it's sufficiently short - the idea's the same here, to repeat without defining a function. 2. The problem with chain is the need to dig up and study its docs if one is unfamiliar with it (as I was), and it adds an import; the core language is much preferred to this end. -- Nonetheless, your approach is a viable alternative to mine - just include the one-liner version somewhere, and I'll accept it. \$\endgroup\$ – OverLordGoldDragon May 1 '20 at 15:50
  • \$\begingroup\$ The answer is now edited accordingly. \$\endgroup\$ – Alex Povel May 1 '20 at 16:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.