-
Notifications
You must be signed in to change notification settings - Fork 28
Programming Patterns
This page contains standard 'patterns' that you should get used to. When you need to perform one of these tasks, follow the standard pattern.
For the most part, this guide is not language-specific, so many patterns are presented as pseudocode.
Python 3 is used where actual code is provided... and Python is similar enough to pseudocode that this is usually suitable as a pattern.
For most programs, you will have a main function and a number of other functions.
Think of main as the whole program with the other functions as the tools that main uses, with the details abstracted away.
main should go at the top of your file, and someone reading your code for the first time should be able to look at main and understand what the program does... that is, main should "look like" the whole program. Example structure:
""" module-level docstring """
import statements
constants
function main()
opening statement
do_step1()
result = do_step2()
do_step2(result)
closing statement
function step1()
...
function step2(parameter)
...
return result
main()
When you need to make a decision in your program, you would usually use one of the following patterns.
(See loops below for when you need to repeatedly make decisions, e.g. for most error-checking.)
The examples below will use situations where you want to print a result for a given score, where score is an integer. Each situation could be stand-alone, or part of a loop, like for score in scores:
Use this if you want to do something when the condition is true, but do nothing when it's false.
In this example, we don't want to print anything for the non-exceptional scores, so there's no else.
if score > 90:
print("That's exceptional!")Some beginning programmers use else for no reason, like the following example... This is redundant and never of any value... Don't do it.
if score > 90:
print("That's exceptional!")
else:
passUse this if you want to do something when the condition is true, and something different when it's false.
In this example, we want to print a result for the score no matter what its value is.
Note that we do not need a second condition to handle the "fail" case, because if the score is not >= 50 we already know that it must be < 50.
if score >= 50:
print("Pass")
else:
print("Fail")Use this if you want to handle all cases in some way - in our example, there will be an output printed for every possible score. This is the pattern that we use for menus as well - handle each menu option we know about and the trailing else handles the invalid option (see below).
In this example, we always want to print one result for the score no matter what its value is.
if score >= 90:
print("Excellent")
elif score >= 50:
print("Passable")
else:
print("Bad")Similar to the if with no else, use this when you want to handle multiple possible results, but there will be some cases where there is no result handled. The results are mutually exclusive, but you're happy to do nothing in some cases. If you use this pattern, ask yourself, "what cases/inputs do I NOT want to handle?"
In this example, the very high scores win a prize, but the others don't, and we don't need to tell them. (E.g. at graduation, they announce which graduates got a University medal, but they don't say which ones did not get a medal.)
if score >= 90:
print("You win a car!")
elif score >= 80:
print("You win a horse :)") # but you do not win a car AND a horseUse this when you want multiple outputs/results for a single value. That is, the results are not mutually exclusive. One condition being true does not affect the other conditions. In this example, we want to print all the results that a score could achieve.
if score > 50:
print("You passed")
if score >= 90:
print("You win a car!")
if score >= 80:
print("You win a horse :)") # here, you can win BOTH a car and a horseSo, as you design your selections, recognise what each pattern is for and how it applies to your situation.
E.g. You would not use the "if, if, if" pattern for determining a grade (N, C, HD...) from a percentage, because you know that is inefficient since those grades are mutually exclusive - as soon as we know what grade it is, we don't need to ask any more.
In most languages, there are multiple kinds of loops and you should choose the most appropriate kind.
The most common choice is:
- Use for loops for definite iteration, like
for item in sequence... - Use while loops for indefinite iteration, like
while condition...
Using a while loop and maintaining your own counter (e.g. using a while loop to iterate through the numbers from 1 to 10) would be considered an anti-pattern, since this is what for loops are for!
Using a for loop and maintaining your own counter (e.g. iterating through elements in a list and manually using +1 for the index) would also be poor, since for loops can do this for you.
In Python, if you need both the index and the element, use the function enumerate, e.g.
names = ["Barry", "Tux", "Ada", "Maggie"]
for i, name in enumerate(names):
print(i, " - ", name)For loops are mostly used when you want to do something with each item in a sequence.
In Python, if you want a sequence of numbers, this can be generated with range.
One tip for variable naming... you will very often end up with loops of the form:
for singular in plural:
...
Example, for dog in dogs, for number in numbers, for book in books...
If you find yourself writing something that doesn't match this, you might be in trouble.
Example:
names = ["Barry", "Tux", "Ada", "Maggie"]
for i in names:
print(i, " - ") # WAIT, what's i? A name?!?Almost all while loops follow the same standard pattern (as below with menus and error checking).
Do not force the loop to be True the first time by setting a value for your loop condition variable, and do not use while True... unless this is really the best way to do it.
<priming read - do something the loop will depend on, e.g. get/calculate a number>
while <condition based on something from above>:
<body of the loop - do the thing you want to repeat>
<same as the priming read again>
<do next thing now that the loop is finished (condition was false)>
Example - number guessing game
SECRET = 6
guess = int(input("? "))
while guess != SECRET:
print("Guess again!")
guess = int(input("? "))
print("You got it!")Use the if/elif.../else pattern in Python (switch statements in other languages) inside a while loop that handles the quit option.
display menu
get choice
while choice != <quit option>
if choice == <first option>
<do first task>
else if choice == <second option>
<do second task>
...
else if choice == <n-th option>
<do n-th task>
else
display invalid input error message
display menu
get choice
<do final thing, if needed>
<priming read - do something the loop depends on>
while <condition based on something from above>:
display error message
<same as the priming read again>
do next thing now that you know the 'something' is valid
Example:
age = int(input("Age: "))
while age < 0:
print("Invalid age!")
age = int(input("Age: "))
print("You are {} years old".format(age))You can't have a 'normal' priming read since it might crash before you get to the condition, so you need your try/except inside a loop that you control.
Example:
is_valid_input = False
while not is_valid_input:
try:
age = int(input("Age: "))
if age < 0:
print("Age must be >= 0")
else:
is_valid_input = True
except ValueError:
print("Invalid (not an integer)")
print("Next year you will be", age + 1)Suppose you have a function that should do a task provided there are no errors, like adding a value to a collection if it's valid.
In this case, you can check for errors first, then do the task if there are no errors... instead of checking it's valid and doing the task if it's valid.
The difference is in the nesting level. You want your main task to be at the highest level, not inside an if/else.
Example structure:
function do_task(input)
if input has error 1
display error message 1
return
if input has error 2
display error message 2
return
do task (knowing we don't have any of the above errors)
function find(needle)
for each item in items
if item == needle (or however we compare to find the needle in the haystack)
return result, or set value and break
return None (since we did not find it)
(note no need for else or continue, it will move to the next item)
filtered_items = new list
for each item in items
if item matches what we want
add item to filtered_items
(note no need for else or continue, it will move to the next item)
Note that Python (and many languages) have neat shortcuts for filtering, e.g. using list comprehensions:
filtered_items = [item for item in items if item matches what we want]
In most cases, where you are dealing with a condition or value and you care about whether it is true or false, then you never need to compare to True or False. E.g. instead of:
if condition == True:
... or
if condition == False:
You can just use:
if condition:
... or
if not condition:
If you are ever returning (or setting a variable to) True/False depending on a condition, you can just return (or set) to the condition. So, instead of:
if condition:
return True
else:
return False
You can just use:
return condition
In general, there are 3 kinds of functions, those that are designed to:
- get input (from the user or another source)
- process data
- produce output (to the console, file, or another sink)
Very commonly, the structure in terms of parameters and return statements will look something like (example):
function main():
data = get_input()
result = process(data)
display(result)
(This is a simplification to make the point about reusability, not a rule that never changes.)
That is:
- input-getting functions don't take in parameters, but they do return what they get.
- data-processing functions do take in parameters (they do NOT get the input data from the user or other source), and they do return the results (they do NOT display/print/save the result)
- output-producing functions do take in parameter (what they are to display), but do not return anything
Some good ways to understand function design include asking these questions about function reuse:
- What if we wanted to rewrite the program's interface in French or Farsi? We should not have to change the processing function, because it should not do any user interface things (input or output on the screen).
- What if we wanted to get our input from a file instead of the user? We should not have to change the processing function because it should not get any user input. A well-designed function can be used with input either from the user or a file (or anywhere), because it takes in the input as parameters.
- Same as above for if we wanted to write our output to a file instead of display it on the screen... the processing function shouldn't care where the data (input parameters) comes from, or where the results (return values) go, since that's not it's job.
- Functions designed like this are more testable. You can write test code that passes in inputs and compares outputs (returned values) to known correct results for those inputs (e.g. using the
assertstatement, ordoctestmodule). You really can't easily "test" functions that get user input and print results in any automated way.
Always store data in the best, most correct, format. E.g. if you read a price from a file, it will be a string, but you should store it as a float. If you want to print it using string formatting (e.g. $23.40), don't store it as a string, just print it... leaving the variable as a float.
In general, don't store derivable data. E.g. don't store age if you already have a date of birth (DOB).
Doing so can lead to inconsistency, e.g. your age doesn't get updated when "date - DOB" results in a different age. Just calculate the age when you need it and it can't be wrong.
This is a maintenance burden. Even if your code works correctly, when you maintain it and add to it, you have to remember to update the same information in multiple places.
Here are a few things you should never do... You can consider these to be "anti-patterns".
("Never" is a strong word, and there will likely be some rare situations where you might maybe sometimes want to do these things, but it's very unlikely.)
- Never replace function parameters: If you have a function that takes in a parameter (x), you will never want to set that variable (x) immediately... otherwise, why would you pass it in?
- Never convert to the same type: don't convert from type A to type A. E.g. in Python, the
inputfunction always returns a str type, so you never need to write something likex = str(input("?"))... ory = int(0). - Never use the verbose (unbound) syntax for method calls unless you need it: You should always prefer the concise (bound) format. E.g. use
"Hello".upper()notstr.upper("Hello"). - Never use
while Trueloops if you can easily enough use a "standard" while loop. If you have to write an if statement to break out of a loop, that if-condition should probably just be your normal loop condition.