Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce exception argument to iter #64862

Closed
cool-RR mannequin opened this issue Feb 17, 2014 · 11 comments
Closed

Introduce exception argument to iter #64862

cool-RR mannequin opened this issue Feb 17, 2014 · 11 comments
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-feature A feature request or enhancement

Comments

@cool-RR
Copy link
Mannequin

cool-RR mannequin commented Feb 17, 2014

BPO 20663
Nosy @rhettinger, @terryjreedy, @cool-RR, @MojoVampire

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = <Date 2017-06-18.18:16:51.634>
created_at = <Date 2014-02-17.17:43:47.501>
labels = ['interpreter-core', 'type-feature']
title = 'Introduce exception argument to iter'
updated_at = <Date 2017-06-18.18:16:51.632>
user = 'https://github.com/cool-RR'

bugs.python.org fields:

activity = <Date 2017-06-18.18:16:51.632>
actor = 'terry.reedy'
assignee = 'none'
closed = True
closed_date = <Date 2017-06-18.18:16:51.634>
closer = 'terry.reedy'
components = ['Interpreter Core']
creation = <Date 2014-02-17.17:43:47.501>
creator = 'cool-RR'
dependencies = []
files = []
hgrepos = []
issue_num = 20663
keywords = []
message_count = 11.0
messages = ['211431', '222169', '222215', '222221', '222227', '223094', '223096', '223138', '223140', '223152', '264236']
nosy_count = 4.0
nosy_names = ['rhettinger', 'terry.reedy', 'cool-RR', 'josh.r']
pr_nums = []
priority = 'low'
resolution = 'rejected'
stage = 'resolved'
status = 'closed'
superseder = None
type = 'enhancement'
url = 'https://bugs.python.org/issue20663'
versions = ['Python 3.6']

@cool-RR
Copy link
Mannequin Author

cool-RR mannequin commented Feb 17, 2014

See discussion: https://groups.google.com/forum/#!searchin/python-ideas/iter/python-ideas/UCaNfAHkBlQ/5vX7JbpCxDkJ

iter has a very cool sentinel argument. I suggest an additional argument exception; when it's supplied, instead of waiting for a sentinel value, we wait for a sentinel exception to be raised, and then the iteration is finished.

This'll be useful to construct things like this:

    my_iterator = iter(my_deque.popleft, exception=IndexError)

I also suggest being able to pass multiple exceptions in a tuple to have any of them trigger a StopIteration.

@cool-RR cool-RR mannequin added interpreter-core (Objects, Python, Grammar, and Parser dirs) type-feature A feature request or enhancement labels Feb 17, 2014
@cool-RR
Copy link
Mannequin Author

cool-RR mannequin commented Jul 3, 2014

Hey-ho... Anyone feels like implementing this? (I don't program in C so I can't.)

@rhettinger
Copy link
Contributor

Your suggestion and an example appears to have been taken directly from the itertools recipes:

def iter_except(func, exception, first=None):
    """ Call a function repeatedly until an exception is raised.
Converts a call-until-exception interface to an iterator interface.
Like __builtin__.iter(func, sentinel) but uses an exception instead
of a sentinel to end the loop.

Examples:
    bsddbiter = iter_except(db.next, bsddb.error, db.first)
    heapiter = iter_except(functools.partial(heappop, h), IndexError)
    dictiter = iter_except(d.popitem, KeyError)
    dequeiter = iter_except(d.popleft, IndexError)
    queueiter = iter_except(q.get_nowait, Queue.Empty)
    setiter = iter_except(s.pop, KeyError)

"""
try:
    if first is not None:
        yield first()
    while 1:
        yield func()
except exception:
    pass

FWIW, this idea was explored before an aside from the examples given in the docstring above, it seems to have very limited application. Accordingly, it was left as a recipe and not added to itertools or the the iter() function.

@rhettinger rhettinger self-assigned this Jul 3, 2014
@cool-RR
Copy link
Mannequin Author

cool-RR mannequin commented Jul 3, 2014

I understand. Personally I think it'll be useful enough (and more useful to me than the builtin sentinel), but maybe that's just me. And I guess Terry liked it too. I don't know whether other people would like it as well.

@MojoVampire
Copy link
Mannequin

MojoVampire mannequin commented Jul 3, 2014

+1; I've had several cases where I'd have used something like this (for the exact purpose mentioned, to destructively consume an input iterable). I don't think it's more or less useful than the sentinel version, which is convenient for iterating a file by blocks instead of by line, e.g.:

from functools import partial

with open('...', 'rb') as f:
    for block in iter(partial(f.read, 4096), b''):
        ...

But it would still nice to be able to destructively iterate sequences, particularly in CPython, where doing it at the C level can get you atomicity without relying on anything beyond the GIL (and without wrapping infinite while loops in try/except: pass blocks, which is pointlessly verbose).

One issue: You can't just make the second argument allow exception types as well, since it's possible (however unlikely) for an exception type to be a legitimate return type from a function. Making it keyword only would solve that problem though.

@rhettinger
Copy link
Contributor

The recipe has been in the docs for a good while and as far as I can tell, no one ever uses this in real-code. That suggests that it should remain as a recipe and not become part of the core language (feature creep is not good for learnability or maintainability).

I also don't see people writing simple generators that exhibit this functionality. It just doesn't seem to come-up in real code.

[Josh]

I've had several cases where I'd have used something like this

Please post concrete examples so we can actually assess whether code is better-off with or without the feature.

FWIW, the standard for expanding the API complexity of built-in functions is very high. It is not enough to say, "I might have used this a couple of times".

Unnecessary API expansion is not cost free and can make the language worse off on the balance (does the time spent learning, remembering, and teaching the function payoff warrant the rare occasion where it will save a couple of lines of code? is code harder to customize or debug with hard-wired functionality rather than general purpose try-excepts? Do we even want people to write code like this? If heaps, deques, dicts and queues really needed to be destructively iterated, we would have long since had a feature request for them. But we haven't and destructive for-loops would be unusual and unexpected for Python.

@MojoVampire
Copy link
Mannequin

MojoVampire mannequin commented Jul 15, 2014

The main example that comes to mind was a variant of functools.lru_cache I wrote that expired cache entries after they reached a staleness threshold. The details elude me (this was a work project from a year ago), but it was basically what I was describing; a case where I wanted to efficiently, destructively iterate a collections.deque, and it would have been nice to be able to do so without needing a lock (at least for that operation) and without (IMO) ugly infinite loops terminated by an exception. (Caveat: Like I said, this was a while ago; iter_except might only have simplified the code a bit, not saved me the lock)

No, it's not critical. But for a lot of stuff like this, the recipe saves nothing over inlining a while True: inside a try/except, and people have to know the recipe is there to even look for it. The only reason my particular example came to mind is that the atomic aspect was mildly important in that particular case, so it stuck in my head (normally I'm not trying to squeeze cycles out of Python, but performance oriented decorators are a special case). I do stuff that would be simplified by this more often, it's just cases where I currently do something else would all be made a little nicer if I could have a single obvious way to accomplish it that didn't feel oddly verbose/ugly.

@terryjreedy
Copy link
Member

Ram, your opening post here is essentially a copy of your opening post on python-ideas, as if the long discussion there, involving about 6 serious discussants other than you, never happened. Instead of restarting the discussion from scratch, you need to summarize the previous discussion, including a proposed python equivalent for the revised iter() (to exactly pin down the api) and how much support the proposal got.

A couple of notes that might be additions to what I said before: If a collection is fixed during an iteration, then destructive iteration might as well be done, when possible, by normal iteration followed by deletion of the collection.

That leaves as use cases iterations where the collection is mutated during the iteration, as in breadth-first search. For many collections, like deques and hashables, mutation means that direct (normal) iteration with for is prohibited. The current solution is to interleave exception-raising access and mutation within a try and while-True loop.

The following example is similar to a 'breadth-first search'. It uses a deque rather than a list to limit the maximum length of the collection to the maximum number of live candidates rather than the total number of candidates.

from collections import deque
d = deque((0,))
try:
  while True:
        n = d.popleft()
        print(n, len(d))
        if n < 5:
            d.extend((n+1, n+2))
except IndexError:
    pass

This prints 25 items, with a max len before the pop of 11.

Under one variation of the proposal, the try-while block would be replaced by

for n in iter(d.popleft, None, IndexError):
        print(n, len(d))
        if n < 5:
            d.extend((n+1, n+2))

Is the difference enough to add a parameter to iter? Perhaps so. It reduces boilerplate and is a little easier to get right. It eliminates there question of whether the try should be inside or outside the loop. It also matches

d = [0]
for n in d:
    print(n, len(d))
    if n < 5:
        d.extend((n+1, n+2))

which processes the same items in the same order, but extends the list to 25 rather than a max of 11 items. It makes deques and sets look more like direct replacements for lists.

Ram: If you program in Python, you should be able to write a test. To start, replace the prints above with out = [] ... out.append((n, len(d))) and assert that the out lists of the current and proposed deque loops are the same.

Raymond: I started this post with a recommendation to close but changed my mind after actually writing out the two deque examples. The fact that people rarely relegate the try - while True loop to a separate function (which would often be used just once) does not mean that the pattern itself is rare. Just yesterday or so, someone asked on python-list about how to search a graph when the set of candidate nodes got additions and 'for item in set' would not work. He was given the try - while True pattern as the answer.

I think iter(func, ... exception) would be more useful than iter(func, sentinel) is now. The problem with the latter is that for general collections, the sentinel needs to be a hidden instance of object() so that it cannot be placed in the collection and become a non-special legal return value. It is then inaccessible to pass to iter. To signal 'no return', Python often raises an exception instead of returning a special object.

@cool-RR
Copy link
Mannequin Author

cool-RR mannequin commented Jul 15, 2014

Terry: Thanks for your example use case. I hope that Raymond would be convinced.

I take your point regarding summarizing the discussion, sorry about that.

Regarding me writing a test: I'm only willing to write code for a feature for Python if there's general interest from python-dev in getting said feature into Python. If there's general agreement from core python-dev members that this feature should be implemented, I'll be happy to do my part and write the test. But otherwise... I really have better things to do than spending my time writing code that will never be used, especially when it'll take me 10x more time to write it than a python-dev member because 90% of the work would be making the test case comply to the development practices of CPython, and only 10% of it would be to write the actual simple logic.

@terryjreedy
Copy link
Member

Here is a test that now fails.
------------

from collections import deque

d = deque((0,))
old = []
try:
    while True:
        n = d.popleft()
        old.append((n, len(d)))
        if n < 5:
            d.extend((n+1, n+2))
except IndexError:
    pass

d = deque((0,))
new = []
for n in iter(d.popleft, exception=IndexError):
        new.append((n, len(d)))
        if n < 5:
            d.extend((n+1, n+2))

assert new == old
--------

Here is Python code, partly from my python-ideas comments, that makes the test pass. This version allows stopping on both a sentinel value and an exception (or tuple thereof, I believe).
-------

__sentinel = object()

class callable_iterator:
    class stop_exception: pass

    def __init__(self, func, sentinel, exception):
        self.func = func
        self.sentinel = sentinel
        if exception is not None:
            self.stop_exception = exception
    def __iter__(self):
        return self
    def __next__(self):
        try:
            x = self.func()
        except self.stop_exception:
            raise StopIteration from None
        if x == self.sentinel:
            raise StopIteration
        else:
            return x

def iter(it_func, sentinel=__sentinel, exception=None):
    if sentinel == __sentinel and exception == None:
        pass  # do as at present
    else:
        return callable_iterator(it_func, sentinel, exception)

@rhettinger
Copy link
Contributor

I really don't think this is worth it. In my programming career, I may have had an occasion to use this only once every few years.

@rhettinger rhettinger removed their assignment Apr 26, 2016
@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

2 participants