For starters, here is a version that uses branches and loops so that the expected behavior is clear:
from collections import Iterable def flatten_gen(nested, isatom=lambda x: not isinstance(x, Iterable)): for item in nested: if isatom(item): yield item else: yield from flatten_gen(item)
Example:
lst = [1, [2, 3], 4, [[6, 7]]] print(list(flatten_gen(lst))) # -> [1, 2, 3, 4, 6, 7]
where isatom() predicate defines what is an atom (non-breaking object) from the point of view of this method. The implication is that "not atoms" can be passed into a loop to get the objects of which they are composed.
The algorithm is straightforward: going through the elements of the transferred list, we give the atoms as they are, and for composite objects, we call the generator recursively.
Since strings work with loops by default, if you want to treat them as atoms in your case, pass isatom explicitly:
isatom = lambda x: isinstance(x, str) or not instance(x, Iterable)
If only integers are considered atoms, then isatom = lambda x: isinstance(x, int) .
Recursive version without explicit branching, cycles, using iterable unpacking syntax :
def flatten(nested): try: first, *rest = nested except TypeError: # not an iterable return [nested] except ValueError: # empty return [] return flatten(first) + flatten(rest)
The code cuts off the first item from the specified nested list and connects the flat list obtained from it using the recursive call flatten(first) with the flat list received by calling the rest of the input flatten(rest) list.
If it is not possible to cut off the first element due to the fact that the input is empty or the scalar is given (not an enumerated value — a number for example), then an empty list is returned or this scalar is inside the list, respectively.
If the input contains a nested list with numbers, then at each level the task is reduced until the function is called with the simplest parameters: a number or an empty list, which leads to the return of specific values ​​and collecting the final result when lifting through the stack of calls.
This implementation does not work with strings without adaptation, since 'a' == 'a'[0] (a string of one character is equal to its first character) - this leads to infinite recursion in this case.
The code is not optimal in performance (for simplicity), but there are no if / else branches and loops.
Here is a similar, but more efficient, version that uses the generator:
def flatten_gen(nested): try: it = iter(nested) except TypeError: # not an iterable yield nested return try: first = next(it) except StopIteration: # empty return yield from flatten_gen(first) yield from flatten_gen(it)
The generator tries to get its iterator using iter(nested) and if a scalar is entered (for example, an integer), then TypeError thrown TypeError and the generator returns this scalar ( yield ). next(it) returns the first item from the iterator if it is not empty. Then scalars are recursively generated for the first element and the remaining elements in the it iterator.
If you can use loops from the library code, like a sum(map(flatten, lst, [])) solution from the @borisrozumnuk answer , you can improve the flatten_gen() generator using itertools.chain() :
from itertools import chain def flatten_gen(nested): try: it = iter(nested) except TypeError: # not an iterable yield nested else: yield from chain.from_iterable(map(flatten_gen, it))
Since recursion is not used in this case to implement the cycle, the number of calls is half as much.
If you want, you can write in the form of a single expression:
def flatten(nested): return (isinstance(nested, int) and [nested] # an int or nested # empty and flatten(nested[0]) + flatten(nested[1:])) # non-empty list
What works, thanks to the short-circuit behavior and / or logical operators. It is not clear whether this should be considered as branching.
If nested is an integer, then the code returns this number wrapped in a list ( [nested] ). If nested not an integer (but a list), then it is immediately returned if it is empty ( len(nested) == 0 means that bool(nested) is False ). If nested not an empty list, then the flat list union returned by the recursive calls for the first item and the remainder of the list — as well as in the solutions above in this answer.
If we move further towards the unreadability of the code, then the branch can be replaced by indexing lists and lambda to delay execution:
def flatten(nested): return [ lambda: [ # list (not an int) lambda: flatten(nested[0]) + flatten(nested[1:]), # non-empty list lambda: [] # empty ][not nested](), lambda: [nested] # an int ][isinstance(nested, int)]()
The general idea: True == 1 and False == 0 in Python, so the expression [on_false, on_true][condition]() calls the on_false function if condition false and on_true true.