8 Functions

Most of the time, we use the basic functions or those contained in modules. However, when retrieving data online or formatting data imported from various sources, it may be necessary to create our own functions. The advantage of creating one’ s functions is revealed when one has to carry out a series of instructions repeatedly, with some slight differences (we can then apply the functions within a loop, as we discussed in Chapter 7).

8.1 Definition

A function is declared using the keyword def. What it returns is returned using the keyword return.

La syntaxe est la suivante :

def name_function(arguments):
  body of the function

Once the function is defined, it is called by referring to its name:

name_function()

So, all we need to do is add parentheses to the name of the function to call it. Indeed, function_name refers to the object that contains the function that is called using the expression function_name(). For example, if we want to define the function that calculates the square of a number, here is what we can write:

def square(x):
  return x**2

It can then be called:

print(square(2))
## 4
print(square(-3))
## 9

8.1.1 Adding a Description

It is possible (and strongly recommended) to add a description of what the function does, by adopting some conventions (see https://www.python.org/dev/peps/pep-0257/) =

def square(x):
  """returns the squared value of x"""
  return x**2

When the next instruction is then evaluated, the description of the function is displayed:

`?`(square)

In Jupyter Notebook, after writing the name of the function, the description can also be displayed by pressing the Shift and Tabulation keys on the keyboard.

8.1.2 Arguments of a Function

In the example of the square() function we created, we filled in only one argument, called x. If the function we wish to create requires several argument, they must be separated by a comma.

Let us consider, for example, the following problem. We have a production function \(Y(L, K, M)\), which depends on the number of workers \(L\) and the amount of capital \(K\), and the equipment \(M\), such that \(Y(L, K, M) = L^{0.3} K^{0.5}M^2\). This function can be written in Python as follows:

def production(l, k, m):
  """
  Returns the value of the production according to
  labour, capital and materials

  Keyword arguments:
  l -- labour (float)
  k -- capital (float)
  m -- materials (float)
  """
  return l**0.3 * k**0.5 * m**(0.2)

8.1.2.1 Call Without Argument Names

Using the previous example, if we are given \(L = 60\) and \(K = 42\) and \(M = 40\), we can deduce the production:

prod_val = production(60, 42, 40)
print(prod_val)
## 46.289449781254994

It should be noted that the name of the arguments has not been mentioned here. When the function was called, the value of the first argument was assigned to the first argument (l), the second to the second argument (k) and finally the third to the third argument (m).

8.1.2.2 Positional Arguments, Arguments by Keywords

There are two types of arguments that can be given to a function in Python:

  • the positional arguments;
  • arguments by keywords.

Unlike positional arguments, keyword arguments have a default value assigned by default. We speak of a formal argument to designate the arguments of the function (the variables used in the body of the function) and an effective argument to designate the value that we wish to give to the formal argument To define the value to be given to a formal argument, we use the equality symbol. When calling the function, if the user does not explicitly define a value, the default value will be assigned. Thus, it is not necessarily necessary to specify the arguments by keywords when calling the function.

It is important to note that positional arguments (those that do not have a default value) must appear first in the argument list.

Let’s take an example with two positional arguments (l and m) and one argument per keyword (k):

def production_2(l, m, k=42):
  """
  Returns the value of the production according to
  labour, capital and materials

  Keyword arguments:
  l -- labour (float)
  m -- materials (float)
  k -- capital (float) (default 42)
  """
  return l**0.3 * k**0.5 * m**(0.2)

The production_2() function can be called, to give the same result, in the following three ways:

# By naming all argument, by ommitting k
prod_val_1 = production_2(l = 42, m = 40)
# By naming all argument and specifying k
prod_val_2 = production_2(l = 42, m = 40, k = 42)
# By naming only the argument k
prod_val_3 = production_2(42, 40, k = 42)
# Without naming any argument
prod_val_4 = production_2(42, 40, 42)

res = [prod_val_1, prod_val_2, prod_val_3, prod_val_4]
print(res)
## [41.59215573604822, 41.59215573604822, 41.59215573604822, 41.59215573604822]

If the function contains several positional arguments; when evaluating:

  • or all positional arguments are named by their name;
  • or none;
  • there are no in-between.

As long as all the positional arguments are named during the evaluation, they can be listed in different orders:

def production_3(a, l, m = 40, k=42):
  """
  Returns the value of the production according to
  labour, capital and materials

  Keyword arguments:
  a -- total factor productivity (float)
  l -- labour (float)
  m -- materials (float) (default 40)
  k -- capital (float) (default 42)
  """
  return a * l**0.3 * k**0.5 * m**(0.2)

prod_val_1 = production_3(1, 42, m = 38)
prod_val_2 = production_3(a = 1, l = 42)
prod_val_3 = production_3(l = 42, a = 1)
prod_val_4 = production_3(m = 40, l = 42, a = 1)

res = [prod_val_1, prod_val_2, prod_val_3, prod_val_4]
print(res)
## [41.16765711449734, 41.59215573604822, 41.59215573604822, 41.59215573604822]

8.1.2.3 Function as an Argument to Another Function

A function can be provided as an argument to another function.

def square(x):
  """Returns the squared value of x"""
  return x**2

def apply_fun_to_4(fun):
  """Applies the function `fun` to 4"""
  return fun(4)

print(apply_fun_to_4(square))
## 16

8.2 Scope of a Function

When a function is called, the body of that function is interpreted. Variables that have been defined in the body of the function are assigned to a local namespace. In other words, they live only within this local space, which is created at the moment of the call of the function and destroyed at the end of it. This is referred to as the scope of the variables. Thus, a variable with a local scope (assigned in the local space) can have the same name as a global variable (defined in the global workspace), without designating the same object, or overwrite this object.

Let’s look at this through an example.

# Definition of a global variable:
value = 1

# Definition of a local variable in function f
def f(x):
  value = 2
  new_value = 3
  print("value equals: ", value)
  print("new_value equals: ", new_value)
  return x + value

Let’s call the f() function, then look at the value and new_value values after executing the function.

res = f(3)
## value equals:  2
## new_value equals:  3
print("value equals: ", value)
## value equals:  1
print("new_value equals: ", new_value)
## Error in py_call_impl(callable, dots$args, dots$keywords): NameError: name 'new_value' is not defined
## 
## Detailed traceback: 
##   File "<string>", line 1, in <module>

As can be seen, during the evaluation, the local variable of the name value was 2, which did not refer to the variable of the same name defined in the global environment. After executing the f() function, this local value variable is deleted, and the same applies to the local new_value variable, which does not exist in the global environment (hence the error returned).

Without going into too much detail, it seems important to know some principles about the scope of variables. Variables are defined in environments, which are embedded in each other. If a variable is not defined in the body of a function, Python will search in a parent environment.

value = 1
def f(x):
  return x + value

print(f(2))
## 3

If we define a function within another function, and call a variable not defined in the body of that function, Python will search in the directly superior environment. If it does not find, it will search in the even higher environment, and so on until ir reaches the global environment.

# The value variable is not defined in
# the local environment of g().
# Python will then search in f().
value = 1
def f():
  value = 2
  def g(x):
    return x + value

  return g(2)

print(f())
## 4
# The value variable is not defined in g() or f()
# but in the higher environment (here, global)
value = 1
def f():
  def g(x):
    return x + value

  return g(2)

print(f())
## 3

If a variable is defined in the body of a function and we want it to be accessible in the global environment, we can use the keyword global:

def f(x):
  global y
  y = x+1

f(3)
print(y)
## 4

The variable that we want to define globally from a local space of the function must not have the same name of one of the arguments.

8.3 Lambda Functions

Python offers what are called lambdas functions, or anonymous functions. A lambda function has only one instruction whose result is that of the function.

They are defined using the keyword lambda. The syntax is as follows:

name_function = lambda arguments : result

The arguments are to be separated by commas.

Let’s take the function square() created previously:

def square(x):
  return x**2

The equivalent lambda function is written:

square_2 = lambda x: x**2
print(square_2(4))
## 16

With several arguments, let’s look at the lambda function equivalent to the production() function:

def production(l, k, m):
  """
  Returns the value of the production according to
  labour, capital and materials.

  Keyword arguments:
  l -- labour (float)
  k -- capital (float)
  m -- materials (float)
  """
  return l**0.3 * k**0.5 * m**(0.2)
production_2 = lambda l,k,m : l**0.3 * k**0.5 * m**(0.2)
print(production(42, 40, 42))
## 40.987803063838406
print(production_2(42, 40, 42))
## 40.987803063838406

8.4 Returning Several Values

It can sometimes be convenient to return several elements in return for a function. Although the list is a candidate for this feature, it may be better to use a dictionary, to be able to access the values with their key!

import statistics
def desc_stats(x):
  """Returns the mean and standard deviation of `x`"""
  return {"mean": statistics.mean(x),
  "std_dev": statistics.stdev(x)}

x = [1,3,2,6,4,1,8,9,3,2]
res = desc_stats(x)
print(res)
## {'mean': 3.9, 'std_dev': 2.8460498941515415}
message = "The average value equals {} and the standard deviation is {}"
print(message.format(res["mean"], res["std_dev"]))
## The average value equals 3.9 and the standard deviation is 2.8460498941515415

8.5 Exercise

  1. Create a function named sum_n_integers which returns the sum of the first integer \(n\). Its only argument will be n.
  2. Using a loop, display the sum of the first 2 integers, then 3 first integers, then 4 first integers, etc. up to 10.
  3. Create a function that from two points represented by pairs of coordinates (\(x_1\), \(y_1\)) and (\(x_2\), \(y_2\)) returns the Euclidean distance between these two points. Propose a second solution using a lambda function.