Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Hold up, "idiomatic Python" and no generator comprehensions?!

    def fizzbuzz(i):
      outputs = [(3, 'Fizz'), (5, 'Buzz'), (7, 'Bar')]
      return ''.join(word for x, word in outputs if i % x == 0) or str(i)


It was over 5 years ago and these are more accepted now.


Sorry it wasn't meant very seriously, just me expressing a caricature of idiomatic Python :)

So disclaimer: good old `for` loops are often a better choice unless you're doing a simple modification or filtering of another sequence.


Really? I actually liked that solution, I think it's both readable and clever. The rule priority is defined by the order of your list of tuples ('fizzbuzz' vs. 'buzzfizz'), so easy to change. That's superior to the solution above with sorted(outputs.keys()).


I would say my solution is borderline. It requires maybe one or two extra reads to grok in its entirety due to multiple conditionals, one of which is in a loop and the other outside. Here's a more readable version for someone looking at it the first time, or who is not too used to Python:

    OUTPUTS = [(3, 'Fizz'), (5, 'Buzz'), (7, 'Bar')]
    def fizzbuzz(i):
      result = ''
      for x, word in OUTPUTS:
        if i % x == 0:
          result += word
      return result or str(i)
Even the final `or` could be broken out to an `if not`, but in this case it's so simple I prefer putting it together, much like a coalescing operator.

And then for shits and giggles we can say screw it and put it all in one line:

    print '\n'.join(''.join(word for x, word in [(3, 'Fizz'), (5, 'Buzz'), (7, 'Bar')] if i % x == 0) or str(i) for i in xrange(1, 101))


Heh, I actually like blixt's solution better than mine too. :-)


Here's mine. Almost exactly the same as yours but written a tiny bit different. Also I wrote the complete program.

    #!/usr/bin/env python3
    
    # Word of factor
    wof = { 3: 'Fizz', 5: 'Buzz', 7: 'Bar' }
    
    def fizz (i):
    
        out = ''.join([wof[k] for k in sorted(wof.keys()) if not(i % k)])
    
        return out or str(i)
    
    for i in range(1, 101):
    
        print(fizz(i))
Some notes on my version:

1. I used dicts like the parent to yours. Tuples are almost just as fine and you do get a little bit nicer code in the part that you say

    word for x, word in outputs
where our say

    wof[k] for k in sorted(wof.keys())
However, when I wrote mine I imagined myself sitting in front of an interactive interpreter session.

Now let's say that we've each typed in the code we have here and then the interviewer says "now make it say Bazinga when the number is divisible by 19, and keep the order like with FizzBuzzBar".

Since my wof is global, I can add to it directly

    wof[19] = 'Bazinga'
But let's pretend that yours was global as well

    outputs.append((19, 'Bazinga'))
So aside from the global / not global we are on equal ground thus far.

But then the interviewer says "now make it say Fnord when the number is divisible by 13, and keep the order still".

Then once again I can simply

    wof[13] = 'Fnord'
Whereas if you did

    outputs.append((13, 'Fnord'))
then now you are out of order.

So you have to do some extra steps to ensure it's in order. Either you have to redeclare the whole thing (imagine there were one million pairs of numbers and strings!), or you have to sort it, or you have to find where you are going to insert and split it in two and join it around.

Not saying that mine is in any way better, just felt like sharing how I thought about this :)

2. (Wow note #1 was looong, sorry 'bout that.) I prefer splitting up my code like

    out = ''.join([wof[k] for k in sorted(wof.keys()) if not(i % k)])

    return out or str(i)
as opposed to your all-at-once

    return ''.join(word for x, word in outputs if i % x == 0) or str(i)
In my opinion splitting it exactly the way I did is more readable. Again, not a critique against what you wrote. Esp. since you said it yourself that your code was a bit tongue-in-cheek.

3. Even when split like I did there, I actually don't think my

    out = ''.join([wof[k] for k in sorted(wof.keys()) if not(i % k)])
is all that nice either.

One thing, which others have pointed out about this often is that too much is happening in one line but also there is another issue.

We all make typos or logic errors every now and then. In my exprience it's much more difficult to debug list comprehensions compared to... well whatever we call that which is not list comprehensions. Both with regards to error messages and with regards to following what's going on. I maintain that this is true even if you have a habit of using a lot of list comprehensions.


Cool, this is another valid way of doing it and I suppose it comes down to preference.

A few notes I'd like to mention though, that are not relevant to our toy examples, but may have implications in a production environment:

1. Unless you intend to store the resulting list from a list comprehension, it's good practice to prefer generator comprehensions because they don't grow in memory as the number of iterations increases. Obviously in our examples the sample size is so small that it doesn't matter, but the difference between ''.join(x for x in y) and ''.join([x for x in y]) can grow very large if y consists of many thousands of items.

Here's an example with just integers which requires over a million items before it starts becoming a concern – ultimately it depends on the memory footprint of each item:

    In [2]: %memit sum(x for x in xrange(1234567))
    peak memory: 46.87 MiB, increment: 0.00 MiB
    
    In [3]: %memit sum([x for x in xrange(1234567)])
    peak memory: 68.52 MiB, increment: 11.78 MiB
2. I still prefer the list of tuples in this situation as you have control of word order (you may not want to go through the factors in ascending order). There's [x, z].insert(1, y) which would change the list to [x, y, z] to avoid having to add to the end. Finally, because it's a list you can very easily do a one-time cost in-place sort:

    [(5, 'Buzz'), (3, 'Fizz')].sort(key=lambda t: t[0])
Again, it's silly to talk about performance in our toy examples, but small details like these can actually have memory and execution time implications if you don't consider the differences.

Ultimately, the best advice is to always write the code as readable and maintainable as possible and then optimize. Especially when dealing with a language like Python which was built for expressiveness, not leading performance.


By the way, speaking of sum, I find it a bit strange that Python allows

    'hello' + 'world'
but not

    sum(['hello', 'world'])
Intuitively I would have expected the latter to be possible given the former but I guess it comes down to how the + operator and the sum function are implemented in Python, such that counter to my expectation sum is not a function that "applies the + operator" to it's arguments. The notion that this is how it should work stems from my impression that "sum" belongs to the same family as do "map", "reduce" and "apply" -- that these are somehow "functional" in nature in the sense that is observed in the Lisp family of languages.


I guess sum was only implemented to support numeric values. However you can easily roll your own:

    >>> def add(it):
    ...   return reduce(lambda x, y: x + y, it)
    ...
    >>> add(['hello ', 'world'])
    'hello world'
    >>> add(x for x in xrange(10))
    45
Edit: almost forgot a possibly even more Pythonic way:

    >>> import operator
    >>> def add(it):
    ...   return reduce(operator.add, it)


    >>> import operator
    >>> def add(it):
    ...   return reduce(operator.add, it)
Ooh, I like this one. Thanks!


Looping back to why sum doesn't work with strings, it looks like they implemented it like this:

    >>> def sum(it):
    ...   return reduce(operator.add, it, 0)
Basically the first value is always added to 0. This has one important difference which can be a valid compromise if you expect to almost always sum numbers. If you try to call add([]) without the initial 0, you'll get an error because there's no way to know what the zero value is.

In a typed language you could use type inference and use the inferred type's zero value (if the language has such a concept for all types like for example Go does). In Python I guess you could fall back to None, but then you'd have code that doesn't behave consistently for all inputs.


The notion that that is how it work probably stems from you not having flunked computer science.


Thanks for the response, I appreciated it :)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: