Pythonic style

Léo El Amri leo at superlel.me
Mon Sep 21 08:58:03 EDT 2020


On 21/09/2020 00:34, Stavros Macrakis wrote:
> I'm trying to improve my Python style.
> 
> Consider a simple function which returns the first element of an iterable
> if it has exactly one element, and throws an exception otherwise. It should
> work even if the iterable doesn't terminate. I've written this function in
> multiple ways, all of which feel a bit clumsy.
> 
> I'd be interested to hear thoughts on which of these solutions is most
> Pythonic in style. And of course if there is a more elegant way to solve
> this, I'm all ears! I'm probably missing something obvious!

Hello Stavros,

As there is no formal definition nor consensus on what is Pythonic and
what isn't, this reply will be very subjective.



My opinion on your code suggestions is the following:

> 1 def firstf(iterable):
> 2     n = -1
> 3     for n,i in enumerate(iterable):
> 4         if n>0:
> 5             raise ValueError("first1: arg not exactly 1 long")
> 6     if n==0:
> 7         return i
> 8     else:
> 9         raise ValueError("first1: arg not exactly 1 long")

firstf isn't Pythonic:

1. We are checking twice for the same thing at line 4 (n>0)
   and 6 (n==0).

2. We are using enumarate(), from which we ignore the second element
   it yields in its tuple

> 1 def firstd(iterable):
> 2     it = iter(iterable)
> 3     try:
> 4         val = next(it)
> 5     except StopIteration:
> 6         raise ValueError("first1: arg not exactly 1 long")
> 7     for i in it:
> 8         raise ValueError("first1: arg not exactly 1 long")
> 9     return val

firstd isn't Pythonic. While the usage of a for statement in place of a
try..except saves two lines, it is at the expense of showing a clear
intent: When I see a for statement, I expect a "complex" operation on
the iterable items (which we are ignoring here).

>  1 def firsta(iterable):
>  2     it = iter(iterable)
>  3     try:
>  4         val = next(it)
>  5     except StopIteration:
>  6         raise ValueError("first1: arg not exactly 1 long")
>  7     try:
>  8         next(it)
>  9     except StopIteration:
> 10         return val
> 11     raise ValueError("first1: arg not exactly 1 long")

>  1 def firstb(iterable):
>  2     it = iter(iterable)
>  3     try:
>  4         val = next(it)
>  5     except StopIteration:
>  6         raise ValueError("first1: arg not exactly 1 long")
>  7     try:
>  8         next(it)
>  9     except StopIteration:
> 10         return val
> 11     else:
> 12         raise ValueError("first1: arg not exactly 1 long")

>  1 def firstc(iterable):
>  2     it = iter(iterable)
>  3     try:
>  4         val = next(it)
>  5     except StopIteration:
>  6         raise ValueError("first1: arg not exactly 1 long")
>  7     try:
>  8         next(it)
>  9         raise ValueError("first1: arg not exactly 1 long")
> 10     except StopIteration:
> 11         return val

firsta, firstb and firstc are equally Pythonic. I have a preference for
firsta, which is more concise and have a better "reading flow".

>  1 def firste(iterable):
>  2     it = iter(iterable)
>  3     try:
>  4         good = False
>  5         val = next(it)
>  6         good = True
>  7         val = next(it)
>  8         good = False
>  9         raise StopIteration   # or raise ValueError
> 10     except StopIteration:
> 11         if good:
> 12             return val
> 13         else:
> 14             raise ValueError("first1: arg not exactly 1 long")

firste might be Pythonic although it's very "C-ish". I can grasp the
intent and there is no repetition. I wouldn't write the assignation at
line 7, though.



Mixing firsta and firste would make something more Pythonic:

def firstg(iterable):
    it = iter(iterable)
    try:
        val = next(it)
        try:
            next(it)
        except StopIteration:
            return val
    except StopIteration:
        pass
    raise ValueError("first: arg not exactly 1 long")

1. The code isn't repetitive (The "raise ValueError" is written
   only once)

2. The intent is a bit harder to grasp than for firsta or firste, but
   the code is shorter than firste

3. The try..catch nesting is considered a bad practice, but the code
   here is simple enough so it shouldn't trigger a strong aversion
   reading it



- Léo


More information about the Python-list mailing list