Why is it useful?
Save space; save trees. Well, not physical trees (unless for some reason you want to print your code [but not print() your code]), but memory space. The alternative is writing several functions with if statements that call each other when needed. A clear problem appears as you realize that you would have to write a lot of code if many steps are needed!
Tracing Back
Trace back is a good exercise when wanting to understand what a piece of recursive code does. Let's try tracing back the following piece of code:
def looper(L): ''' (list) -> int Awful docstring that explains nothing. ''' return sum([looper(x) for x in L]) if isinstance(L, list) else 5
(Oh! BTW, that is a new trick I learned from the labs. You can make a return statement while implementing an if statement)
These are the examples that we will trace back:
looper([]) --> 0 #since L has no elements, recursion does not take place
looper([17]) --> sum([looper(x) for x in [17])
-- > sum([5])
--> 5
looper([1, 2, 3, 4, [5, 6]]) --> sum([looper(x) for x in [1, 2, 3, 4, [5, 6]])
--> sum([5, 5, 5, 5, 10]) #we do not solve when the function is executed inside
--> 30
looper(['some', 'here', 5, ['hello', 'there']]) --> sum([looper(x) for x in ['some', 'here', 5, ['hello', 'there']]])
--> sum([5, 5, 5, 10])
--> 25