Pythonic style

Chris Angelico rosuav at gmail.com
Tue Sep 22 14:15:19 EDT 2020


On Wed, Sep 23, 2020 at 3:52 AM Stavros Macrakis <macrakis at alum.mit.edu> wrote:
>
> Thanks to everyone for the comments, especially Tim Chase for the simple
> and elegant tuple unpacking solution, and Léo El Amri for the detailed
> comments on the variants. Below are some more variants which *don't *use
> tuple unpacking, on the theory that the coding patterns may be useful in
> other cases where unpacking doesn't apply.

When doesn't it apply? Can you elaborate on this? It might be easier
to advise on Pythonic style when the specific requirements are known.

> For me, one of the interesting lessons from all these finger exercises is
> that *for* and unpacking hide a lot of messiness, both the initial *iter* call
> and the exception handling. I don't see any way to eliminate the *iter*,
> but there are ways to avoid the verbose exception handling.

In Python, exception handling IS the way to do these things. Having a
two-part return value rather than using an exception is an unusual
idiom in Python (although it's well known in other languages; I
believe JavaScript does iterators this way, for one).

> Using the second arg to *next*, we get what is arguably a more elegant
> solution:
>
>
> _uniq = []
> def firstg(iterable):
>     it = iter(iterable)
>     val0 = next(it,_uniq)
>     val1 = next(it,_uniq)
>     if val0 is not _uniq and val1 is _uniq:
>         return val0
>     else:
>         raise ValueError("first1: arg not exactly 1 long")
>
> But I don't know if the *_uniq* technique is considered Pythonic.

It is when it's needed, but a more common way to write this would be
to have the sentinel be local to the function (since it doesn't need
to be an argument):

def firstg_variant(iterable):
    it = iter(iterable)
    sentinel = object()
    first = next(it, sentinel)
    if first is sentinel:
        raise ValueError("empty iterable")
    second = next(it, sentinel)
    if second is not sentinel:
        raise ValueError("too many values")
    return first

But getting a return value and immediately checking it is far better
spelled "try/except" here. (Note, BTW, that I made a subtle change to
the logic here: this version doesn't call next() a second time if the
first one returned the sentinel. This avoids problems with broken
iterators that raise StopException and then keep going.)

> If *next* were instead defined to return a flag (rather than raising an
> exception), the code becomes cleaner and clearer, something like this:
>
>
> def firsth(iterable):
>   it = iter(iterable)
>   (val0, good0) = next2(it)
>   (val1, good1) = next2(it)  # val1 is dummy
>   if good0 and not good1:
>     return val0
>   else:
>     raise ValueError("first1: arg not exactly 1 long")
>

IMO this isn't any better than the previous one. You still need a
sentinel, but now you use True and False instead of a special object.
It isn't *terrible*, but it's no advantage either.

ChrisA


More information about the Python-list mailing list