
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 <steve@pearwood.info> 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