On Wed, Dec 11, 2019 at 05:20:13PM +1100, Chris Angelico wrote:
> On Wed, Dec 11, 2019 at 5:14 PM Steven D'Aprano <[email protected]> wrote:
> >
> > On Tue, Dec 10, 2019 at 07:21:13PM -0600, Tim Peters wrote:
> > > While the meaning of `first()` is clear for any iterable argument.
> >
> > Sorry Tim, I have to disagree. The meaning of `first` is:
> >
> > return the first element of a sequence or container (in standard
> > iteration order), OR the *next* element of an iterator
> >
> > and I don't think that this is even a little bit clear from the name.
> >
>
> You know that cliche about how today is the first day of the rest of
> your life?
There's a difference between the first day of *the rest* of your life
and the first day of your life.
There's a difference between the first item of *the rest* of the
iterator and the first item of the iterator.
Cliches and platitudes will only take you so far. What counts here is
the behaviour of the code. When you iterate over an iterable using a for
loop, you get the same sequence of items whether it is an iterator or
not. But that's not what happens if you call `first(iterable)` multiple
times. Calling it once is fine, but people will call it multiple times.
I am writing some code as we speak to process a bunch of lines of text
from an iterable. I'm expecting a mandatory header as the first line, an
optional line of dashes "-------", and then one or more lines that need
processing. If all I remembered is that `first` works on both iterators
and non-iterators, I might write something like this:
def process(lines):
# can't use next
header = first(lines, '')
line = first(lines, '')
if is_dashes(line):
line = first(lines, '')
while line:
do_stuff(line)
line = first(lines, '')
Seems reasonable, if you read "first" as meaning "first line in the
remaining iterator". But as soon as I pass a concrete sequence of lines,
rather than an iterator, I'll have an infinite loop.
Let me try to anticipate your likely objection:
"Well of course it's not going to work, you're calling `first`
instead of `next`. When you want to consume items, you should
call `next`."
Okay, but isn't it your position that the difference between "first" and
"next" is a difference that makes no difference? (See quote below.)
Obviously I could re-write that function in many ways, but the simplest
fix is to call `lines = iter(lines)` at the top of the function.
But if I do that, I don't need "first", I can just use `next`, which
reads better. What does "first" give me?
I know, I can use it to look-ahead into a sequence:
head = first(lines) # like lines[0] but works if it's empty
if head is None:
print("empty")
else:
do_stuff(lines)
Works fine... until I pass an iterator, and then wonder why the first
line is skipped.
The thing is, we're fooled by the close similarity of iteration over
iterators and other iterables (sequences and containers). Destructive
iteration and non-destructive iteration is a big difference. Utility
functions like the proposed `first` that try to pretend there is no such
difference are, I believe, a gotcha waiting to happen.
> Your life is an iterator.
Speak for yourself. My life is a box of chocolates.
> "Next" and "first" are basically
> synonymous when you can't go backwards. IMO the distinction you
> describe here isn't actually significant at all - either way, you get
> the first element of "whatever remains", and the only difference is
> whether it's nondestructive (with most containers) or destructive
> (iterators).
--
Steven
_______________________________________________
Python-ideas mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at
https://mail.python.org/archives/list/[email protected]/message/2DREAFZQEE6ZMWFB2OMUN5XDTQMPGKB6/
Code of Conduct: http://python.org/psf/codeofconduct/