I'm the poster of the question, and I would
like to summarize here what I have learned and what I think has been
left. (I do not plan to post it as a new question.)
In Python, StopIteration
coming from the __next__
method of an
iterator is treated as a signal that the iterator has reached the end.
(Otherwise, it's the signal of an error.) Thus, the __next__
method of an iterator must catch all StopIteration
which is not
meant to be a signal of the end.
A map object is created with a code of the form map(func, *iterables)
, where func
is a function, and *iterables
stands
for a finite sequence of one (as of Python 3.8.1) or more iterables.
There are (at least) two kinds of subprocess of a __next__
process
of the resulting map object which may raise StopIteration
:
- Process where the
__next__
method of one of the iterables in
the sequence *iterables
is called.
- Process where the argument
func
is called.
The intention of map
as I understand it from it's document
(or displayed by help(map)
) is that StopIteration
coming from a subprocess of kind (2) is NOT the end of the map object.
However, the current behaviour of a map object's __next__
is such
that it's process emits StopIteration
in this case. (I haven't
checked whether it actually catches StopIteration
or not. If it
does, then it raises StopIteration
again anyway.) This appears
the cause of the problem I asked about.
In an answer above, user2357112 supports Monica (let me friendlily abbreviate the name as "User Primes") finds the consequence of this ugly, but answered it's Python's fault,
and not map
's. Unfortunately, I do not find convincing support for this conclusion in the answer. I suspect fixing map
would be better, but
some other people seem to disagree to this for performance reasons. I
know nothing about the implementation of built-in functions of Python
and cannot judge. So this point has been left for me. Nevertheless, User Primes' answer was informative enough to leave the left question
unimportant for me now. (Thanks user2357112 supports Monica again!)
By the way, the code I tried to post in a comment to User Primes'
answer is as follows. (I think it would have worked before PEP 479.)
def map2(function, iterable):
"This is a 2-argument version for simplicity."
iterator = iter(iterable)
while True:
arg = next(iterator) # StopIteration out here would have been propagated.
try:
yield function(arg)
except StopIteration:
raise RuntimeError("generator raised StopIteration")
What's below is a slightly different version of this (again, a
2-argument version), which might be more convenient (posted with the hope of getting
suggestions for improvement!).:
import functools
import itertools
class StopIteration1(RuntimeError):
pass
class map1(map):
def __new__(cls, func, iterable):
iterator = iter(iterable)
self = super().__new__(cls, func, iterator)
def __next__():
arg = next(iterator)
try:
return func(arg)
except StopIteration:
raise StopIteration1(0)
except StopIteration1 as error:
raise StopIteration1(int(str(error)) + 1)
self.__next__ = __next__
return self
def __next__(self):
return self.__next__()
# tuple(map1(tuple,
# [map1(next,
# [iter([])])]))
# ---> <module>.StopIteration1: 1