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