On Sun, Dec 08, 2019 at 01:45:08PM +0000, Oscar Benjamin wrote:
> On Sat, 7 Dec 2019 at 00:43, Steven D'Aprano <st...@pearwood.info> wrote:

[...]
> > But there's a major difference in behaviour depending on your input, and
> > one which is surely going to lead to bugs from people who didn't realise
> > that iterator arguments and iterable arguments will behave differently:
> >
> >     # non-iterator iterable
> >     py> obj = [1, 2, 3, 4]
> >     py> [first(obj) for __ in range(5)]
> >     [1, 1, 1, 1, 1]
> >
> >     # iterator
> >     py> obj = iter([1, 2, 3, 4])
> >     py> [first(obj) for __ in range(5)]
> >     [1, 2, 3, 4, None]
> >
> > We could document the difference in behaviour, but it will still bite
> > people and surprise them.
> 
> This kind of confusion can come with iterators and iterables all the
> time.

Do you have some concrete examples of where this is common, because I 
don't recall seeing anything like this ever. Since next() doesn't accept 
a non-iterator, this confusion doesn't come up for next. I suppose it 
could come up with itertools islice:

    py> s = "abcdefghijklmn"
    py> [list(itertools.islice(s, 0, 1)) for __ in range(5)]
    [['a'], ['a'], ['a'], ['a'], ['a']]


but I've never seen that in real code, so I wouldn't say it happens all 
the time, or even a lot of the time. YMMV I guess, but I like to think 
I have a reasonable grasp of the kinds of gotchas people often trip 
over, and this isn't one of them.

The closest I can think of is the perennial gotcha that you can 
iterate over a sequence as often as you like, but an iterator only once.

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

There's already a take() in the itertools recipes. Rather than add a new 
"first" itertools function, I'd rather promote `take` out of the recipes 
and give it an optional default, something like this:


    def take(n, iterable, /, *default):
        if default:
            (default,) = default
            iterable = chain(iterable, repeat(default))
        return list(islice(iterable, n))


"Get the first item" with a default then becomes:

    a = take(1, iterable, default)[0]

"Get the first 5 items" becomes:

    a, b, c, d, e = take(5, iterable, default)


If you want to distinguish the case where the iterable is empty or 
shorter than you expect, you can pass a known sentinel and check for 
that, or pass no default at all and test the length of the resulting 
list.


-- 
Steven
_______________________________________________
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/O5RYM6ZDXEB3OAQT75IADT4YLXE25HTT/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to