Procedural Programming
Overview
Teaching: 40 min
Exercises: 30 minQuestions
What is the Procedural paradigm?
How do I define functions in Python?
When should I use the Procedural paradigm?
Objectives
Use functions to abstract details
Programming Paradigms
See topic slides for general introduction to paradigms.
The Procedural Paradigm
So far we’ve been writing our code as one continuous piece. If we want to reuse some of our code, we can use loops to repeat some task, but sometimes we need more flexibility than this.
It would also be useful to be able to hide some of the complexity of our code once it’s grown to the point where it no longer fits on a single screen.
Procedural Programming is based around the idea that code should be structured into a set of procedures. Each procedure (optionally) takes some input, performs some computation and (optionally) returns some output. A program can then use these procedures to perform computation, without having to be concerned with exactly how the computation is performed.
You may wish to think of the Procedural Paradigm as focussing on the verbs of a computation.
Using Functions
In most modern programming languages these procedures are called functions. Python has many pre-defined functions built in. We’ve already met some of them.
To use, or call, a function we use the name of the function, followed by brackets containing any parameters that the function will accept. All functions in Python return a single value as their result.
Return Values
Though all functions return a single value in Python, this value may itself be:
- a collection of values
None
- a special value that is interpreted as though nothing has been returned
print(len('Python'))
6
Some functions are a little different in that they belong to an object, so must be accessed through the object using the dot operator. These are called methods or member functions. We’ve already seen some of these as well, but we’ll see more when we get to the Object Oriented Paradigm later.
nums = [1, 2, 3]
nums.append(4)
print(nums)
[1, 2, 3, 4]
Creating Functions
def add_one(value):
return value + 1
print(add_one(1))
2
def say_hello(name):
return 'Hello, ' + name + '!'
print(say_hello('World'))
Hello, World!
Functions may have default values for parameters. When we call a function, parameters with default values can be used in one of three ways:
- We can use the default value, by not providing our own value
- We can provide our own value in the way we have previously
- We can provide a value in the form of a named argument - arguments which are not named are called positional arguments
def say_hello(name='World'):
return 'Hello, ' + name + '!'
print(say_hello())
print(say_hello('Python'))
print(say_hello(name='Named Argument'))
Hello, World!
Hello, Python!
Hello, Named Argument!
Combining Strings
“Adding” two strings produces their concatenation:
'a'
+'b'
is'ab'
. Write a short function calledfence
that takes two parameters called original and wrapper and returns a new string that has the wrapper character at the beginning and end of the original. A call to your function should look like this:print(fence('name', '*'))
*name*
Solution
def fence(original, wrapper): return wrapper + original + wrapper
Custom Greetings
Create a new version of the
say_hello
function which has two parameters,greeting
andname
, both with default values. How many different ways can you call this function?Solution
def say_hello(greeting='Hello', name='World'): return greeting + ', ' + name + '!' # No arguments - both default values print(say_hello()) # One positional argument, one default value print(say_hello('Hello')) # One named argument print(say_hello(greeting='Hello')) print(say_hello(name='World')) # Both positional arguments print(say_hello('Hello', 'World')) # One positional argument, then one named argument print(say_hello('Hello', name='World')) # Both named arguments print(say_hello(greeting='Hello', name='World'))
You should have found that Python will not let you have positional arguments after named ones.
How do function parameters work?
It’s important to note that even though variables defined inside a function may use the same name as variables defined outside, they don’t refer to the same thing. This is because of variable scoping.
Within a function, any variables that are created (such as parameters or other variables), only exist within the scope of the function.
For example, what would be the output from the following:
f = 0 k = 0 def multiply_by_10(f): k = f * 10 return k multiply_by_10(2) multiply_by_10(8) print(k)
- 20
- 80
- 0
Solution
3 - the f and k variables defined and used within the function do not interfere with those defined outside of the function.
This is really useful, since it means we don’t have to worry about conflicts with variable names that are defined outside of our function that may cause it to behave incorrectly. This is known as variable scoping.
Function Composition
One of the main reasons for defining a function is to encapsulate our code, so that we can use it without having to worry about how the computation is performed. This means we’re free to use any way we want, including deferring some part of the task to another function that already exists.
def fahr_to_cels(fahr):
# Convert temperature in Fahrenheit to Celsius
cels = (fahr + 32) * (5 / 9)
return cels
def fahr_to_kelv(fahr):
# Convert temperature in Fahrenheit to Kelvin
cels = (fahr + 32) * (5 / 9)
kelv = cels + 273.15
return kelv
print(fahr_to_kelv(32))
print(fahr_to_kelv(212))
def fahr_to_cels(fahr):
# Convert temperature in Fahrenheit to Celsius
cels = (fahr + 32) * (5 / 9)
return cels
def fahr_to_kelv(fahr):
# Convert temperature in Fahrenheit to Kelvin
cels = fahr_to_cels(fahr)
kelv = cels + 273.15
return kelv
print(fahr_to_kelv(32))
print(fahr_to_kelv(212))
def fahr_to_cels(fahr):
# Convert temperature in Fahrenheit to Celsius
return (fahr + 32) * (5 / 9)
def fahr_to_kelv(fahr):
# Convert temperature in Fahrenheit to Kelvin
return fahr_to_cels(fahr) + 273.15
print(fahr_to_kelv(32))
print(fahr_to_kelv(212))
Managing Academics
As a common example to illustrate each of the paradigms, we’ll write some code to help manage a group of academics.
First, let’s create a data structure to keep track of the papers that a group of academics are publishing.
academics = [
{
'name': 'Alice',
'papers': [
{
'title': 'My science paper',
'day': 0
},
{
'title': 'My other science paper',
'day': 5
}
]
},
{
'name': 'Bob',
'papers': [
{
'title': 'Bob writes about science',
'day': 3
]
}
]
We want a convenient way to add new papers to the data structure.
def write_paper(academics, name, title, day):
paper = {
'title': title,
'day': day
}
for academic in academics:
if academic['name'] == name:
academic['name']['papers'].append(paper)
break
What happens if we call this function for an academic who doesn’t exist?
Exceptions
In many programming languages, we use exceptions to indicate that exceptional behaviour has occured and the flow of execution should be diverted.
Exceptions are often raised (thrown in some other programming languages) as the result of an error condition. The flow of execution is then returned (the exception is caught or handled) to a point where the error may be corrected or logged.
In Python, exceptions may also be used to alter the flow of execution even when an error has not occured. For example, when iterating over a collection, a
StopIteration
exception is used to tell the loop construct to terminate.def write_paper(academics, name, title, day): if name not in academics: raise KeyError('Named academic does not exist') paper = { 'title': title, 'day': day } for academic in academics: if academic['name'] == name: academic['name']['papers'].append(paper) break
Or
def write_paper(academics, name, title, day): paper = { 'title': title, 'day': day } for academic in academics: if academic['name'] == name: academic['name']['papers'].append(paper) break else: raise KeyError('Named academic does not exist')
Passing Lists to Functions
We have seen previously that functions are not able to change the value of a variable which is used as their argument.
def append_to_list(l): l.append('appended') l = [1, 2, 3] l.append('again') return l a_list = ['this', 'is', 'a', 'list'] print(append_to_list(a_list)) print(a_list)
Before running this code, think about what you expect the output to be. Now run the code, does it behave as you expected? Why does the function behave in this way?
Solution
[1, 2, 3, 'again'] ['this', 'is', 'a', 'list', 'appended']
The reason for this behaviour is that lists are mutable so when we pass one in to a function any modifications are made to the actual list as it exist in memory. Using
=
to assign a new value creates a new list in memory and assigns it to the variablel
. Any changes made tol
after this are changes to the new list, so do not affect the previous list.
FIXME - add JSON as a callout - content in RSD course
def count_papers(academics):
count = 0
for academic in academics:
count = count + len(academic['papers'])
return count
total = count_papers(academics)
print(total)
3
def list_papers(academics):
papers = []
for academic in academics:
papers = papers + academic['papers']
return papers
Key Points
Functions allow us to separate out blocks of code which perform a common task
Functions have their own scope and do not clash with variables defined outside