1

I have var DICTIONARY, which is a dictionary where the keys are English letters and the values are words that start with the corresponding letter. The initial filling of DICTIONARY looks like this:

DICTIONARY = {
    'a': 'apple',
    'b': 'banana',
    'c': 'cat',
    'd': 'dog',
}

My code has 2 while loops, since higher try-except would end the entire generator loop:

def alphabet():
    while True:
        try:
            letter = yield  #waiting for first input from send
            while True:
                try:
                    letter = yield DICTIONARY[letter]
                except KeyError:  
                    letter = yield 'default'  # return 'default', if key is not found
                except Exception:
                    letter = yield 'default'
        except KeyError:
            letter = yield 'default' 
        except Exception:
            letter = yield 'default' 

However for the input:

coro = alphabet()
next(coro)
print(coro.send('apple'))
print(coro.send('banana'))
print(coro.throw(KeyError))
print(coro.send('dog'))
print(coro.send('d'))

expected output:

default
default
default
default
dog

But I don't catch last default - it's None:

default
default
default
None
dog

What is wrong?

9
  • 3
    It would be easier to understand if instead of the same "default" in 4 different cases there would be 4 different results. Commented Oct 7, 2024 at 21:32
  • I don't see the point of the outer try. You already catch any possible exceptions with the inner try. It's just protecting letter = yield which can't raise an exception. Commented Oct 7, 2024 at 21:34
  • 1
    And you could also use DICTIONARY.get(letter, 'default') rather than catching KeyError. Commented Oct 7, 2024 at 21:35
  • From your problem description, I think your dictionary should store strings that point lists of strings, not strings that point to strings Commented Oct 7, 2024 at 21:37
  • 1
    @Dunes I think you read it backwards. They expect default, they're actually getting None. Commented Oct 8, 2024 at 4:37

3 Answers 3

3

What Went Wrong

The issue is that at you have put yield statements in exception handlers. This means that the code can break out of both your while loops and terminate the coroutine. This happens when you throw() an exception in the coroutine when it has just yielded from one of statements that are in exception handlers, but without their own try/catch. Pretty sure, this is not what you intended.

In your example, what is happening is that when you send 'banana' you cause a KeyError in the generator. So when you next do coro.throw(KeyError), you in the inner most exception handler for KeyError. Because there is no additional exception handling, the code breaks out of the inner while loop. We then catch the KeyError in the exception handler for the outer loop and start the outer while loop again. This means that the next yield from the generator will be letter = yield -- the implicit yielding of None. I've annotated your code to try and show what happens. Start at # 1: and follow the numbers until # 6:

def alphabet():
    while True:
        try:
            # 6: next yield after coro.throw(KeyError). So when do we do coro.send('dog'),
            # the coro yields None
            letter = yield  #waiting for first input from send
            while True:
                try:
                    # 1: coro.send('banana') sets letter to banana -- no exception
                    # 2: inner while loop starts again, does `DICTIONARY['banana'] and gets
                    # a KeyError
                    letter = yield DICTIONARY[letter]
                except KeyError:
                    # 3: yield 'default' after KeyError('banana')
                    # 4: coro.throw(KeyError) causes this statement to raise a KeyError
                    letter = yield 'default'
                except Exception:
                    letter = yield 'default'
        except KeyError:
            # 5: KeyError raised at `4` handled here
            # Had you done throw(KeyError) a second consecutive time, this statement would
            # have raise an exception, and then terminated your coroutine.
            letter = yield 'default' 
        except Exception:
            letter = yield 'default'

Fixing the Problem

You can get your desired behaviour by only ever doing yield statements in a try/except. You can prevent the need to nest while loops by pulling apart the two things (yield and DICTIONARY[letter]) that can cause exceptions and putting them in their own try/excepts.

def alphabet():
    next_result = None
    while True:
        try:
            letter = yield next_result
        except Exception as ex:
            # yield statement raised an exception -- letter is unchanged
            print(f'{ex!r} was raised')
            next_result = 'default-after-exception'
        else:
            # yield statement set letter to a new value
            next_result = DICTIONARY.get(letter, f'default-{letter}-is-missing')
            ## alternatively:
            # try:
            #     next_result = DICTIONARY[letter]
            # except KeyError:
            #     next_result = 'default'

coro = alphabet()
assert next(coro) is None
assert coro.send('apple') == 'default-apple-is-missing'
assert coro.send('banana') == 'default-banana-is-missing'
assert coro.throw(KeyError) == 'default-after-exception'
assert coro.send('dog') == 'default-dog-is-missing'
assert coro.send('d') == 'dog'
Sign up to request clarification or add additional context in comments.

Comments

2

I believe that there is two errors. First - is that you are using while True: block twice. I would simplify the code into smth like this:

def alphabet():
    while True:
        try:
            letter = yield  #waiting for first input from send
            try:
                word = DICTIONARY[letter]
            except KeyError:  
                word = 'default'  # return 'default', if key is not found
            except Exception:
                word = 'default'
            yield word
        except KeyError:
            yield 'default' 
        except Exception:
            yield 'default' 

Second issue is that generator stack on previous yield function. If you'll add more next calls the issue will be fixed:

coro = alphabet()
next(coro)
print(coro.send('apple'))
next(coro)
print(coro.send('banana'))
next(coro)
print(coro.throw(KeyError))
next(coro)
print(coro.send('dog'))
next(coro)
print(coro.send('d'))

Output:

default
default
default
default
dog

Comments

0

this is my answer:

def alphabet():
    letter = None
    while True:
        try:
            letter = yield DICTIONARY[letter]  
        except KeyError:
            letter = yield 'default'  

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.