Better while loop

Hello there :kissing_closed_eyes:
I was wondering if there is a shorter or more functional way to write “while loops” in situations where you have to check multiple conditions.
my problem is this while is way too long and it is hard to work with.

while candle_riddle_ans.lower() != 'candle' and candle_riddle_ans.lower() != 'a candle' and candle_riddle_ans.lower() != 'the candle' and candle_riddle_ans.lower() != 'candles':

i am glad to hear your suggestions.

Hi @Naadiyaar :slight_smile:
Could you please tell us more information about the data your script manages, and the desired outputs?

Cheers.

maybe,

lst = ['candle', 'a candle', 'the candle', 'candles']
while all((candle_riddle_ans.lower() != i) for i in lst):

or,

import operator as op
while all((op.ne(candle_riddle_ans.lower(), i)) for i in lst):

or,

while all(map(lambda x: (op.ne(candle_riddle_ans.lower(), x)), lst)):

or,

while all(map(lambda x: (op.ne(map(str.lower, candle_riddle_ans), x)), lst)):

could use a match case, to check for containment also,

class MatchSubstring(str):
  def __eq__(self, other):
    return other in self

match (a := MatchSubstring('candle')):
  case 'candle' | 'a candle' | 'candles' | 'the candle':
    print(1)
  case _:
    print(2)
2 Likes

The “best”, most general way is to write a function:

def equals(value, target):
    value = value.lower()
    if value.startswith('a '):
        value = value[2:]  # deletes the 'a '
    elif value.startswith('the '):
        value = value[4:]  # deletes the 'the '
    return value == target

And then use it like this:

while not equals(candle_riddle_ans, 'candle'):
    ...

The function can be as complicated as you need it to be.

Another way is to learn about regular expressions, or “regexes”.

Regexes are an extremely compact, terse, somewhat cryptic mini-programming language specifically for matching strings. Untested:

import re

while not re.match(r"(?i)(a|(the) )?candle$", candle_riddle_ans):
    ...

This particular regex is not too bad. You can probably guess what most of it does without any more explanation. But more complex regexes get very hairy.

3 Likes

I am trying to write an interactive story, and this is a frame of this part, in which the user should answer the riddle.

print('''
                but the door was locked!
                to open it, you shoud answer one quistion
        
                "My life can be measured in hours.
                I serve by being devoured.
                Thin, I am quick. Fat, I am slow.
                Wind is my foe."
                
                what am i?
            ''')
        candle_riddle_ans = input()

        while candle_riddle_ans.lower() != 'candle' and candle_riddle_ans.lower() != 'a candle' and candle_riddle_ans.lower() != 'the candle':
            print('wrong answer, try again')
            candle_riddle_ans = input()
        else:
            print('it is corroct, door opend')

so I want the reader to try guessing until finding the right answer.

@Naadiyaar
If these are the inputs, then all you have to do is look for the word “candle”, with word boundaries (\b), so you won’t have a false positive with some words that may contain “candle”.
This may help you:

from re import search

print('''
                but the door was locked!
                to open it, you shoud answer one quistion
        
                "My life can be measured in hours.
                I serve by being devoured.
                Thin, I am quick. Fat, I am slow.
                Wind is my foe."
                
                what am i?
            ''')
candle_riddle_ans = input()

while search(r'\bcandle\b', candle_riddle_ans.lower()) is None: 
    print('wrong answer, try again')
    candle_riddle_ans = input()
else:
    print('it is correct, door opened')

Happy coding :slight_smile:

Cheers.

If you don’t want to mess with regexes at all, similar can be achieved with any('candle' in word for word in candle_riddle_ans.lower().split()). I’d still suggest to wrap this up in a function like Steven suggested earlier, though:

def is_correct_answer(answer, expected):
    return any(expected in word for word in answer.lower().split())

while not is_correct_answer(candle_riddle_ans, 'candle'):
    ...

This way you can tweak how is_correct_answer is implemented later (even going to a regex solution if you like) and reuse it for other question/answer bits as well.

3 Likes

interesting module, I will definitely use this feature for this situation.
thanks for reply

Wouldn’t an if statement be better for this, or even the new match functionality, instead of using a loop?

I assume that would be a bug since the program would be thrown in an infinite never ending loop incase the parameters mismatch.

====edit=====
Sorry, I meant to reply to this person’s code instead.

from re import search

print('''
                but the door was locked!
                to open it, you shoud answer one quistion
        
                "My life can be measured in hours.
                I serve by being devoured.
                Thin, I am quick. Fat, I am slow.
                Wind is my foe."
                
                what am i?
            ''')
candle_riddle_ans = input()

while search(r'\bcandle\b', candle_riddle_ans.lower()) is None: 
    print('wrong answer, try again')
    candle_riddle_ans = input()
else:
    print('it is correct, door opened')
1 Like

Never mind… It’s a while else loop. Just saw that :sweat_smile:

1 Like

This code can be simplified:

while input().lower() not in ('candle', 'a candle', 'the candle'):
    print('Wrong answer, try again')
else:
    print('it is correct, door open')
1 Like

By naadi via Discussions on Python.org at 09Jun2022 17:22:

I was wondering if there is a shorter or more functional way to write
“while loops” in situations where you have to check multiple
conditions.
my problem is this while is way too long and it is hard to work with.

while candle_riddle_ans.lower() != 'candle' and candle_riddle_ans.lower() != 'a candle' and candle_riddle_ans.lower() != 'the candle' and candle_riddle_ans.lower() != 'candles':

I’m guessing that this is difficult because of the line length. Others
have pointed out using functions for the test if the test is…
complicated.

However, if your issue is readability (a core tenet of the Zen - run the
“import this” statement), you can use brackets to extend an expression
over multiple lines, for example:

while (
    candle_riddle_ans.lower() != 'candle'
    and candle_riddle_ans.lower() != 'a candle'
    and candle_riddle_ans.lower() != 'the candle'
    and candle_riddle_ans.lower() != 'candles'
):

Also, that particular condition can be written as:

while candle_riddle_ans.lower() not in ('candle', 'a candle', 'the candle', 'candles'):

or:

while candle_riddle_ans.lower() not in (
    'candle',
    'a candle',
    'the candle',
    'candles'
):

according to your taste.

Cheers,
Cameron Simpson cs@cskk.id.au

Some people find this variant readable while it occupies much less vertical space in the code:

while candle_riddle_ans.lower() not in (
        'candle', 'a candle', 'the candle', 'candles'):
    ...

There are already many good suggestions. In general if you have complex conditions writing helper functions is a very good idea to help manage the complexity.

def normalize_word(word):
    """
    Example: 'Candles' becomes 'candle'.
    """
    return word.lower().rstrip('s')

def normalize_answer(answer, words_to_ignore=['a','the']):
    """
    Turn sentence into a list of words.
    Example: 'the candles' becomes ['the', 'candles']
    Then ignore unimportant words, and normalize the other words.
    Example: ['candle']
    """
    words = answer.split()
    return [normalize_word(word) for word in words
            if word not in words_to_ignore]

def is_correct_answer(given_answer, expected_answer):
    return normalize_answer(given_answer) == normalize_answer(expected_answer)

answer = input('What is the answer? ')
while not is_correct_answer(answer, 'candle'):
    answer = input('No. Try again. ')
print('Yes.')

You can then put very complex things in the helper functions.
In the specific example, you are processing natural language text.
There are very advanced Python libraries that implement such functionality.
For example the nltk library can do this for you:

  • find individual words (“tokenizing”)
  • ignore unimportant words (“stopwords”)
  • normalize the words (“stemming”)
    You can put these in your helper functions instead:
"""Use `pip install nltk` to install the nltk natural language toolkit."""
import nltk

# Download some data about languages.
nltk.download('stopwords', quiet=True)

def normalize_word(word, stemmer=nltk.stem.PorterStemmer()):
    """
    Example: 'Candles' becomes 'candle'.
    """
    return stemmer.stem(word).lower()

def normalize_answer(answer,
        words_to_ignore = nltk.corpus.stopwords.words('english')):
    """
    Turn sentence into a list of words.
    Example: 'the candles' becomes ['the', 'candles']
    Then ignore unimportant words, and normalize the other words.
    Example: ['candle']
    """
    words = nltk.tokenize.word_tokenize(answer)
    return [normalize_word(word) for word in words
            if word not in words_to_ignore]

def is_correct_answer(given_answer, expected_answer):
    return normalize_answer(given_answer) == normalize_answer(expected_answer)

# The main code remains simple:
answer = input('What is the answer? ')
while not is_correct_answer(answer, 'candle'):
    answer = input('No. Try again. ')
print('Yes.')

In fact, this can be made both simpler and more correct by just using the in operator directly on the list of words, since you presumably only want to match strings that contain a certain whole word, rather than a word that happens to contain the given substring (for example, if the answer was car, you wouldn’t want to match carpet, vicar or incarceration):

def is_correct_answer(answer, expected):
    return expected in answer.lower().split()

while not is_correct_answer(candle_riddle_ans, 'candle'):
    ...

or, for one specific case, you could get away with just

while not 'candle' in answer.lower().split():
    ...

If you need to match multiple discrete words and don’t want to use regex, for simple cases you can use a set intersection:

while not {"candle", "candlestick", "flambeau"} & set(answer.lower().split())

Just FYI, your strings contain numerous obvious typos; using an editor or IDE with a spellchecker (or asking someone else to proofread your code) is very helpful to catch and fix them.

This is a good point, Cameron. There’s also the line continuance symbol:

while candle_riddle_ans.lower() != 'candle' and \
      candle_riddle_ans.lower() != 'a candle' and \
      candle_riddle_ans.lower() != 'the candle' and \
      candle_riddle_ans.lower() != 'candles':

Pardon the artistic license from PEP8. If the While: loop is short, my sensibility says that readability wins over 4-space indentation.

Your options will depend very much on your situation. In the case you presented, all of your answers contain the string “candle”. As several posts have mentioned, you can simply look for the answer word in the response string.

So in this case you can “refactor” your code (restructure it to achieve the same result) into only checking one condition.

answer = 'candle'
prompt = "What is your answer? : "
response = ''
while answer not in response:
    response = input(prompt).lower()
    prompt = "Incorrect.  Try again: "
print("Correct!!")

This code has a some string swapping but otherwise is simple and very readable. But it also has some deficiencies:

  1. What if the user enters “Candle” with a capital ‘C’?
    This is already handled in the code above with a simple lower() function to make all user replies lowercase.
  2. What if the user enters “candleabra”?
    If you want this to be an incorrect answer, you will need to evaluate the user’s response with more sophisticated code, such as from one of the answers above or the list approach in the post below this one.

So it’s up to you how intelligent/sophisticated you want your answer evaluations to be. If you’re doing this riddle game as a learning exercise, I recommend that you take a simple approach that gives you a working result.

Using a simple approach will give you PLENTY of opportunities to practice manipulating and evaluating strings. You can always revise the evaluations of certain riddles as a later Version.

One simple way to check several possible answers, such as when an action could be described by multiple words [hit, smack, bash, thump, wallop, kick, bang, thwack], is to use a list and see if the user’s response contains any words or phrases in the “correct answers” list.
So a possible approach is:

  1. Break the user’s response into a list of words.
  2. See if any of the user’s words match a word on the answer list.
#STEP 1: Break response into words
respWords = []
response = input(prompt).lower()
i = 0
for j, char in enumerate(response):
    if char == ' ':
        respWords.append(response[i:j])
        i = j + 1
respWords.append(response[i:len(response)])
print(respWords)  #View result

This code uses “list slicing” to slice out the words, since a string value is treated as a list of characters. The notation is fairly simple: listName[start:stop] where start and stop are positions in the list.

The last line before print() adds the last word in the user’s response to the respWords list. (The for: loop does not add the last word because there is no space after it.)

Or you can use the built-in split() function to make the list of words:

#STEP 1: Split the response
respWords = response.split()

NOTE: We aren’t stripping out commas or other punctuation that will stay attached to the words and mess up the ‘==’ comparison.

Now we can compare the words in the two lists:

#STEP 2: Check for answer words
for word in respWords:
        if word in action: correctAns = True

Putting it all together in a while: loop gives:

actions = ['hit', 'smack', 'bash', 'thump', 'wallop', 'kick', 'bang', 'thwack']
respWords = []
prompt = "What would you like to do? : "
result = ''
response = ''
correctAction = False
while not correctAction:
    print(result)
    response = input(prompt).lower()
    respWords = response.split()
    for word in respWords:
        if word in actions: correctAction = True
    result = "Nothing happened."

print("The door lock broke.")
print("The door is open!!")

You could also process the respWords list a second time to see if the user had mentioned ‘door’ as the target.

If you know about creating functions, then you can probably figure out how to make the code above into a function. If functions aren’t in your toolkit yet, just program it all linearly and learn about strings and loops. You’ll get to functions soon enough. :sunglasses::+1:

FYI, if you want to use line breaks after the operators and not use a hanging indent (differing from modern PEP 8 recommendations and standard black style, but still potentially justifiable stylistic choices) but still ensure everything lines up, why not just use parenthesis continuation to avoid the discouraged line continuation operator, like so?

while (candle_riddle_ans.lower() != 'candle' and
       candle_riddle_ans.lower() != 'a candle' and
       candle_riddle_ans.lower() != 'the candle' and
       candle_riddle_ans.lower() != 'candles'):
2 Likes

I mostly mentioned it for completeness to provide the OP with all options.

To your question: Since the continuation character is a linear connector, it does flow well when reading linear code constructions.

However, the \ is a bit abrupt visually and perhaps would be better replaced by something smoother like ’ ~ ', which is a continuance symbol in regular grammatical usage (at least in English). My guess is that Guido/Barry/Nick (PEP 8 authors) dislike the backslash because of its rough visual effect; perhaps the smoother symbol would be more acceptable. A new symbol would also address the ambiguity of backslash in strings as an escape character. On the other hand, the abrupt slash visual helps the reader to notice the continuation. Conversely, the ’ ~ ’ would be very easy to highlight in an IDE for good visibility (distinguishing between ’ \ ’ used as continuance versus its other contexts and uses is not as straightforward).

Brackets also require the reader to visually jump over the bracketed content to find the end and possibly do some mental parenthesis matching if the content contains parentheses–as it very often does [1]. One can make the brackets easier to find by placing the bracketed content on separate lines from the brackets, as in your post above, but this adds lines. In general, I find that line breaks via brackets degrades readability when applied to an inherently linear context. This is a style choice, obviously.

PEP 3125 was proposed in 2007 to remove the line continuation symbol but was rejected for lack of support. Since line continuation method is a style choice, removing ’ \ ’ for line continuation might not have any more support today than it did back then. As you mentioned, PEP 8 does say…

Long lines can be broken over multiple lines by wrapping expressions in parentheses. These should be used in preference to using a backslash for line continuation.

We’ve probably covered line continuation here within the context of the OP and are at risk of going off topic. I’d be happy to continue the discussion in a new thread with the topic ‘Line Continuation’ (or something like that). I did not @mention Guido, Barry, and Nick above because that would definitely throw the thread off topic if they jumped into the discussion here.

[1] VS Code v1.60 introduced colors to perform bracket matching as a core function rather than an extension. Thank you VS Code Team!! You do have to enable it, though.