It is now time to organize/refactor our code! When you begin, this is a strange process since the final code does the same as before. However, refactoring is the only way to create a code easy to maintain and expand.
This post is part of the Discover Python and Patterns series
Define a function
In the previous programs, we used existing functions like print() or input(). We can also create our own functions using the def statement:
def printHello():
print("Hello!")
The syntax is:
def <function name>(<arguments>):
<function body>
We can choose a function name as for variable names, as long as the name is not already used. The arguments are the parameters the function can use; in the printHello() example there are no arguments. The function body is a usual Python code block defined using indentation.
Call a function
To call a function, use its name and parentheses, as we did before with functions like print():
printHello()
If you run this program (with the function definition before!), it executes the code inside the function body, and prints the message.
Organize/refactor the game
In the previous posts, we created the small game “guess the number”. I propose now to refactor this game using functions to organize it better.
Note that in software design, people talk about “code refactoring” to name the process of restructuring an existing code. The aim is to improve the code presentation but not its behavior. It is hard to prove in a few words why refactoring is essential in software design. It is the sum of thousands of little reasons that motivate it, each one of these being the result of personal experiences. So, if you don’t understand why, please trust the many programmers that bled on unstructured code and hear them telling you how much this process matters!
A first refactoring we can do is embed the game into a single function:
def runGame():
magicNumber = random.randint(1,10)
guessCount = 0
while True:
word = input("What is the magic number? ")
if word == "quit":
break
try:
playerNumber = int(word)
except ValueError:
print("Please type a number without decimals!")
continue
guessCount += 1
if playerNumber == magicNumber:
print("This is correct! You win!")
print("You guessed the magic number in {} guesses.".format(guessCount))
break
elif magicNumber < playerNumber:
print("The magic number is lower")
elif magicNumber > playerNumber:
print("The magic number is higher")
print("This is not the magic number. Try again!")
Thanks to this refactoring, we can run the game calling the runGame() function. That means that if we want to create a menu to start new games, the code is easier to read. For instance:
import random
def runGame():
... the game code ...
while True:
print("Game menu")
print("1) Start a new game")
print("2) Quit")
choice = input("Enter your choice:")
if choice == "1":
runGame()
elif choice == "2":
break
The game menu is very easy to understand, even for a developer that never saw it: the choice “1” runs the game, and the choice “2” leaves it. Note that I choose function names that describe as much as possible the task. For instance, for the runGame() we can’t be more explicit: we ask this function to run the game!
More refactoring
I propose now to continue the refactoring with the creation of a function askPlayer() for the management of the player input:
def askPlayer():
while True:
word = input("What is the magic number? ")
if word == "quit":
return None
try:
playerNumber = int(word)
break
except ValueError:
print("Please type a number without decimals!")
continue
return playerNumber
The main purpose of this function is to get a number from the player, and handles cases like bad numbers and quit the game. I copied and pasted all the code related to this task, put it in a while loop. We repeat the input until the player enters a number or “quit”.
The return statement
At the end of this function, there is a return statement. It returns the value of the playerNumber variable. We need to use this statement because Python defines variables inside function blocks. For instance, playerNumber in askPlayer() only exists in this function. If you try to access it in another function, it does not work, even if you create a variable with the same name.
For instance, if you update the runGame() function to use the askPlayer() function in the following way:
def runGame():
magicNumber = random.randint(1,10)
guessCount = 0
while True:
askPlayer()
if playerNumber is None:
break
...
Spyder tells you that playerNumber is undefined. If you run the program, Python raises an exception.
The right way to use the function is to copy the value returned by askPlayer() into a variable of runGame():
def runGame():
magicNumber = random.randint(1,10)
guessCount = 0
while True:
playerNumber = askPlayer()
if playerNumber is None:
break
...
With this code, the value of playerNumber of askPlayer() is copied into the value of playerNumber of runGame().
In the askPlayer() function, if the player enters “quit”, the function returns None. First, note that the return statement stops the function. No more code lines of the function are executed after this statement. About None, it is a special Python symbol that means “nothing” or “empty”. I use it rather than “quit” because I want to be able to change the way the player can stop the game without having to change the way askPlayer() works.
Function documentation
The creation of askPlayer() makes the code easier to read, but to even more ease its use, I highly recommend to add some documentation using Python docstrings. Documentation saves you a lot of time in large projects, where it is hard to remember the inputs and outputs of all functions your team created:
def askPlayer():
"""
Ask player to enter a number
Output:
* playerNumber: the number entered by the player, or None if the player wants to stop the game
"""
while True:
...
Python docstrings is a large comment starting right after the def statement. It is very close to the markdown language, you can use it to layout your documentation.
The most important things are:
- A general description at the beginning. Here it is “Ask player to enter a number”
- Description of each input as a list. In this example, there is no input.
- Description of each output as a list. In this example, there is a single output playerNumber.
In Spyder, you can see the documentation of a function (if any). Put the cursor on a function name, and then hit Ctrl+I. In the top right part of the interface, the documentation appears (if it does not, select the “Help” tab):
Final code
# Import the random package
import random
def askPlayer():
"""
Ask player to enter a number
Output:
* playerNumber: the number entered by the player, or None if the player wants to stop the game
"""
while True:
# Player input
word = input("What is the magic number? ")
# Quit if the player types "quit"
if word == "quit":
return None
# Int casting with exception handling
try:
playerNumber = int(word)
break
except ValueError:
print("Please type a number without decimals!")
continue
return playerNumber
def runGame():
# Generate a random Magic number
magicNumber = random.randint(1,10)
# An integer to count the guesses
guessCount = 0
while True:
playerNumber = askPlayer()
if playerNumber is None:
break
# Increase the number of guesses
guessCount += 1
# Cases
if playerNumber == magicNumber:
print("This is correct! You win!")
print("You guessed the magic number in {} guesses.".format(guessCount))
break
elif magicNumber < playerNumber:
print("The magic number is lower")
elif magicNumber > playerNumber:
print("The magic number is higher")
# Wrong number, continue
print("This is not the magic number. Try again!")
runGame()
I started the refactoring in this post, but we can do much more, especially using the Game Loop pattern I present in the next post.