
On Wed, Dec 11, 2019 at 7:46 PM Steven D'Aprano <steve@pearwood.info> wrote:
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.
An iterator doesn't HAVE anything other than "the rest". By definition, an iterator's contents is the same as the iterator's remaining contents. Do you consider it a fundamental flaw of the "in" operator that, when used with an iterator, it is destructive?
x = [1, 2, 3, 4, 5] 3 in x True 3 in x True x = iter(x) 3 in x True 3 in x False
Does the meaning of "in" change when used on an iterator? Or is this an acceptable consequence of the inherently destructive nature of querying an iterator?
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?
Nothing. It's not the tool for this job. You're trying to shoehorn first() into a job that isn't its job.
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.
And ordered vs unordered is also a big difference. Should first() raise an error with sets because there's no real concept of "the first element"? With a list, first(x) will remain the same value even if you add more to the end of the list, but unrelated mutations to a set might change which element is "first". Does that mean that first() and next() are undefined for sets? No. We just accept that there are these differences. ChrisA