Skip to main content

Software Architecture and Design

Procedural Program...

Containers [python]

Software Architecture and Design

Procedural Program...

Arrays [python]

This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training.

This material has been adapted from the "Software Engineering" module of the SABS R³ Center for Doctoral Training.

Creative Commons License
This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1

This course material was developed as part of UNIVERSE-HPC, which is funded through the SPF ExCALIBUR programme under grant number EP/W035731/1

Creative Commons License

Functions

Using Functions

In most modern programming languages these procedures are called functions. Python has many pre-defined functions built in and 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 arguments that we wish to pass to the function. 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
char_count = len('Python') print(char_count)
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]
The append function is actually also one of these functions that return None. We can test this again by printing its output.
nums = [1, 2, 3] result = nums.append(4) print(result) print(nums)
None [1, 2, 3, 4]
It's relatively common for a function to return None if the purpose of the function is to modify one of its input values. That's the case here - the purpose of the append function is to append a value to an existing list.

Creating Functions

Although Python has many built in functions, it wouldn't be much use if we couldn't also define our own. Most languages use a keyword to signify a function definition, in Python that keyword is def.
def add_one(value): return value + 1 two = add_one(1) print(two)
2
def say_hello(name): return 'Hello, ' + name + '!' print(say_hello('World'))
Hello, World!
To define a function, we use def, followed by the name of the function and its parameters in brackets. Just like with other code blocks (like for and if), we use a colon to signify the body of the function and indent the body by four spaces.
Note that we used the word argument when we were calling a function, but parameter when we were defining one. The parameters of a function are the names of the variables which are created inside the function to accept its input data. The arguments of a function are the values that we give to a function when we call it, to put into its parameters.
Sometimes, it's useful for a parameter to have a default value. When we call a function, parameters with default values can be used in one of three ways:
  1. We can use the default value, by not providing our own value
  2. We can provide our own value in the normal way
  3. 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!

Declarations and Definitions

Some languages have a distinction between declaration and definition (or implementation) of a function. In these languages function declaration provides a name and information about its return type and parameters, but does not provide the actual code inside the function. Function definition is when the code is provided, and the function's behaviour is defined.
This distinction can be useful as it allows us to call a function from code which appears above the function definition in the source file. Python does not have this distinction - that is, a function is always declared and defined at the same time - so we must define the function before we can use it.
One common language that does have this distinction is C++. See this page for more information.

Combining Strings

"Adding" two strings produces their concatenation: 'a' + 'b' is 'ab'. Write a short function called fence 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*

Custom Greetings

Create a new version of the say_hello function which has two parameters, greeting and name, both with default values. How many different ways can you call this function using combinations of named and positional arguments?

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)
  1. 20
  2. 80
  3. 0

Function Composition

One of the main reasons for defining a function is to encapsulate our code, so that it can be used without having to worry about exactly how the computation is performed. This means we're free to implement the function however we want, including deferring some part of the task to another function that already exists.
For example, if some data processing code we're working on needs to be able to accept temperatures in Fahrenheit, we might need a way to convert these into Kelvin. So we could write these two temperature conversion functions:
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))
But if we look at these two functions, we notice that the conversion from Fahrenheit to Celsius is actually duplicated in both functions. This makes sense, since this is a necessary step in both functions, but duplicated code is wasteful and increases the chance of us making an error - what if we made a typo in one of the equations?
So, we can remove the duplicated code, by calling one function from inside the other:
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))
Now we've removed the duplicated code, but we might actually want to go one step further and remove some of the other unnecessary bits:
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))
Now we have each function down to one statement, which should be easier to read and hopefully has reduced the chance of us making a mistake. Whether you actually prefer the second or third version is up to you, but we should at least try to reduce duplication where posssible.

Managing Academics

As a common example to illustrate each of the paradigms, we'll write some code to help manage a group of academics and their publications.
First, let's create a data structure to keep track of the papers that a group of academics are publishing. Note that we could use an actual date type to store the publication date, but they're much more complicated to work with, so we'll just use the year.
academics = [ { 'name': 'Alice', 'papers': [ { 'title': 'My science paper', 'date': 2015 }, { 'title': 'My other science paper', 'date': 2017 } ] }, { 'name': 'Bob', 'papers': [ { 'title': 'Bob writes about science', 'date': 2018 } ] } ]
We want a convenient way to add new papers to the data structure.
def write_paper(academics, name, title, date): paper = { 'title': title, 'date': date } for academic in academics: if academic['name'] == name: academic['papers'].append(paper) break
We're introducing a new keyword here, break, which exits from inside a loop. When the break keyword is encountered, execution jumps to the next line outside of the loop. If there isn't a next line, as in our example here, then it's the end of the current block of code.
This is useful when we have to search for something in a list - once we've found it we can stop searching and don't need to waste time looping over the remaining items.
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 (or 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. For the moment we'll just raise the exception, and assume that it will get handled properly by someone using our code.
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 the way in which Python tells a loop construct to terminate, though this is hidden from you.
def write_paper(academics, name, title, date): paper = { 'title': title, 'date': date } for academic in academics: if academic['name'] == name: academic['papers'].append(paper) break else: raise KeyError('Named academic does not exist')
{: .language-python}
The for-else structure used here is relatively unusual, but can be useful when you're using a loop to search for a value. The else block is executed if and only if the loop execution completes normally - i.e. when break is not used. When you're using a loop to search for something, this means that it has not been found.
For more information see this section of the Python documentation.

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?

Counting Publications

Write a function called count_papers, that when called with count_papers(academics) returns the total number of publications.

Listing Publications

Write a function called list_papers, that when called with list_papers(academics) returns a list of all publication titles.

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