Mailing List Archive

Pythonic style
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!

Thanks,

-s

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

def firstb(iterable):
it = iter(iterable)
try:
val = next(it)
except StopIteration:
raise ValueError("first1: arg not exactly 1 long")
try:
next(it)
except StopIteration:
return val
else:
raise ValueError("first1: arg not exactly 1 long")

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

def firstd(iterable):
it = iter(iterable)
try:
val = next(it)
except StopIteration:
raise ValueError("first1: arg not exactly 1 long")
for i in it:
raise ValueError("first1: arg not exactly 1 long")
return val

def firste(iterable):
it = iter(iterable)
try:
good = False
val = next(it)
good = True
val = next(it)
good = False
raise StopIteration # or raise ValueError
except StopIteration:
if good:
return val
else:
raise ValueError("first1: arg not exactly 1 long")

def firstf(iterable):
n = -1
for n,i in enumerate(iterable):
if n>0:
raise ValueError("first1: arg not exactly 1 long")
if n==0:
return i
else:
raise ValueError("first1: arg not exactly 1 long")
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
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
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On 2020-09-20 18:34, Stavros Macrakis wrote:
> 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!

You can use tuple unpacking assignment and Python will take care of
the rest for you:

>>> x, = tuple() # no elements
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 1, got 0)
>>> x, = (1, ) # one element
>>> x, = itertools.repeat("hello") # 2 to infinite elements
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 1)

so you can do

def fn(iterable):
x, = iterable
return x

The trailing comma can be hard to spot, so I usually draw a little
extra attention to it with either

(x, ) = iterable

or

x, = iterable # unpack one value

I'm not sure it qualifies as Pythonic, but it uses Pythonic features
like tuple unpacking and the code is a lot more concise.

-tim





--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On 9/20/2020 6:34 PM, 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.

The 'obvious' thing to me was the double try-except StopIteration. It
is clear, and clarity is 'pythonic'.


--
Terry Jan Reedy

--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On Mon, Sep 21, 2020 at 11:37 PM Tim Chase
<python.list@tim.thechases.com> wrote:
>
> On 2020-09-20 18:34, Stavros Macrakis wrote:
> > 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!
>
> You can use tuple unpacking assignment and Python will take care of
> the rest for you:
>
> >>> x, = tuple() # no elements
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> ValueError: not enough values to unpack (expected 1, got 0)
> >>> x, = (1, ) # one element
> >>> x, = itertools.repeat("hello") # 2 to infinite elements
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> ValueError: too many values to unpack (expected 1)
>
> so you can do
>
> def fn(iterable):
> x, = iterable
> return x
>
> The trailing comma can be hard to spot, so I usually draw a little
> extra attention to it with either
>
> (x, ) = iterable
>
> or
>
> x, = iterable # unpack one value
>
> I'm not sure it qualifies as Pythonic, but it uses Pythonic features
> like tuple unpacking and the code is a lot more concise.

Or:

[x] = iterable

I'd definitely recommend using unpacking as the most obvious way to do
this. Among other advantages, it gives different messages for the "too
many" and "too few" cases.

ChrisA
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On 21/09/2020 15:15, Tim Chase wrote:
> You can use tuple unpacking assignment and Python will take care of
> the rest for you:
>
> so you can do
>
> def fn(iterable):
> x, = iterable
> return x
>
> I'm not sure it qualifies as Pythonic, but it uses Pythonic features
> like tuple unpacking and the code is a lot more concise.

I guess you just beat the topic. I think it is Pythonic and I'd be
surprised if someone came with something more Pythonic.

FI: The behavior of this assignation is detailed here:
https://docs.python.org/3/reference/simple_stmts.html#assignment-statements
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
Thanks, Tim! I didn't realize that you could write (x,) on the LHS!
Very nice, very Pythonic!

-s

On Mon, Sep 21, 2020 at 9:15 AM Tim Chase <python.list@tim.thechases.com>
wrote:

> On 2020-09-20 18:34, Stavros Macrakis wrote:
> > 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!
>
> You can use tuple unpacking assignment and Python will take care of
> the rest for you:
>
> >>> x, = tuple() # no elements
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> ValueError: not enough values to unpack (expected 1, got 0)
> >>> x, = (1, ) # one element
> >>> x, = itertools.repeat("hello") # 2 to infinite elements
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> ValueError: too many values to unpack (expected 1)
>
> so you can do
>
> def fn(iterable):
> x, = iterable
> return x
>
> The trailing comma can be hard to spot, so I usually draw a little
> extra attention to it with either
>
> (x, ) = iterable
>
> or
>
> x, = iterable # unpack one value
>
> I'm not sure it qualifies as Pythonic, but it uses Pythonic features
> like tuple unpacking and the code is a lot more concise.
>
> -tim
>
>
>
>
>
>
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On 2020-09-21 09:48, Stavros Macrakis wrote:
>> def fn(iterable):
>> x, = iterable
>> return x
>
> Thanks, Tim! I didn't realize that you could write (x,) on the LHS!
> Very nice, very Pythonic!

It also expands nicely for other cases, so you want the 3-and-only-3
first values with errors for too many or too few?

x, y, z = iterable
x, y, z = (1, 2, 3)

The (x,) version is just the single case. And it's fast—a single
Python UNPACK_SEQUENCE opcode

>>> dis.dis(fn)
2 0 LOAD_FAST 0 (i)
2 UNPACK_SEQUENCE 1
4 STORE_FAST 1 (x)

3 6 LOAD_FAST 1 (x)
8 RETURN_VALUE

Though now I'm wondering if there's a way to skip the
STORE_FAST/LOAD_FAST instructions and create a function that
generates the opcode sequence

UNPACK_SEQUENCE 1
RETURN_VALUE

:-)

(totally tangential ramblings)

-tkc







--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On 2020-09-21 3:46 PM, Chris Angelico wrote:
> On Mon, Sep 21, 2020 at 11:37 PM Tim Chase
> <python.list@tim.thechases.com> wrote:
>>
>> On 2020-09-20 18:34, Stavros Macrakis wrote:
>>> 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!
>>
>> You can use tuple unpacking assignment and Python will take care of
>> the rest for you:
>>
>> >>> x, = tuple() # no elements
>> Traceback (most recent call last):
>> File "<stdin>", line 1, in <module>
>> ValueError: not enough values to unpack (expected 1, got 0)
>> >>> x, = (1, ) # one element
>> >>> x, = itertools.repeat("hello") # 2 to infinite elements
>> Traceback (most recent call last):
>> File "<stdin>", line 1, in <module>
>> ValueError: too many values to unpack (expected 1)
>>
>> so you can do
>>
>> def fn(iterable):
>> x, = iterable
>> return x
>>
>> The trailing comma can be hard to spot, so I usually draw a little
>> extra attention to it with either
>>
>> (x, ) = iterable
>>
>> or
>>
>> x, = iterable # unpack one value
>>
>> I'm not sure it qualifies as Pythonic, but it uses Pythonic features
>> like tuple unpacking and the code is a lot more concise.
>
> Or:
>
> [x] = iterable
>
> I'd definitely recommend using unpacking as the most obvious way to do
> this. Among other advantages, it gives different messages for the "too
> many" and "too few" cases.
>

I used something similar years ago, but I made the mistake of relying on
the error message in my logic, to distinguish between 'too few' and 'too
many'. Guess what happened - Python changed the wording of the messages,
and my logic failed.

After messing about with some alternatives, I ended up with the OP's
first option (with some added comments), and have stuck with it ever
since. It is not pretty, but it is readable and unambiguous.

Frank Millman
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On 2020-09-21 3:46 PM, Chris Angelico wrote:
> On Mon, Sep 21, 2020 at 11:37 PM Tim Chase
> <python.list@tim.thechases.com> wrote:
>>
>> On 2020-09-20 18:34, Stavros Macrakis wrote:
>>> 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!
>>
>> You can use tuple unpacking assignment and Python will take care of
>> the rest for you:
>>
>> >>> x, = tuple() # no elements
>> Traceback (most recent call last):
>> File "<stdin>", line 1, in <module>
>> ValueError: not enough values to unpack (expected 1, got 0)
>> >>> x, = (1, ) # one element
>> >>> x, = itertools.repeat("hello") # 2 to infinite elements
>> Traceback (most recent call last):
>> File "<stdin>", line 1, in <module>
>> ValueError: too many values to unpack (expected 1)
>>
>> so you can do
>>
>> def fn(iterable):
>> x, = iterable
>> return x
>>
>> The trailing comma can be hard to spot, so I usually draw a little
>> extra attention to it with either
>>
>> (x, ) = iterable
>>
>> or
>>
>> x, = iterable # unpack one value
>>
>> I'm not sure it qualifies as Pythonic, but it uses Pythonic features
>> like tuple unpacking and the code is a lot more concise.
>
> Or:
>
> [x] = iterable
>
> I'd definitely recommend using unpacking as the most obvious way to do
> this. Among other advantages, it gives different messages for the "too
> many" and "too few" cases.
>

I used something similar years ago, but I made the mistake of relying on
the error message in my logic, to distinguish between 'too few' and 'too
many'. Guess what happened - Python changed the wording of the messages,
and my logic failed.

After messing about with some alternatives, I ended up with the OP's
first option (with some added comments), and have stuck with it ever
since. It is not pretty, but it is readable and unambiguous.

Frank Millman

--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
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.

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.

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.

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")

# returns (value, validp)

# validp is False if no more values

def next2(iterable):
try:
val = next(iterable)
except StopIteration:
return (None, False)
return (val, True)


(To be clear, I'm *not *suggesting that *next2* replace *next*!)

Thoughts?

-s
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
On Wed, Sep 23, 2020 at 3:52 AM Stavros Macrakis <macrakis@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
--
https://mail.python.org/mailman/listinfo/python-list
Re: Pythonic style [ In reply to ]
Thanks, Chris, for the useful comments. Comments in line (with snipping of
unnecessary content):


> > 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.
>

No specific requirement. These are *finger exercises* intended to isolate
one issue for discussion rather than be useful in themselves.


> > ...but there are ways to avoid the verbose exception handling.
> In Python, exception handling IS the way to do these things.


Good to know. Since *try* statements are at least 4 lines long, I was
trying for something more concise. And maybe somewhere in the back of my
mind, I have the anti-Pythonic principle that exceptions should be reserved
for errors. I'll try to suppress that. I remember one (very old) paper
<http://web.cecs.pdx.edu/~black/publications/Black%20D.%20Phil%20Thesis.pdf>
arguing that exceptions were a bad idea in general... but I have
implemented and used exceptions in many languages, both before and after
that paper!


> > _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):
>

What do you mean by an argument?

Is it not considered Pythonic for constants to be calculated once at the
module or class level rather than be recalculated every time a function is
entered?

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.)
>

Yes, I hadn't thought of that. I was trying to avoid repeating the *raise*
statement.

Thanks again for the helpful comments. They give me a better idea of good
Python style.

-s
--
https://mail.python.org/mailman/listinfo/python-list