string replacement with random element in Python

I am trying to make a fun, randomized madlibs story for my colleagues. The idea is to have them submit adjectives, nouns, and verbs, which I will add to a separate lists. I have also added markers (e.g. "(noun)") to the story as replaceables. I am trying to figure out how to replace each marker in the string with a random, UNIQUE choice from the appropriate list of nouns, adjectives, or verbs.

My first idea has a couple of bugs:

import random

nouns = ["bee","hammer","person"]
adj = ["dazzling","punctual","dizzy","petite"]
verbs = ["sprint","blab","sniff","die"]

story = "blah blah blah (noun). Blah blah blah (adj), blah blah (verb) blah blah. Blah blah blah (noun) blah blah (noun)s blah blah (verb)."

spl_story = story.split()

for i in spl_story:
    if i == "(noun)":
        i = random.choice(nouns)
#same if statement for adjectives and verbs

new_story = " ".join(spl_story)

There are 2 main issues with this:

  1. if there is a "s" for a plural value (e.g. "(noun)s") or a special character after the marker, it doesn't split properly. I want to find a way to keep my punctuation and the s character, but make sure that the markers split properly.

I could do a long string of conditionals, such as:

    if i == "(noun)"
        i = random.choice(nouns)
    elif i == "(noun)s"
        i = random.choice(nouns) + "s"
    elif i == "(noun).":
        i = random.choice(nouns) + "."

but that's inefficient and I'd like to find a smoother solution.

Secondly: I need a UNIQUE choice from the list without replacing repetative nouns, verbs, etc.

My solution might suck from the get-go and if anyone thinks it would be good to throw it away and go a different path, I'm all ears. Thanks for any advice!


Solution 1:

I would structure the input a little differently. Give each replacement variable a separate name and then use the string format syntax to reference them. You can also have a dictionary which says how many of each noun, adj, verb, etc is required.

story = (
    "blah blah blah {noun0}. "
    "Blah blah blah {adj0}, "
    "blah blah {verb0} blah blah. "
    "Blah blah blah {noun1} "
    "blah blah {noun2}s "
    "blah blah {verb1}."
)
required = {
    "noun": 3,
    "adj": 1,
    "verb": 2
}

If you structure your random options in the same way as your requirements file, then you can easy construct an input for str.format

options = {
    "noun": ["bee", "hammer", "person"],
    "adj": ["dazzling", "punctual", "dizzy", "petite"],
    "verb": ["sprint", "blab", "sniff", "die"]
}

choices = {}
for kind in required:
    for i in range(required[kind]):
        choices[f"{kind}{i}"] = random.choice(options[kind])

Then print the formatted string!

print(story.format(**choices))
blah blah blah person. Blah blah blah dizzy, blah blah sprint blah blah. Blah blah blah hammer blah blah hammers blah blah blab.

Solution 2:

Personally I'd use regex (with say re.search @ https://docs.python.org/3.9/library/re.html#re.search) to find the substrings. For nouns for example, the regexes \(noun\) and \(noun\)s should work. re.searchs output contains (among other things) the position of the match in the string, so you can iteratively find and replace until you have replaced all the nouns. You can then do the same for the adjectives and verbs.

As for finding unique random elements, I can think of two approaches:

  1. Use random.choice on the list and then remove the returned element from the list so that it can't be selected again. You can make a copy of the list if you don't want to modify it in-place.
  2. Use random.shuffle on the list and then keeping poping to get elements. These elements will be random with respect to the original list. Note that this will modify the list in-place.