5

I noticed that something weird happens when I use a recursion inside a list comprehension. If the recursion goes too deep the interpreter seemingly goes idle (I waited for 5 minutes and nothing happened).

For the sake of this question assume that I want to flatten nested lists (I don't - but it's a short code sample that illustrates the problem I am experiencing):

def flatten(x):
    if isinstance(x, list):
        return [a for i in x for a in flatten(i)]
    else:
        return [x]

With a helper function to create a nested list:

def wrap_in_lists(value, depth):
    a = value
    for _ in range(depth):
        a = [a]
    return a

It works great when using:

>>> flatten(wrap_in_lists(1, 2**10))
[1]

But it completely stops when I use:

>>> flatten(wrap_in_lists(1, 2**11))
# Nothing happens, no exception, no result, no segfault, ...

My question is: What happens/happened here? Why is there no response at all?


What is odd is that a similar approach using a generator doesn't show this behavior:

def flatten(l):
    def inner(x):
        for item in x:
            if isinstance(item, list):
                yield from inner(item)
            else:
                yield item
    return list(inner(l))

>>> flatten(wrap_in_lists(1, 2**11))
[1]

>>> # although increasing the depth leads to an recursion error
>>> flatten(wrap_in_lists(1, 2**12))
RecursionError: maximum recursion depth exceeded

If it's important I'm using Python 64bit 3.6.6 on Windows in a jupyter lab.

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • try to assign the result: `a = flatten(wrap_in_lists(1, 2**11))`, the issue is probably when trying to print the result – Jean-François Fabre Jul 14 '18 at 09:37
  • @Jean-FrançoisFabre But why should there be an issue printing out `[1]`? – MSeifert Jul 14 '18 at 09:38
  • oh, WTF.???? you're right. – Jean-François Fabre Jul 14 '18 at 09:38
  • I just tested assigning the result. But that also shows the same behavior (by that I mean the lack of any visible behavior). – MSeifert Jul 14 '18 at 09:40
  • `flatten` is indeed called recursively, though. Maybe a bug because called inside list comprehension? – Jean-François Fabre Jul 14 '18 at 09:41
  • 2
    on python 3.4 windows I get `File "S:\module1.py", line 3, in flatten return [a for i in x for a in flatten(i)] RuntimeError: maximum recursion depth exceeded` even with 2**10. Seems expected. So I cannot reproduce your issue. Looks like a bug of your python version – Jean-François Fabre Jul 14 '18 at 09:43
  • Cannot repro in any of python 3.4.0, 3.6.4, or 3.7.0 (all 32 bit on Windows). I get a RecursionError as expected. – Aran-Fey Jul 14 '18 at 09:52
  • In 3.6 and Spyder, I get `RecursionError` so I'm not sure it's an iPython thing. It might be confined to Jupyter – roganjosh Jul 14 '18 at 09:52
  • I'm getting [RecursionError here](https://repl.it/repls/SpringgreenCuddlyDemos), as expected. Cannot reproduce. – Mulan Jul 14 '18 at 12:42
  • Related: If you're trying to flatten lists that are nested deeper than the recursion limit, try the technique used in [this answer](https://stackoverflow.com/a/48997476/633183) – Mulan Jul 14 '18 at 12:45
  • Thanks but I'm not actually trying to flatten lists as already stated in the question. – MSeifert Jul 14 '18 at 12:47

2 Answers2

5

It's a simple StackOverflow that happens before the recursion limit is reached.

In the second (generator) approach it hit the recursion limit with a depth of 2**12. That means 2**11 should've hit the recursion limit in the first approach. That's because list-comprehensions create an additional stack frame so it's twice the stack frames than the generator solution. The fact that it doesn't throw a RecursionError means that something "fatal" happened to the interpreter (or there is an infinite loop somewhere).

However it's not an infinite loop because if you inspect the jupyter lab responses (for example if you start it from the command line with jupyter lab) you'll notice that shortly after running the flatten(wrap_in_lists(1, 2**11)) line it will print a kernel <xyz> restarted. So it isn't correct that there is no response, the kernel just crashed and the [*] displayed in the jupyter lab cell in this case just means the calculation didn't finish (because of the crash).

That's one of the reasons why you be really really careful if you change Pythons recursion limit or use an interpreter that changed it for you.

MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 1
    So the take-home message is that Jupyter changes the recursion limit? On the surface it seems like such an arbitrary change, I wonder what they intended. – roganjosh Jul 14 '18 at 09:56
  • Yeah and probably that Jupyter (at least jupyter lab) doesn't indicate in the browser that the kernel crashed. If it did it would've been obvious. – MSeifert Jul 14 '18 at 09:58
  • 1
    I'm tempted to edit in the Jupyter tag to your question because I think this sidesteps some of the weirdness that even iPython can introduce (I can't reproduce), so it seems more localised. But I'll leave that decision to you. – roganjosh Jul 14 '18 at 10:03
  • Not sure if it's actually strictly related to Jupyter (except that it hides the crash and in my case changed the default recursion limit). The more I think about it it seems like a weird combination of defaults and environmental influences that lead to the crash. The available stack-size, the size of the individual Python stack frames (both probably OS and compiler dependent) and the configured recursion limit all contributed there. – MSeifert Jul 14 '18 at 10:14
  • Yeah, I think I agree with you on that. Maybe jupyter's silent change of the recursion limit uncovers a broader issue :) – roganjosh Jul 14 '18 at 10:23
  • 1
    After some investigation I found that it's not jupyter but [jedi](https://github.com/davidhalter/jedi) that changes the recursion limit: [sys.setrecursionlimit(3000)](https://github.com/davidhalter/jedi/blob/v0.12.1/jedi/api/__init__.py#L42). However jupyter startup imports that module so it seemed like jupyter did change the recursion limit. – MSeifert Jul 14 '18 at 10:37
-1

I'm not sure why the error is getting silenced in your case. I can't reproduce it here. I tried many values and it either gives error or works.

>>> sys.setrecursionlimit(3000)
>>> flatten(wrap_in_lists(1, 2**10)) #works
[1]
>>> flatten(wrap_in_lists(1, 2**11)) #fails with exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  ...
RecursionError: maximum recursion depth exceeded

I'm also using python3 in jupyter notebook.

nosklo
  • 217,122
  • 57
  • 293
  • 297
  • Seems like my jupyter lab has a different recursion limit, it says `3000` when I use `sys.getrecursionlimit`. Although I checked my configs and code and I don't change that anywhere, so it's probably the jupyter-lab default?! – MSeifert Jul 14 '18 at 09:35
  • @MSeifert I tried setting the recursion limit higher, by using `sys.setrecursionlimit(3000)`. I still can't reproduce the behavior you're seeing. I get either the result or the error. – nosklo Jul 14 '18 at 09:37
  • it also doesn't answer the question. OP is trying to print a flattened transformation – Jean-François Fabre Jul 14 '18 at 09:40
  • I edited the answer to remove the printing issue as it is not related – nosklo Jul 14 '18 at 09:44
  • 3
    so your answer is now: "I cannot reproduce your issue" / "your code works fine by me". – Jean-François Fabre Jul 14 '18 at 09:44