We're not changing next(). It's too fundamental to change even subtly.

We might add itertools.first(), but not builtins.first(). This kind of
functionality is not fundamental but it's easy to get slightly wrong
(witness many hasty attempts in these threads).

itertools.first() should be implemented in C, but its semantics should be
given by this (well, let me see if I can get it right):

def first(it, /, default=None):
    it = iter(it)
    try:
        return next(it)
    except StopIteration:
        return default

Note the call to iter() -- this ensures it works if the argument is e.g. a
collection. Because iter() on an iterator returns itself, the function also
works if the argument is alread an iterator (e.g. first(iter("abc"))).

On Sun, Dec 8, 2019 at 6:18 AM Chris Angelico <ros...@gmail.com> wrote:

> On Mon, Dec 9, 2019 at 12:48 AM Oscar Benjamin
> <oscar.j.benja...@gmail.com> wrote:
> >
> > On Sat, 7 Dec 2019 at 00:43, Steven D'Aprano <st...@pearwood.info>
> wrote:
> > >
> > > On Fri, Dec 06, 2019 at 09:11:44AM -0400, Juancarlo Añez wrote:
> > >
> > > [...]
> > > > > Sure, but in this case, it isn't a fragment of a larger function,
> and
> > > > > that's not what it looks like. If it looked like what you wrote, I
> would
> > > > > understand it. But it doesn't, so I didn't really understand what
> it was
> > > > > supposed to do, until I read the equivalent version using
> first/next.
> > > > >
> > > >
> > > > Exactly my point.
> > >
> > > Indeed, and I agree with that. But I still don't see what advantage
> > > there is to having a `first` builtin which does so little. It's a
> really
> > > thin wrapper around `next` that:
> > >
> > >     calls iter() on its iterable argument
> > >     supplies a default
> > >     and then calls next() with two arguments
> > >
> > > I guess my question is asking you to justify adding a builtin rather
> > > than just educating people how to use next effectively.
> >
> > The real problem with next is the fact that it raises StopIteration
> > with no default. That can be useful when you are *implementing*
> > iterators but it is very much not what you want when you are just
> > *using* iterators. That makes next something of a footgun because it's
> > tempting to write something like
> >
> > first = next(iter(iterable))
> >
> > but if there is no applicable default value that should really be
> >
> > try:
> >     first = next(iter(iterable))
> > except StopIteration:
> >     raise ValueError
>
> If you're defining a first() function, then this would absolutely be
> correct. I don't think it's a fundamental problem with next(), since
> its job is to grab the next value from an iterator, or tell you to
> stop iterating. (BTW, when you're converting exceptions like this in
> production code, use "raise ValueError from None" to hide the
> StopIteration from the traceback.)
>
> > There is a PEP that attempted to solve this problem:
> > PEP 479 -- Change StopIteration handling inside generators
> > https://www.python.org/dev/peps/pep-0479/
> >
> > However PEP 479 (wrongly IMO) attributed the problem to generators
> > rather than iterators. Consequently the fix does nothing for users of
> > itertools type functions like map etc. The root of the problem it
> > attempted to fix is the fact that bare next raises StopIteration and
> > so is not directly suitable in situations where you just want to get
> > the next/first element of an iterable.
>
> Hmm. Actually, I'd say that PEP 479 was correct, but that map() is
> wrong. If you define map() in the most obvious pure-Python way, it
> will be a generator:
>
> def map(func, *iter):
>     while True:
>         args = [next(it) for it in iter]
>         yield func(*args)
>
> (modulo some error handling)
>
> Written thus, it would be guarded by PEP 479's conversion of
> StopIteration. I'd say that a more correct implementation of map would
> be something like:
>
> def map(func, *iter):
>     while True:
>         args = [next(it) for it in iter]
>         try:
>             yield func(*args)
>         except StopIteration:
>             raise RuntimeError("mapped function raised StopIteration")
>
> > The reason this is particularly pernicious is that it leads to silent
> > action-at-a-distance failure and can be hard to debug. This was
> > considered enough of a problem for PEP 479 to attempt to solve in the
> > case of generators (but not iterators in general).
>
> Agreed, but the problem isn't iterators or next. The problem is with
> functions that convert iterators into other iterators, while doing
> other work along the way; if the *other work* raises StopIteration, it
> causes problems.
>
> > > This is how I would implement the function in Python:
> > >
> > >     def first(iterable, default=None):
> > >         return next(iter(iterable), default)
> >
> > I agree that that doesn't need to be a builtin. However I would
> > advocate for a function like this:
> >
> > def first(iterable, *default):
> >     iterator = iter(iterable)
> >     if default:
> >         (default,) = default
> >         return next(iterator, default)
> >     else:
> >         try:
> >             return next(iterator)
> >         except StopIteration:
> >             raise ValueError('Empty iterable')
>
> Ahh the good ol' bikeshedding. The simpler form guarantees that next()
> is always given a default, ergo it shouldn't ever leak. If you'd
> prefer it to raise ValueError, then I reckon don't bother implementing
> the version that takes a default - just let next() do that job, and
> implement first() the easy way:
>
> def first(iterable):
>     it = iter(iterable)
>     try:
>         return next(it)
>     except StopIteration:
>         raise ValueError("Empty iterable")
>
> > # silently aborts if any of csvfiles is empty
> > for header in map(lambda e: next(iter(e)), csvfiles):
> >     print(header)
>
> (With files, there's no point calling iter, as it'll return the same
> thing. So you could write this as map(next, csvfiles).)
>
> > This kind of confusion can come with iterators and iterables all the
> > time. I can see that the name "first" is potentially confusing.
> > Another possible name is "take" which might make more sense in the
> > context of partially consumed iterators. Essentially the idea should
> > just be that this is next for users rather than implementers of
> > iterables.
> >
>
> Maybe. If it's imported from itertools, though, there shouldn't be any
> confusion.
>
> Since it's somewhat orthogonal to the discussion of first(), I'm going
> to spin off a separate thread to look at PEP479ifying some iterator
> conversion functions.
>
> ChrisA
> _______________________________________________
> Python-ideas mailing list -- python-ideas@python.org
> To unsubscribe send an email to python-ideas-le...@python.org
> https://mail.python.org/mailman3/lists/python-ideas.python.org/
> Message archived at
> https://mail.python.org/archives/list/python-ideas@python.org/message/GW3J7S7U6K5YUHPFS6XMGP5HXS43T432/
> Code of Conduct: http://python.org/psf/codeofconduct/
>


-- 
--Guido van Rossum (python.org/~guido)
*Pronouns: he/him **(why is my pronoun here?)*
<http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/ZCF3O6JZXHIC6R3XRP6NTMQRGJG6ZCI3/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to