3
\$\begingroup\$

I'm running through the popular python practice exercises located at github: https://github.com/zhiwehu/Python-programming-exercises/blob/master/100%2B%20Python%20challenging%20programming%20exercises.txt

I recently discovered recursive functions. So when asked the following in the above link:

Question 9
Level 2

Question£º
Write a program that accepts sequence of lines as input and prints the lines after making all characters in the sentence capitalized.
Suppose the following input is supplied to the program:
Hello world
Practice makes perfect
Then, the output should be:
HELLO WORLD
PRACTICE MAKES PERFECT

My answer is:

def capitalize_line(string=""):
    """Capitalize and add a newline to the input str(if present)"""
    inp = input("enter: ").upper()
    string = "\n".join([string, inp]) if string and inp else "".join([string, inp])
    if not inp:
        print(string)
        return string
    return capitalize_line(string=string)

E.g. Two inputs of 'hello' and 'world' would return 'HELLO\nWORLD'.

Is this a suitable solution? Pythonic?

The link answer was:

Solution:
lines = []
while True:
    s = raw_input()
    if s:
        lines.append(s.upper())
    else:
        break;

for sentence in lines:
    print sentence
\$\endgroup\$
3
\$\begingroup\$

Is this pythonic ?

The answer to this question is often found by doing import this:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!


Why is this line not pythonic ? Because it is not readable enough.

Readability counts.

string = "\n".join([string, inp]) if string and inp else "".join([string, inp])

Why is this line not pythonic ? Because it contains more than one instruction by line

Sparse is better than dense.

string = ('\n' if string and inp else '').join([string, inp])

Why is this line not pythonic ? Still not the prettiest way of writing that.

Beautiful is better than ugly.

delimiter = '\n' if string and inp else ''
string = delimiter.join([string, inp])

I will say later in this post why this is still ugly and how to improve this code.


Is a recursive function pythonic ?

No, yes, it's complicated. First, the 🔥call stack hell🔥. Every time you call a function, python has to "store" the addresses of the caller into a call stack, the depth of the call stack is 1000, which is not much. So...

  1. while and for are often better than recursive in terms of readability (not always true)
  2. while and for are often better than recursive in terms of performance (not always true)
  3. Everything that can be done in a loop can be done recursively (with python not always true)
  4. Everything that can be done recursively can be done with a while or for loop. (always true)
  5. The stack is the limit. 1000 calls comes quick, for example there are more than 1000 words in this post, it wouldn't be a good idea to use recursion to parse it, it would raise a RecursionError: maximum recursion depth exceeded.

Is my implementation of a recursive function good ?

No, because you pass the accumulator as an argument (classic beginner mistake). You think in terms of loops, even when using recursion. Let's take a look at a simple function.

def sum(l):                  #  No hidden mystery accumulator
    if len(l)==1:            #  The first line is usually the stop condition
        return l[0]          #  Return the last element
    return l[0] + sum(l[1:]) #  Return the first + the sum of the rest

sum(range(42))  # 861

Let's take a closer look at how it calculates this:

def sum(l):
    if len(l)==1:
        return l[0]
    return f"({l[0]}+{sum(l[1:])})"

sum(range(10))  # '(0+(1+(2+(3+(4+(5+(6+(7+(8+9)))))))))'

Impressive !


Now look at your function:

def capitalize_line(string=""):     # Hidden mystery accumulator
    inp = input("enter: ").upper()  # Move this outside of your function
    # This line is already debunked
    string = "\n".join([string, inp]) if string and inp else "".join([string, inp])
    if not inp:
        print(string)    # Move this outside of your function
        return string    # You don't use this but you should
    return capitalize_line(string=string) # not using return as intended 
                         # (carrying processed data down the call stack)

Explicit is better than implicit.

Using stdin and stdout inside a function whose purpose is to capitalize letters is not a good thing. It makes the code impossible to divide because everything is linked. The capitalize function should capitalize and return a string and nothing else.

How should I write it ?

You think I will give you the solution like that... I'm sorry Dave, I'm afraid I can't do that !

Just kidding, but promise me you will try before looking at it:

def capitalize(l):
    if not l:
        return ''
    return l[0].upper() + '\n' + capitalize(l[1:])
def capitalize(l): # alternative way
    if len(l)==1:
        return l[0]
    return l[0].upper() + '\n' + capitalize(l[1:])
def main(): # Python 3.8 only
    inputs = []
    while s:=input('enter: '): inputs.append(s)
    print(capitalize(inputs))
def main():
    inputs = []
    while True:
        s = input('enter: ')
        if not s:
            break
        inputs.append(s)
    print(capitalize(inputs))
if __name__ == '__main__':
    main()   # In case someone imports your module to use your functions
But You want to do like the pros don't you ? Use generators
def capitalize(iterable):
    try:
        s = next(iterable)
    except StopIteration:
        return ''
    return s.upper() + '\n' + capitalize(iterable)
def inputs():
    while True:
        s = input('enter: ')
        if not s:
            break
        yield s
Even better (or worse), a recursive generator:
def inputs():
     s = input('enter: ')
     if not s: return
     yield s
     yield from inputs()
def main():
    print(capitalize(inputs()))

Wow, you're still there, it means you had the time to read my entire post, and since you've time on your hands, I recommend you Socratica, more specifically the video about recursion.

And remember, recursion isn't often the best solution.

\$\endgroup\$
2
\$\begingroup\$

While the answer by @BenoîtPilatte is fine as long as you want to keep this a recursive solution, here is a solution using only generators.

First, Python 2 is going to be no longer supported in less than a year. If you have not yet switched to Python 3, now is the time. As a small incentive I am going to assume Python 3 in the rest of this answer.

Now, let's start by building a generator that takes the user input until an empty line is entered:

def user_input():
    while True:
        s = input()
        if not s:
            return
        yield s

That was easy, and now we are already done, because we can just use str.upper directly, since the output of input is a string, no need for a recursive function to do it for you:

def solution():
    for line in user_input():
        print(line.upper())

Alternatively we can chain generators and use map:

def solution():
    for line in map(str.upper, user_input()):
        print(line)

As you can see this is vastly shorter and more readable, regardless of the stack size limit in Python.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.