14
\$\begingroup\$

Below are two solutions to the FizzBuzz problem in Python. Which one of these is more "Pythonic" and why is it more "Pythonic" than the other?

Solution One:

fizzbuzz = ''

start = int(input("Start Value:"))
end = int(input("End Value:"))

for i in range(start,end+1):
    if i%3 == 0:
        fizzbuzz += "fizz"
    if i%5 == 0:
        fizzbuzz += "buzz"
    if i%3 != 0 and i%5 != 0:
        fizzbuzz += str(i)

    fizzbuzz += ' '

print(fizzbuzz)

Solution Two:

fizzbuzz = []

start = int(input("Start Value:"))
end = int(input("End Value:"))

for i in range(start,end+1):
    entry = ''
    if i%3 == 0:
        entry += "fizz"
    if i%5 == 0:
        entry += "buzz"
    if i%3 != 0 and i%5 != 0:
        entry = i

    fizzbuzz.append(entry)

for i in fizzbuzz:
    print(i)
\$\endgroup\$

8 Answers 8

20
\$\begingroup\$

As has already been pointed out, creating a list is preferable as it avoids the concatenation of large strings. However neither of your solutions is the most pythonic solution possible:

Whenever you find yourself appending to a list inside a for-loop, it's a good idea to consider whether you could use a list comprehension instead. List comprehensions aren't only more pythonic, they're also usually faster.

In this case the body of the loop is a bit big to fit into a list comprehension, but that's easily fixed by refactoring it into its own function, which is almost always a good idea software design-wise. So your code becomes:

def int_to_fizzbuzz(i):
    entry = ''
    if i%3 == 0:
        entry += "fizz"
    if i%5 == 0:
        entry += "buzz"
    if i%3 != 0 and i%5 != 0:
        entry = i
    return entry

fizzbuzz = [int_to_fizzbuzz(i) for i in range(start, end+1)]

However, while we're at it we could just put the whole fizzbuzz logic into a function as well. The function can take start and end as its argument and return the list. This way the IO-logic, living outside the function, is completely separated from the fizzbuzz logic - also almost always a good idea design-wise.

And once we did that, we can put the IO code into a if __name__ == "__main__": block. This way your code can be run either as a script on the command line, which will execute the IO code, or loaded as a library from another python file without executing the IO code. So if you should ever feel the need to write a GUI or web interface for fizzbuzz, you can just load your fizzbuzz function from the file without changing a thing. Reusability for the win!

def fizzbuzz(start, end):
    def int_to_fizzbuzz(i):
        entry = ''
        if i%3 == 0:
            entry += "fizz"
        if i%5 == 0:
            entry += "buzz"
        if i%3 != 0 and i%5 != 0:
            entry = i
        return entry

    return [int_to_fizzbuzz(i) for i in range(start, end+1)]

if __name__ == "__main__":
    start = int(input("Start Value:"))
    end = int(input("End Value:"))
    for i in fizzbuzz(start, end):
        print(i)

(Note that I've made int_to_fizzbuzz an inner function here, as there's no reason you'd want to call it outside of the fizzbuzz function.)

\$\endgroup\$
3
  • \$\begingroup\$ You could also take start end as command line arguments (as opposed to user input) so any future GUI can call the command line version in its present state without having to refactor any code. You'll probably need to add a --help argument so users have a way to look up what arguments are available. \$\endgroup\$ Feb 16, 2011 at 0:10
  • \$\begingroup\$ I think those last two lines could better be print '\n'.join(fizzbuzz(start, end)). Also, refactored like this, the third if can be written if not entry:. Also, writing a bunch of Java may have made me hypersensitive, but entry = str(i) would make the list be of consistent type. \$\endgroup\$
    – pjz
    Apr 28, 2012 at 2:22
  • \$\begingroup\$ The list comprehension could be replaced with a generator. \$\endgroup\$
    – wei2912
    Dec 4, 2014 at 9:05
4
\$\begingroup\$

I have read a good solution with decorator, and I think it is a pythonic way to achieve a FizzBuzz solution:

@fizzbuzzness( (3, "fizz"), (5, "buzz") )
def f(n): return n

Generators are also a good pythonic way to get a list of numbers.

\$\endgroup\$
1
  • \$\begingroup\$ If you read python guide for beginners, you probably read about pep20. it says that "Explicit is better than implicit." also YAGNI principle defines not to do anything if you need it only once. so there is no point to write decorator. \$\endgroup\$ Sep 7, 2017 at 14:09
3
\$\begingroup\$

I am not sure either solution has any 'Pythonic' elements. What I mean is, you have not used any features that are characteristic of Python. I think that for a beginner litmus test, you should demonstrate your ability to create functions and make some use of lambda, map, reduce, filter, or list comprehensions. Since output formatting is also an important fundamental skill, I would throw in some gratuitous use of it. Using comments before code blocks and using docstrings inside functions is always a good idea. Using the 'else' of iterators would also be more python-like, but this problem does not lend itself to such a solution.

I would leave out the type casting on the user input. If you are using Python 2.X then it is redundant to cast the value since the print statement evaluates the input. If you are using 3.X then the point of type casting would be to force the value from a char to an int. The problem is, if the input included alphabetic characters then Python would throw an error. Also, since you do no bounds checking, casting to an int would not protect you from a negative integer screwing up you range.

Here is what I would do in Python 2.x:

# gather start and end value from user
start = input("Start value: ")
end = input("End value: ")

def fizzbuzz(x):
    """ The FizzBuzz algorithm applied to any value x """
    if x % 3 == 0 and x % 5 == 0:
        return "FizzBuzz"
    elif x % 3 == 0:
        return "Fizz"
    elif x % 5 == 0:
        return "Buzz"
    else:
        return str(x)

# apply fizzbuzz function to all values in the range
for x in map(fizzbuzz, range(start,end+1)):
    print "{0:>8s}".format(x)
\$\endgroup\$
2
\$\begingroup\$

From my brief experience with Python, I would say the second is more Pythonic as it takes advantage of the Python lists and the first is just appending to strings which causes the output to be a wee bit ugly and clumped together. Although you could eliminate an entire extra iteration by appending to a temporary string within the first for loop, printing at the end of the loop and resetting the value so that the values don't need to be stored for more than one iteration.

\$\endgroup\$
1
\$\begingroup\$

Not a decorator nor a function it's a right solution. Do some benchmarks and you will see. Call a function in Python is quite expensive, so try to avoid them.

for n in xrange(1, 101):
    s = ""
    if n % 3 == 0:
        s += "Fizz"
    if n % 5 == 0:
        s += "Buzz"
    print s or n

OR

for n in xrange(1, 101):
    print("Fizz"*(n % 3 == 0) + "Buzz"*(n % 5 == 0) or n)
\$\endgroup\$
2
  • \$\begingroup\$ Call a function in Python is quite expensive, so try to avoid them. You do not pay money to call a function. You just wait a tiny bit of time. If a function is going to be called 100 or 10000 times, then you need not to worry. When writing in a high level language we should optimize for readability and maintainability, if you want rough speed use C, do not write highly obfuscated Python code. \$\endgroup\$
    – Caridorc
    Feb 28, 2015 at 16:52
  • \$\begingroup\$ I dont see any obfuscated code here, it's just a more pytonic way of writing. I am really worried that you don't understand what a function call is in Python. \$\endgroup\$ Mar 1, 2015 at 17:56
1
\$\begingroup\$

Answering my own question: "why noone uses yield?"

# the fizbuz logic, returns an iterator object that
# calculates one value at a time, not all ot them at once
def fiz(numbers):
    for i in numbers:
        if i % 15 == 0:
            yield 'fizbuz'
        elif i % 5 == 0:
            yield 'buz'
        elif i % 3 == 0:
            yield 'fiz'
        else:
            yield str(i)

# xrange evaluates lazily, good for big numbers
# matches well with the lazy-eval generator function
numbers = xrange(1,2**20)

# this gets one number, turns that one number into fuz, repeat
print ' '.join(fiz(numbers))

# returns: 1 2 fiz 4 buz fiz [...] fiz 1048573 1048574 fizbuz
  • clearly separates fizbuz logic from concatenation
  • is as plain and readeable as possible
  • generator iterator does not keep all the array in memory
  • so that you can do it on arbitrary numbers (see Euler problem #10)

What I do not like in this solution is the three ifs, whereas the problem can be solved with two.

Answer: because yield is efficient when you do not want to keep big arrays in memory just to iterate through them. But this question is not about big arrays.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ I was tempted to post a answer solely to explain that 3 and 5 are primes, and that you can check for 15 rather than 3 and 5 individually. \$\endgroup\$
    – BlackCap
    Jun 9, 2016 at 14:51
  • \$\begingroup\$ And why is yield a better solution? Please explain so that the OP can learn from your thought process, instead of just saying "Here's how I would do". \$\endgroup\$ Jun 25, 2016 at 10:01
  • \$\begingroup\$ Actually, you can do yeild only once, also you dont need to get remainder by 15. look def fizzbuzz(count, step): for num in range(1, count, step): output = "" if not num % 3: output += "fizz" if not num % 5: output += "buzz" yield (output if output else str(num)) print(', '.join(fizzbuzz(count=100, step=2))) \$\endgroup\$ Sep 7, 2017 at 14:13
0
\$\begingroup\$

There is no difference in how "Pythonic" those solutions are. Both are perfectly acceptable. If the fizzbuzz string gets very long, using a list is preferable, as you don't have to make a copy of the string in every iteration, but that's a very minor issue.

\$\endgroup\$
0
\$\begingroup\$

Most definitely solution two. However, you could further improve that solution by dumping the string concatenation altogether. Remember, string concatenation is expensive. Because strings are immutable, every time you concatenate, a new string is created. While the garbage collector can pick up the trash later, you're still having to go through the expense of copying a string.

Instead, I'd recommend making a format string and then inserting the items in via string formatting, then appending them to your fizzbuzz list.

\$\endgroup\$
1
  • \$\begingroup\$ can you please write an example for the laymen? :) \$\endgroup\$ Jun 9, 2016 at 12:53

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