## Functions

### Definition

We use def to define a function, and return to pass back a value: Let’s define a function double with parameter x

def double(x):
return x*2


We can now call the function:

double(5)


10

However, what happens if we pass a list

double([5])


[5, 5]

or a string?

double('five')


‘fivefive’

As you can see, the value is repeated.

### Default Parameters

We can specify default values for parameters:

def jeeves(name = "Sir"):
return "Very good, "+ name

jeeves()


‘Very good, Sir’

If we now pass a value to the function:

jeeves('James')


‘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:

def product(x=5, y=7):
return x*y


If we pass the single value 9

product(9)


63

this sets the value for x, but y remains the default.

We can also explicitly state which parameter we want to pass a value to:

product(y=11)


55

As before, if we call the function with no values, the defaults are use:

product()


35

### 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

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

z = list(range(4))
double_inplace(z)
print(z)


[0, 2, 4, 6]

letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
letters[:] = []


In this example, we’re using [:] to access into the same list, and write it’s data.

vec = [element*2 for element in vec]


would just move a local label, not change the input.

But I’d usually just write this as a function which returned the output:

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


Let’s remind ourselves of the behaviour for modifying lists in-place using [:] with a simple array:

x=5
x=7
x=['a','b','c']
y=x

x


[‘a’, ‘b’, ‘c’]

x[:]=["Hooray!", "Yippee"]

y


[‘Hooray!’, ‘Yippee’]

### Early Return

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.

def extend(to, vec, pad):
if len(vec) >= to:
return # Exit early, list is already long enough
vec[:] = vec + [pad] * (to - len(vec))

x = (list(range(3)))
extend(6, x, 'a')
print(x)


[1, 2, 3, ‘a’, ‘a’, ‘a’]

z = range(9)
extend(6, z, 'a')
print(z)


range(0,9)

### Unpacking arguments

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

def arrow(before, after):
return str(before) + " -> " + str(after)

print(arrow(1, 3))


1 -> 3

x = [1 , -1]

print(arrow(*x))


1 -> -1

This can be quite powerful:

charges={"neutron": 0, "proton": 1, "electron": -1}

charges.items()


dict_items([(‘proton’, 1), (‘neutron’, 0), (‘electron’, -1)])

for particle in charges.items():
print(arrow(*particle))


proton -> 1

neutron -> 0

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:

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

doubler(1,2,3)


[2, 4, 6]

doubler(5,2,"Wow!")


[10, 4, ‘Wow!Wow!’]

### Keyword Arguments

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

def arrowify(**args):
for key, value in args.items():
print(key + " -> " + value)

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


neutron -> n

proton -> p

electron -> e

These different approaches can be mixed:

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)