1

I am learning generator from this thread. This is really a good example for generator. But I am confused by one of the example code.

>>> class Bank(): # let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # when everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())  
$100  
>>> print(corner_street_atm.next())  
$100  
>>> print([corner_street_atm.next() for cash in range(5)])  
['$100', '$100', '$100', '$100', '$100']  
>>> hsbc.crisis = True # crisis is coming, no more money!  
>>> print(corner_street_atm.next())  
<type 'exceptions.StopIteration'>   
>>> wall_street_atm = hsbc.create_atm() # it's even true for new ATMs  
>>> print(wall_street_atm.next())  
<type 'exceptions.StopIteration'>  

>>> hsbc.crisis = False # trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>

>>> brand_new_atm = hsbc.create_atm() # build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

When hsbc.crisis is reset to False, the corner_steet_atm can not yield anything but StopIteration. Why does this happen. I think the corner_street_atm is not empty after crisis.

Community
  • 1
  • 1
l0o0
  • 773
  • 1
  • 9
  • 26

3 Answers3

2

The problem is coming from the fact that when you say

corner_street_atm = hsbc.create_atm()

You are calling the function once, and that is all the times it will be called. Thus, let's keep a view of what is happening

  def create_atm(self):
        # Called right away, when you say the above line and only then
        print("Called function") 

        # Loops while crisis = False
        while not self.crisis: 
            yield "$100"

Now, we can ask the question of where our method is looking when crisis is True, and we find that it is point at the spot shown below:

  def create_atm(self):
        # Called right away, when you say the above line and only then
        print("Called function") 

        # Loops while crisis = False
        while not self.crisis: 
            yield "$100"
            #<------ Where you function points after you set crisis to false and get the next

Well, there is nothing there, so you get an error.

What you are really looking for appears to be an infinite loop in atm. Something like this:

class Bank():
    crisis = False

    def create_atm(self):
        print("Called function")
        while True:
            if not self.crisis:
                yield "$100"
            else:
                yield "$0"

Careful with this, as your final call

for cash in brand_new_atm:
    print(cash)

will loop forever because you currently don't see how much cash is in the atm (So as long as there isn't a crisis it will just spew out dollar bills.

Michael Lampe
  • 664
  • 5
  • 11
1

Generally it is due to that you called the corner_street_atm.next() after you set the hsbc.crisis=True. If you do it like this, you would have no problem:

hsbc.crisis=False

corner_street_atm = hsbc.create_atm()

hsbc.crisis=True

hsbc.crisis=False

corner_street_atm.next()
Out[54]: '$100'

The reason is the the corner_street_atm is a generator(iterator) object, and once The generator created in this case will call o with no arguments for each call to its next() method; if the value returned is equal to sentinel, StopIteration will be raised, otherwise the value will be returned[1].

The thing is once you call the corner_street_atm.next() after the hsbc.crisis=True, the Exception will be raised. And once the exception is raised, there is not way to loop this object any more and unless you create a new object!

2342G456DI8
  • 1,819
  • 3
  • 16
  • 29
1

The problem here is that your generator (which is a type of iterator) has been exhausted. Once it has been exhausted, an iterator should not flip back and forth between yielding values and raising the StopIteration (in other words your example is behaving as it should even though it isn't what you want it to do). Here is some more about the iterator protocol:

https://docs.python.org/3/tutorial/classes.html#iterators

The reason the generator stops yielding values in this particular case is that once you exit the while loop (which happens as soon as next is called when 'crisis' is True), there is no way to re enter the loop.

What you most likely want to be doing is using the __iter__ magic method on a separate ATM object. More about that here:

https://docs.python.org/3/library/functions.html#iter

It might look something like this:

class ATM():
    def __init__(self, owner):
        self.owner = owner
    def __iter__(self):
        yield "$100"
    def __call__(self):
        if self.owner.crisis is True:
            return 'crisis!'

class Bank(): # let's create a bank, building ATMs
    crisis = False
    def create_atm(self):
        return ATM(self)

You'll want to get an atm iterator this way:

>>> hsbc = Bank() 
>>> corner_street_atm = hsbc.create_atm() # this way
>>> corner_street_atm = ATM(hsbc) # or this way
>>> corner_street_atm_iterator = iter(corner_street_atm, 'crisis!')

The last line uses an optional form of the iter function since a second argument is supplied. The iterator returned by this second form raises StopIteration if corner_street_atm.__call__() returns the sentinel value that was supplied to iter. In this case the sentinel is 'crisis!'.

And iterate through it like this:

>>> print(corner_street_atm_iterator.next())  
$100  

NOTE: an ATM object never runs out of money if you don't use iter with the 'crisis!' sentinel argument!

infinite_atm_iterator = iter(ATM(some_bank))
Rick
  • 43,029
  • 15
  • 76
  • 119