XClose

COMP0233: Research Software Engineering With Python

Home
Menu

Functions

Definition

We use def to define a function, and return to pass back a value:

In [1]:
def double(x):
    return x * 2
In [2]:
double(5)
Out[2]:
10
In [3]:
double([5])
Out[3]:
[5, 5]
In [4]:
double("five")
Out[4]:
'fivefive'

Default Parameters

We can specify default values for parameters:

In [5]:
def jeeves(name="Sir"):
    return f"Very good, {name}"
In [6]:
jeeves()
Out[6]:
'Very good, Sir'
In [7]:
jeeves("James")
Out[7]:
'Very good, James'

If you have some parameters with defaults, and some without, those with defaults must go later.

If you have multiple default arguments, you can specify neither, one or both:

In [8]:
def jeeves(greeting="Very good", name="Sir"):
    return f"{greeting}, {name}"
In [9]:
jeeves()
Out[9]:
'Very good, Sir'
In [10]:
jeeves("Hello")
Out[10]:
'Hello, Sir'
In [11]:
jeeves(name = "John")
Out[11]:
'Very good, John'
In [12]:
jeeves(greeting="Suits you")
Out[12]:
'Suits you, Sir'
In [13]:
jeeves("Hello", "Producer")
Out[13]:
'Hello, Producer'

Side effects

Functions can do things to change their mutable arguments, so return is optional.

This is pretty awful style, in general, functions should normally be side-effect free.

Here is a contrived example of a function that makes plausible use of a side-effect.

Note that the function below uses [:]. This is used to update the contents of the list, and though this function is not returning anything, it's changing the elements of the list.

In [14]:
def double_inplace(vec):
    vec[:] = [element * 2 for element in vec]


z = [0, 1, 2, 3]  #  This could be simplified using list(range(4))
double_inplace(z)
print(z)
[0, 2, 4, 6]

In this example, we're using [:] to access into the same list, and write its data. Whereas, if we do

vec = [element * 2 for element in vec]

would just move a local label, not change the input - i.e., a new container is created and the label vec is moved from the old one to the new one.

A more common case would be to this as a function which returned the output:

In [15]:
def double(vec):
    return [element * 2 for element in vec]

Let's remind ourselves of this behaviour with a simple array:

In [16]:
x = 5
x = 7
x = ["a", "b", "c"]
y = x
In [17]:
x
Out[17]:
['a', 'b', 'c']
In [18]:
x[:] = ["Hooray!", "Yippee"]
In [19]:
y
Out[19]:
['Hooray!', 'Yippee']

Early Return

Having multiple return statements is a common practice in programming. These return statements can be placed far from each other, allowing a function to return early if a specific condition is met.

For example, a function isbigger could be written as:

def isbigger(x, limit=20):
    return x > limit

However, what if you want to print a message on the screen when a smaller value has been found? That's what we do below, where the function below returns early if a number greater than given limit.

In [20]:
def isbigger(x, limit=20):
    if x > limit:
        return True
    print("Value is smaller")
    return False


isbigger(25, 15)
Out[20]:
True
In [21]:
isbigger(40, 15)
Out[21]:
True

The dynamic typing of Python also makes it easy to return different types of values based on different conditions, but such code is not considered a good practice. It is also a good practice to have a default return value in the function if it is returning something in the first place. For instance, the function below could use an elif or an else condition for the second return statement, but that would not be a good practice. In those cases, Python would be using the implicit return statement. For example, what's returned in the following example when the argument x is equal to the limit?

In [22]:
def isbigger(x, limit=20):
    if x > limit:
        return True
    elif x < limit:
        print("Value is smaller")
        return False
In [23]:
# Write your own code to find out

Return without arguments can be used to exit early from a function

Here's a slightly more plausibly useful function-with-side-effects to extend a list with a specified padding datum.

In [24]:
def extend(to, vec, pad):
    if len(vec) >= to:
        return  # Exit early, list is already long enough.
    vec[:] = vec + [pad] * (to - len(vec))
In [25]:
x = [1, 2, 3]
extend(6, x, "a")
print(x)
[1, 2, 3, 'a', 'a', 'a']
In [26]:
z = list(range(9))
extend(6, z, "a")
print(z)
[0, 1, 2, 3, 4, 5, 6, 7, 8]

Unpacking arguments

If a vector is supplied to a function with a *, its elements are used to fill each of a function's arguments.

In [27]:
def arrow(before, after):
    return f"{before} -> {after}"


print(arrow(1, 3))
1 -> 3
In [28]:
x = [1, -1]

print(arrow(*x))
1 -> -1

This can be quite powerful:

In [29]:
charges = {"neutron": 0, "proton": 1, "electron": -1}
In [30]:
charges.items()
Out[30]:
dict_items([('neutron', 0), ('proton', 1), ('electron', -1)])
In [31]:
for particle in charges.items():
    print(arrow(*particle))
neutron -> 0
proton -> 1
electron -> -1

Sequence Arguments

Similiarly, if a * is used in the definition of a function, multiple arguments are absorbed into a list inside the function:

In [32]:
def doubler(*sequence):
    return [x * 2 for x in sequence]


print(doubler(1, 2, 3, "four"))
[2, 4, 6, 'fourfour']

Keyword Arguments

If two asterisks are used, named arguments are supplied as a dictionary:

In [33]:
def arrowify(**args):
    for key, value in args.items():
        print(f"{key} -> {value}")


arrowify(neutron="n", proton="p", electron="e")
neutron -> n
proton -> p
electron -> e

These different approaches can be mixed:

In [34]:
def somefunc(a, b, *args, **kwargs):
    print("A:", a)
    print("B:", b)
    print("args:", args)
    print("keyword args", kwargs)

somefunc(1, 2, 3, 4, 5, fish="Haddock")
A: 1
B: 2
args: (3, 4, 5)
keyword args {'fish': 'Haddock'}