In this version of the song “Ninety-Nine Bottles,” the program introduces small imperfections in each stanza by either removing a letter, swapping the casing of a letter, transposing two letters, or doubling a letter.
As the song continues to play, these mutations add up, resulting in a very silly song. It’s a good idea to try Project 50, “Ninety-Nine Bottles,” before attempting this one.
The Program in Action
When you run ninetyninebottles2.py, the output will look like this:
niNety-nniinE BoOttels, by Al Sweigart [email protected]--snip--
99 bottles of milk on the wall,
99 bottles of milk,
Take one down, pass it around,
98 bottles of milk on the wall!
98 bottles of milk on the wall,
98 bottles of milk,
Take one d wn, pass it around,
97 bottles of milk on the wall!
97 bottles of milk on the wall,
97 bottels of milk,
Take one d wn, pass it around,
96 bottles of milk on the wall!
75b otlte of mIl on teh wall,
75 ottels f miLk,
Take one d wn, pass it ar und,
74 bbOttles of milk on t e wall!
1 otlE t of iml oo nteh lall,
1 o Tle FF FmMLIIkk,
Taake on d wn, pAasSs itt au nn d,
No more bottles of milk on the wall!
How It Works
String values in Python are immutable, meaning they cannot be changed. If the string 'Hello' is stored in a variable called greeting, the code greeting = greeting + ' world!' doesn’t actually change the 'Hello' string. Rather, it creates a new string, 'Hello world!', to replace the 'Hello' string in greeting. The technical reasons for this are beyond the scope of this book, but it’s important to understand the distinction, because it means code like greeting = 'h' isn’t allowed, since strings are immutable. However, since lists are mutable, we can create a list of single-character strings (as line 62 does), change the characters in the list, and then create a string from the list (line 85). This is how our program seemingly changes, or mutates, the strings containing the song lyrics.
1. """niNety-nniinE BoOttels of Mlik On teh waLl
2. By Al Sweigart [email protected]
3. Print the full lyrics to one of the longest songs ever! The song
4. gets sillier and sillier with each verse. Press Ctrl-C to stop.
5. View this code at https://nostarch.com/big-book-small-python-projects
6. Tags: short, scrolling, word"""
8. import random, sys, time
10. # Set up the constants:
11. # (!) Try changing both of these to 0 to print all the lyrics at once.
12. SPEED = 0.01 # The pause in between printing letters.
13. LINE_PAUSE = 1.5 # The pause at the end of each line.
16. def slowPrint(text, pauseAmount=0.1):
17. """Slowly print out the characters in text one at a time."""
18. for character in text:
19. # Set flush=True here so the text is immediately printed:
20. print(character, flush=True, end='') # end='' means no newline.
21. time.sleep(pauseAmount) # Pause in between each character.
22. print() # Print a newline.
25. print('niNety-nniinE BoOttels, by Al Sweigart [email protected]')
27. print('(Press Ctrl-C to quit.)')
31. bottles = 99 # This is the starting number of bottles.
33. # This list holds the string used for the lyrics:
34. lines = [' bottles of milk on the wall,',
35. ' bottles of milk,',
36. 'Take one down, pass it around,',
37. ' bottles of milk on the wall!']
40. while bottles > 0: # Keep looping and display the lyrics.
41. slowPrint(str(bottles) + lines, SPEED)
43. slowPrint(str(bottles) + lines, SPEED)
45. slowPrint(lines, SPEED)
47. bottles = bottles - 1 # Decrease the number of bottles by one.
49. if bottles > 0: # Print the last line of the current stanza.
50. slowPrint(str(bottles) + lines, SPEED)
51. else: # Print the last line of the entire song.
52. slowPrint('No more bottles of milk on the wall!', SPEED)
55. print() # Print a newline.
57. # Choose a random line to make "sillier":
58. lineNum = random.randint(0, 3)
60. # Make a list from the line string so we can edit it. (Strings
61. # in Python are immutable.)
62. line = list(lines[lineNum])
64. effect = random.randint(0, 3)
65. if effect == 0: # Replace a character with a space.
66. charIndex = random.randint(0, len(line) - 1)
67. line[charIndex] = ' '
68. elif effect == 1: # Change the casing of a character.
69. charIndex = random.randint(0, len(line) - 1)
70. if line[charIndex].isupper():
71. line[charIndex] = line[charIndex].lower()
72. elif line[charIndex].islower():
73. line[charIndex] = line[charIndex].upper()
74. elif effect == 2: # Transpose two characters.
75. charIndex = random.randint(0, len(line) - 2)
76. firstChar = line[charIndex]
77. secondChar = line[charIndex + 1]
78. line[charIndex] = secondChar
79. line[charIndex + 1] = firstChar
80. elif effect == 3: # Double a character.
81. charIndex = random.randint(0, len(line) - 2)
82. line.insert(charIndex, line[charIndex])
84. # Convert the line list back to a string and put it in lines:
85. lines[lineNum] = ''.join(line)
86. except KeyboardInterrupt:
87. sys.exit() # When Ctrl-C is pressed, end the program.
After entering the source code and running it a few times, try making experimental changes to it. The comments marked with (!) have suggestions for small changes you can make. On your own, you can also try to figure out how to do the following:
Swap the order of two adjacent words, where a “word” is text separated by spaces.
On rare occasions, have the song start counting upward for a few iterations.
Change the case of an entire word.
Exploring the Program
Try to find the answers to the following questions. Experiment with some modifications to the code and rerun the program to see what effect the changes have.
What happens if you change bottles = bottles - 1 on line 47 to bottles = bottles - 2?
What happens if you change effect = random.randint(0, 3) on line 64 to effect = 0?
What error happens if you delete or comment out line = list(lines[lineNum]) on line 62?