12

suppose I have some manager object. This object's API has a main_hook function, that gets another function f as it's argument, and runs the given f in a loop, doing some stuff in between each iteration:

def main_hook(self,f):
    while (self.shouldContinue()):
        #do some preparations
        f(self)
        #do some tear down

Now, I also have (more accurately, would like to have) a function stop_and_do_stuff, that once called, stops main_hook dead in it's tracks, returns the control to whichever func called main_hook, and after that func finished what's it doing, get control back to main_hook and continue. Basically the result will be the same as doing

def main_hook(self,f):
    while (self.shouldContinue()):
        #do some preparations
        yield
        #do some tear down

Except that instead yield I want to have a call to f(), while giving f the option to call self.stop_and_do_stuff()

I can't work around this by making f also a generator for 2 reasons:

1.f isn't part of my API - it's given to me by a user who uses my lib

2.Even if could ask him to use yield, the place in the code in which he will need to call stop_and_do_stuff won't be directly inside f, rather in some place in the function stack which will be inside f(), but not directly in it, e.g

def h(manager):
    #do stuff
    if should stop:
        manager.stop_and_do_stuff()
    #do more stuff
def g(manager):
    #some stuff
    if should stop:
        manager.stop_and_do_stuff()
    #more stuff
    if should stop again:
        manager.stop_and_do_stuff()  
    if should call h:
        h()
def f(manager):
    g(manager)

so if I choose to make f a generator, I also need to make g a generator and also h, otherwise this trick won't work.

Is there any solution to all of this? maybe I'm trying to solve it the wrong way?

(I know this question is long and ugly - it's the best I could do. If something isn't clear please tell me and I'll clarify it)

EDIT

Maybe pep 342 is the solution?

Community
  • 1
  • 1
olamundo
  • 23,991
  • 34
  • 108
  • 149
  • I have the same understanding as Anurag and also think (like him) that you really didn't asked a question but provided elements of your own solution (that does not work yet). So the best you can expect is gettting your solution to work, not getting a really pythonic solution. Also, from what I've seen in the question I have a strange feeling. It seems odd to me to speak of functions as "doing something" instead of "returning a result", sounds like what you are doing is mostly some interactive side effect. Is it ? – kriss Jun 19 '10 at 08:21
  • 1
    it is not clear to me that if f is a foreign lib function how can it call stop_and_do_stuff in middle and if it can do that why can't it yield? – Anurag Uniyal Jun 19 '10 at 11:03
  • @Anurag- f gets a `manager` object as it's argument, and it has the function stop_and_do_stuff – olamundo Jun 19 '10 at 13:41

5 Answers5

8

My previous answer describes how to do this in Python2, which is very ugly. But now I ran across PEP 380: Syntax for Delegating to a Subgenerator. That does exactly what you ask. The only problem is that it requires Python3. But that shouldn't really be a problem.

Here's how it works:

def worker():
    yield 1
    yield 2
    return 3

def main():
    yield 0
    value = yield from worker()
    print('returned %d' % value)
    yield 4

for m in main():
    print('generator yields %d' % m)

The result of this is:

generator yields 0
generator yields 1
generator yields 2
returned 3
generator yields 4

Exceptions are passed through the way you would expect.

Bas Wijnen
  • 1,288
  • 1
  • 8
  • 17
  • this doesn't solve it... you made `main` uses yield directly now, unlike the question states. – Gulzar Jan 05 '21 at 11:13
  • Those yields are just to show the order in which things are executed. "yield 0" and "yield 4" can be removed. The "yield from" command will still work (and will also mean that main is a generator function itself). – Bas Wijnen Apr 15 '21 at 13:18
  • Exactly. Main can't become a generator. – Gulzar Apr 15 '21 at 15:03
  • It can, and it must. There is no other way to do this. As OP writes at the end, f and g and h must all be generators. That was very cumbersome in Python2, but with "yield from" it's very elegant. Of course there is an option for not making f a generator, but in that case it cannot wait for another generator either (neither itself nor any of the functions it calls). If that is what the user wants, the library can call f, see if the result is a generator and if so, iterate over it. Otherwise, use (or discard) the return value. – Bas Wijnen Apr 15 '21 at 20:20
  • The question states it can't, so the answer is invalid if it must. – Gulzar Apr 15 '21 at 23:22
  • Feel free to add an answer that simply states "this is impossible". That's true, but I believe this answer is more helpful. – Bas Wijnen Apr 17 '21 at 14:20
4

I believe I should also add an answer from the other point of view, ie not trying to explain how you could achieve what we can understand of what you are trying to do, but why yield definitely couldn't possibly work.

When a function contains yield keyword it is deeply modified. It is still a callable but not a normal function any more : it becomes a factory that return an iterator.

From the caller's point of view there is no difference between the three implementations below (except that the yield one is so much simpler).

##########################################
print "Function iterator using yield",

def gen():
    for x in range(0, 10):
        yield x

f = gen()
try:
    while True:
        print f.next(),
except StopIteration:
    pass

for x in gen():
    print x,

print

#########################################
print "Class iterator defining iter and next",

class gen2(object):

    def __init__(self):
        self.index = 0;
        self.limit = 10;

    def __iter__(self):
        return self

    def next(self):
        if self.index >= self.limit:
            raise StopIteration
        self.index += 1;
        return self.index - 1;


f = gen2()
try:
    while True:
        print f.next(),
except StopIteration:
    pass

for x in gen2():
    print x,
print

#########################################
print "Function iterator using iter() and sentinel",
def gen3():
    def g3():
        if g3.index is None:
            g3.index = 0
        g3.index += 1;
        return g3.index - 1

    g3.index = None
    return iter(g3, 10)

f = gen3()
try:
    while True:
        print f.next(),
except StopIteration:
    pass

for x in gen3():
    print x,
print

Then you should understand that yield is not much about control flow, but about keeping call context inside variables. Once it is understood you have to decide if the API of main_loop really want to provide an iterator to it's caller. Then if so, if f may loop it must should also be an iterator (and there should be a loop around calls to f() like below).

def main_hook(self,f):
    while (self.shouldContinue()):
        #do some preparations
        for v in f(self):
            yield v
        #do some tear down

But you should not care if f() has to call inner functions g(), etc. That is completely irrelevant. You provide a lib and it is your user problem to call with an appropriate iterable. If you believe your lib user won't be able to, you will have to change the overall design.

Hope it helps.

0TTT0
  • 1,288
  • 1
  • 13
  • 23
kriss
  • 23,497
  • 17
  • 97
  • 116
1

I don't understand the whole either (what does the main_hook caller look like ?), but i would say, Throw a StopNow exception, when you should stop, just like you should throw StopIteration when your generator is finished.

here is how i understood the thing as well as what i would do.

class StopNow(Exception):
    pass

def main_hook(self,f):
    got_stop_now_exc = False
    while (!got_stop_now_exc and self.shouldContinue()):
        #do some preparations
        try:
             f(self)
        except StopNow:
             got_stop_now_exc = True

        #do some compulsary tear down, exception or not

def stop_and_do_stuff()
    raise StopNow()
def my_f():
    if needed:
        stop_and_do_stuff()

def the_main_hook_caller():
    while i_should:
        managerthingie.main_hook(my_f)
        do_stuff()
BatchyX
  • 4,986
  • 2
  • 18
  • 17
  • 2
    The question was not "how do I abort the function?", but rather "how to I suspend it in a way that I can later resume?" A generator does exactly that, using yield. The only problem is that if the generator wants to call a function that should yield, then that function instead becomes a generator itself, which ruins everything. The question is how to get around it. I have posted what I currently do as an answer, but I'm certainly interested in more elegant solutions. – Bas Wijnen Feb 15 '14 at 06:45
0

I am not quite sure what exactly you are trying to achieve, so maybe if you can explain the problem more instead of giving solution that would be better.

From my partial understanding why don't you do something like this

def main_hook(self,f):
    while (self.shouldContinue()):
        #do some preparations
        stop_and_do_stuff = f(self)
        if stop_and_do_stuff :
            yield
            
        #do some tear down
    

So basically f returns a flag to stop or not, and if it says stop we yield to function which called main_hook and that function can continue after doing some stuff

e.g.

class A(object):
    def main_hook(self,f):
        while (self.shouldContinue()):
            #do some preparations
            stop = f(self)
            if stop:
                yield
                
            #do some tear down
        
    def shouldContinue(self):
        return True
    
def f(a):
    return True

a = A()
for x in a.main_hook(f):
    print x
Gulzar
  • 23,452
  • 27
  • 113
  • 201
Anurag Uniyal
  • 85,954
  • 40
  • 175
  • 219
  • This solution is far from the desired result. The whole point was that `f` can call `stop_and_do_stuff` in the middle of it, without finishing (i.e as though `f` has a `yield` in it) . In your solution, `f` has to finish and return a flag, which forces `f` to finish before `stop_and_do_stuff` can be called. Regarding my question, if you can elaborate what's not clear in my explanation, I would be happy to clear it up. – olamundo Jun 19 '10 at 08:28
  • @noam: a part of what is unclear is the "stop_and_" in "stop_and_do_stuff". Either you want to stop f() like a return, that is what Anurag understood, Either it's just an usual function call inside of f() (in wich case it's also easy, you just have to provide a function to call). Looks like you want to call a function, but not provide it explicitely. The usual use of yield in python is the other way (give back control), your use looks more a ruby's yield (call to anonymous function). The larger problem is that you do not explain what you are trying to achieve, but only how you did it. – kriss Jun 19 '10 at 11:54
  • this is the best solution so far, but it changes `main_hook` which is not as requested – Gulzar Jan 05 '21 at 11:23
0

The behavior you describe looks exactly like a simple function call. Like below.

def f(manager):
    print("Entering f")
    manager.stop_and_do_stuff()
    print("Exiting f")

class Manager(Object):
    def shouldContinue(self):
        return True

    def stop_and_do_stuff(self):
        print("Manager stop and do stuff")

    def main_hook(self,f):
        while self.shouldContinue()
            print("Manager Setup")
            f(self)
            print("Manager Tear Down")

No problem if f() is provided by another user of if stop_and_do_stuff is called from some inner function. If you also want the manager to be able to unwind stack from stop_and_do_stuff and really exit in some cases, no problem. Just raise some exception from it and you would catch it from main_hook or upper code.

You should be able to do from inside stop_and_and_do_stuff() whatever you want to do from the caller of main hook. If not you should explain why.

What is unclear in the question is what's happening on the caller side of main_hook() and why you would want to be able to exit the main_hook loop, but not really. Either the main_loop caller expect a generator either it does not. You need to explain that part if you want to get a sensible answer (some context informations would also be nice, if you really explain WTF you are trying to do, and your real restrictions - you said f is provided by some other user and main_hook is in a lib, what of main_hook caller ? - there is probably well known usual solutions).

kriss
  • 23,497
  • 17
  • 97
  • 116
  • 1
    As I understand it (and at least, what I have an issue with myself), the point is that he wants to write a complex generator, which will not (only) yield "directly", but also calls functions that will yield it. However, when putting yield in a function, that function becomes a generator itself, so that doesn't work. My own answer here shows my ugly workaround. If you know a more elegant way to do it, please let us know. PEP 342 talks about coroutines, which are exactly what we want. And we want to call functions from coroutines. That doesn't seem possible. – Bas Wijnen Feb 15 '14 at 06:51
  • `main_hook`'s flow should start and stop at will of the outside user, just like a generator. – Gulzar Jan 05 '21 at 11:26