13

Very quick and easy homework question. I have it running ok but I think there's a better
way to do it. A more Pythonic way.
Here's my code to recursively decrement each element of a list by 1.

l = range(30)  
def recurseDecrMap(l, x = []):  
    if len(l) == 0:  
        return []  
    else:  
        x.append(l[0] -1)  
    recurseDecrMap(l[1:], x)  
    return x  

So thanks for any input. I'm trying to learn to do better recursion. Having trouble getting
the knack of it.

Paul Bellora
  • 54,340
  • 18
  • 130
  • 181
Loubot
  • 285
  • 4
  • 10
  • 13
    @Haidro Linked this, you should see why `x=[]` as a default argument is a bad idea: http://stackoverflow.com/questions/1132941/least-astonishment-in-python-the-mutable-default-argument – jamylak Apr 27 '13 at 23:34
  • 6
    The Pythonic way is to not use recursion. – asmeurer Apr 28 '13 at 01:30

8 Answers8

25

Probably less pythonic, but there:

def recurseDecrMap(l):
    return [l[0]-1] + recurseDecrMap(l[1:]) if l else []
Elazar
  • 20,415
  • 4
  • 46
  • 67
  • I still think the syntax of the python ternary statement is backwards (coming from a c background). It's worth knowing that your answer can equivalently be expressed as `return [l[0] - 1] + l and recursiveDecrMap(l[1:]) or []` – sapi Apr 28 '13 at 01:11
  • 1
    @sapi It's worth knowing that your suggestion can easily make you write *subtly incorrect* code. if the `and *` part happens to be false, and is different from the right-hand of the `or` part. – Elazar Apr 28 '13 at 01:15
  • @Elazar - sure, but short circuiting of operators is defined behaviour, which it's really hard to screw up – sapi Apr 28 '13 at 05:17
  • 1
    @sapi short-circuiting doesn't have anything to do with this. What Elazar meant is that `A and B or C` if **not** equivalent to `B if A else C`, since, in the case that `A` is true but the value of `B` is considered false, the first expression returns `C` while the second, correctly, returns `B`. – Bakuriu Apr 28 '13 at 13:36
  • @Bakuriu - of course, but that's just how short circuiting of operators works. There's nothing complicated going on here, and in the example given, they are equivalent. – sapi Apr 28 '13 at 22:51
  • @sapi It doesn't matter. This kind of usage is a bad idea; you can use it on your own peril, but please *don't* encourage other people to do the same, just because you think `A if E else B` is backwards. – Elazar Apr 29 '13 at 05:39
  • @Elazar - it's hardly a bad thing to teach people how boolean operators work in python (or most other languages); but feel free to use whatever you prefer in your own code. – sapi Apr 30 '13 at 02:41
14

You can use only one argument, in my opinion it is simpler:

def recurseDecrMap(l):  
    if not l:  
        return []  
    else:
        return [l[0]-1] + recurseDecrMap(l[1:])

But as @jamylak pointed out, the complexity of this algorithm is O(N^2), since l[1:] creates a new list with references to the rest of the items in the list.

If you need efficiency, I'd recommend you using list comprehensions (Haidro's answer), but I suppose it is not a priority if you want it only for learning purposes.

Community
  • 1
  • 1
A. Rodas
  • 20,171
  • 8
  • 62
  • 72
  • But also O(N^2), although definitely cleaner and will suffice in most situations – jamylak Apr 27 '13 at 23:35
  • 4
    I would suggest doing `if not l` instead of `if len(l) == 0` when checking for an empty list. – Volatility Apr 28 '13 at 00:09
  • @Volatility - why? (I used it in my own answer, but for no real reason) – Elazar Apr 28 '13 at 00:25
  • 4
    @Elazar [PEP8](http://www.python.org/dev/peps/pep-0008/#programming-recommendations) says to "use the fact that empty sequences are false" – Volatility Apr 28 '13 at 00:27
  • 1
    @AmineHajyoussef even so, many would say it is less pythonic than `not l`. And you couldn't argue that `not l` isn't readable... – Volatility Apr 28 '13 at 02:46
9

For what it's worth, this is a terrible way to learn about recursion, because you're using it to do something that is not inherently recursive. If your teacher really is asking you to write a program that decrements the elements of a list like [1, 2, 3, 4] recursively, then shame on him/her.

As Haidro noted, the most Pythonic way to solve this problem is to just iterate over the list using a list comprehension

[i - 1 for i in l]

As a loop, this is

def decr(l):
    a = []
    for i in l:
        a.append(i - 1)
    return a

Recursion is useful if you want to solve the same problem at arbitrary levels of depth. For example, say you had something like [1, [2, 3], [[4], 5]] and you wanted to decrement each number by 1, while maintaining the list structure. In that case, a recursive solution would use the iterative solution for the base case, and call itself for the recursive case.

def decr_recursive(l):
    a = []
    for i in l:
        if isinstance(i, list):
            a.append(decr_recursive(i))
        else:
            a.append(i - 1)
    return a

This can be easily modified if you want to support more than just lists or integers.

>>> decr([1, [2, 3], [[4], 5]])
[0, [1, 2], [[3], 4]]

This is the sort of problem that is very difficult to solve without using recursion, but is easy to solve with it. What you are asking is the sort of problem that is easy to solve without recursion (it's just a simple iteration over a list for God's sake), but somewhat difficult to solve with it.

Some reasons why you should avoid recursion in Python

  • It's harder to read. Compare [i - 1 for i in l], or even the explicit loop, to something like

    def decr(l):
        if not l:
            return []
        return [l[0] - 1] + decr(l[:1])
    
  • Calling a function in Python can be expensive. I get roughly the same timings as Ashwini Chaudhary on my computer. But [i - 1 for i in range(10**4)] takes 559 µs on my computer. That's three orders of magnitude faster than the fastest recursive method.

  • Recursive functions don't work past 1000 calls unless you set the recursion limit higher. You may have noticed the sys.setrecursionlimit(10**5) call in Ashwini Chaudhary's answer. This is necessary because without it, each call would lead to a RuntimeError: maximum recursion depth exceeded following a huge traceback. But even with this, a little bigger list would still lead to a recursion limit. And according to the documentation, there is a top limit for every OS, and setting it too high can lead to a crash.

  • Recursive functions are harder to debug. Not only do they pollute your stack traces with hundreds of calls from the same function, but they are conceptually harder to follow, because the same part of the same function is being used in different ways, depending on which level in the stack you are in. The natural human way of thinking is iterative. We do things one at a time. Our own brains' "stacks" are only a few levels deep, so we have a very hard time solving problems in a recursive way, like "let me start solving a problem, but before I finish, let me solve another problem, and then when I'm done, I'll finish the original problem. And in the smaller problem, I may do the same thing, so that I get several levels deep before I finish." That's why you walk into the kitchen to get a pen, then you see a candy bar and start eating it, and when you are done, you forget about the pen. You "recursed" a level, from the pen problem to the candy bar problem, and your mental stack got too deep (just two levels, but that's enough). If you had instead grabbed the candy bar, but before you opened it and started eating it, also found the pen (the best iterative analouge I could come up with), you would do both without forgetting either. The way you should solve problems in programs should be exactly the same way you solve them in your head, because that's the only way you will understand what your code is doing. Python is such a great language because its high level interface let's you do exactly that (at least more often than in lower-level languages). Use this fact!

asmeurer
  • 86,894
  • 26
  • 169
  • 240
7

Here's the worst way - using Fixed Point Combinator:

Y = lambda g: (lambda f: g(lambda arg: f(f)(arg))) (lambda f: g(lambda arg: f(f)(arg)))
recurseDecrMap = Y(lambda f: lambda l: [l[0]-1] + f(l[1:]) if l else [])
Elazar
  • 20,415
  • 4
  • 46
  • 67
2

Here's a simple pythonic way:

>>> mylist = range(30)
>>> def func(l):
...     return [i-1 for i in l]
>>> func(mylist)
[-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]

Explanation:

I used list comprehensions to create a new list of every element in mylist whose value is one less than what it was.

There's nothing wrong with your piece of code, except if you're going to use it more than once:

>>> recurseDecrMap(l)
[-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]

>>> recurseDecrMap(l)
[-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]

To avoid this, check out this answer.

Community
  • 1
  • 1
TerryA
  • 58,805
  • 11
  • 114
  • 143
2

Timing comparisons of various ways to do this:

In [1]: import sys

In [2]: sys.setrecursionlimit(10**5)

In [3]: from so import *

In [4]: %timeit recur(range(10**4)).show()
10 loops, best of 3: 18.2 ms per loop

In [5]: %timeit recurse1(range(10**4))
1 loops, best of 3: 559 ms per loop

In [6]: %timeit recurse2(range(10**4))
1 loops, best of 3: 1e+03 ms per loop

In [7]: %timeit recurse3(range(10**4))
1 loops, best of 3: 1.02 s per loop

In [8]: %timeit recurse4(range(10**4))
1 loops, best of 3: 596 ms per loop

Code:

class recur:
    # No extra memory is required in this method

    def __init__(self,lis):
        self.lis=lis
        self.len=len(self.lis)
        self.rec(0)

    def show(self):
        return self.lis

    def rec(self,n):
        if n!=self.len:
            self.lis[n]-=1
            self.rec(n+1)

def recurse1(l,lis=None):
    lis=lis if lis is not None else []
    if l:
        lis.append(l[0]-1)
        return recurse1(l[1:],lis)
    else:
        return lis

def recurse2(l):
    return [l[0]-1] + recurse2(l[1:]) if l else []

def recurse3(l):  
    if len(l) == 0:  
        return []  
    else:
        return [l[0] -1] + recurse3(l[1:])

def recurse4(l, x = []):  
    if len(l) == 0:  
        return []  
    else:  
        x.append(l[0] -1)  
    recurse4(l[1:], x)  
    return x  
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
1

Here's a recursive solution that can cope with large lists without hitting the recursion depth limit. By using divide and conquer the recursion depth is O(log(N)) at worst, compared to O(N) with naive recursion. Any sort of recursion is a poor choice of technique for this problem though, as it's trivially solved with a simple for-loop.

def dec_list(xs, a, b):
    if b == a + 1:
        xs[a] -= 1
    if a + 1 >= b: return
    mid = (a + b) // 2
    dec_list(xs, a, mid)
    dec_list(xs, mid, b)

def dec(xs):
    dec_list(xs, 0, len(xs))

xs = range(1001)
dec(xs)
print xs
Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
  • The recursion depth is log N, but the worst-case order of complexity is still O(N). – A. Rodas Apr 28 '13 at 09:54
  • 1
    @A.Rodas Yes, the run-time complexity is O(N). It's not possible to do better than that, since one needs to look at every element in the list. – Paul Hankin Apr 28 '13 at 12:41
0

Maybe something like:

def dec_by_one(alist, index):
    if index == len(alist):
        return alist
    alist[index] -= 1
    return dec_by_one(alist, index + 1)


print(dec_by_one([1, 3, 4], 0)) # [0, 2, 3]
funnydman
  • 9,083
  • 4
  • 40
  • 55