6

I'm trying to flatten some mixed arrays in Python using LC. I'm having some trouble figuring out how to structure it.

Here's the array's i'm trying to flatten

arr_1 = [1, [2, 3], 4, 5]
arr_2 = [1,[2,3],[[4,5]]]

I tried this methods for arr_1 but get "TypeError: 'int' object is not iterable"

print([item if type(items) is list else items for items in arr_1 for item in items])

So I decided to break it into parts to see where it's failing by using this

def check(item):
return item;

print([check(item) if type(items) is list else check(items) for items in [1, [2, 3], 4, 5] for items in arr_2]) 

Through the debugger I found that it's failing at the 2d array in

for items in [1, [2, 3], 4, 5]

I don't need the LC to be in one line but I just wanted to know how to do it in a single nested LC if its even possible.

Toast
  • 83
  • 4
  • 2
    So, just to be sure: this is purely for academic purposes and not for real work, right? :D – user8408080 Apr 05 '22 at 20:55
  • 3
    There probably isn't a clean way to do this. – dumbPotato21 Apr 05 '22 at 20:57
  • This simply can't be done in nested list comprehensions without some recursion variant. That's clean. You may be able to do it in one list comprehension via bytecode manipulation, but that's about as unclean as it gets. Proof of the first statement: For any finite-depth nested list comprehension, it's possible to produce a more deeply nested sequence. – Mous Apr 05 '22 at 21:06
  • After thinking about this a little more and seeing that you want to unpack arbitrary levels of nested lists, I also come to the conclusion that this should not be possible with a LC – user8408080 Apr 05 '22 at 21:08
  • 2
    @Mous Did I disprove your statement? Or would you consider that "some recursion variant"? – Kelly Bundy Apr 05 '22 at 21:21
  • 1
    I think you did, yes. That's impressive. Please don't use this in production. – Mous Apr 05 '22 at 21:24
  • @Mous just curious, why not? – 0x263A May 12 '22 at 15:22
  • @0x263A because it's opaque to read, and that will burn time in future refactors. – Mous May 12 '22 at 21:04

1 Answers1

19

Using an internal stack and iter's second form to simulate a while loop:

def flatten(obj):
    return [x
            for stack in [[obj]]
            for x, in iter(lambda: stack and [stack.pop()], [])
            if isinstance(x, int)
            or stack.extend(reversed(x))]

print(flatten([1, [2, 3], 4, 5]))
print(flatten([1, [2, 3], [[4, 5]]]))
print(flatten([1, [2, [], 3], [[4, 5]]]))

Output (Try it online!):

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]

Slight variation, splitting the "long" line into two (Try it online!):

def flatten(obj):
    return [x
            for stack in [[obj]]
            for _ in iter(lambda: stack, [])
            for x in [stack.pop()]
            if isinstance(x, int)
            or stack.extend(reversed(x))]

To explain it a bit, here's roughly the same with ordinary code:

def flatten(obj):
    result = []
    stack = [obj]
    while stack:
        x = stack.pop()
        if isinstance(x, int):
            result.append(x)
        else:
            stack.extend(reversed(x))
    return result

If the order doesn't matter, we can use a queue instead (inspired by 0x263A's comment), although it's less memory-efficient (Try it online!):

def flatten(obj):
    return [x
            for queue in [[obj]]
            for x in queue
            if isinstance(x, int) or queue.extend(x)]

We can fix the order if instead of putting each list's contents at the end of the queue, we insert them right after the list (which is less time-efficient) in the "priority" queue (Try it online!):

def flatten(obj):
    return [x
            for pqueue in [[obj]]
            for i, x in enumerate(pqueue, 1)
            if isinstance(x, int) or pqueue.__setitem__(slice(i, i), x)]
Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65
  • I'm curious how this works, could you add an explanation? – Mous Apr 05 '22 at 21:25
  • @Mous Added an ordinary code version of it, does it suffice? – Kelly Bundy Apr 05 '22 at 21:31
  • Not going to post this as an answer since it wont always maintain the order of the original list, but if you don't care about order this will flatten any list of lists/ints. `def flatten(arr): return [num for lst in [[obj] if isinstance(obj, int) else [obj.extend(el) if isinstance(el, list) else el for el in obj] for obj in arr[::]] for num in lst if num != None]`. Similar approach to the above answer, but uses list objects in arr as queues. Still a fun exercise! – 0x263A May 12 '22 at 12:41
  • 1
    @0x263A Yeah, for example for input list `[[[1], 2]]`, it returns the wrong order `[2, 1]` and it has the *side effect* of *modifying the input* list to `[[[1], 2, 1]]`. That's not good. – Kelly Bundy May 12 '22 at 13:37
  • @0x263A But you inspired me to write [another queue solution](https://tio.run/##fc7BCgIhEAbg@z7FHBUkqC2IXmWYw9aOZISaq2BPbyZ4WJb6b8P/Mfz@He/OjmcfSplZg35OMbIV7vqQlwFqAscULGBuV492AV6JE4OpHVZOtAH5Wza1qowGsxi7xMneWGRVVZQr0Z/vONcxs8iShsGH6kQfiHsFeFAwkoKjghNJ@UcgNkO/FdJGlvIB) which ended up inspiring the second solution that I just added to the answer :-) – Kelly Bundy May 12 '22 at 13:52
  • @KellyBundy Very clever! Your use of `or` in these answers is a fantastic trick – 0x263A May 12 '22 at 14:00
  • Thanks, I haven't though of separating the list comprehension across multiple lines before – Tom Huntington Jan 27 '23 at 22:44
  • @TomHuntington Technically I can btw still call it a oneliner. It's multiple *physical* lines, but only one *logical* line. That's official Python [terminology](https://docs.python.org/3/reference/lexical_analysis.html#line-structure). – Kelly Bundy Jan 27 '23 at 22:59
  • I'd just call it a single expression – Tom Huntington Jan 27 '23 at 23:02