Introduction to Python Programming. Section 8. Functions

8 Functions

8.1 Objectives

In this section you will learn

  • How to define and call functions.
  • Why it is important to write docstrings.
  • About the difference between parameters and arguments.
  • How to return multiple values.
  • What one means by a wrapper.
  • How to use parameters with default values.
  • How to accept a variable number of arguments.
  • How to define local functions within functions.
  • About anonymous lambda functions.
  • That functions can create and return other functions.
  • That functions are objects, and how to take advantage of that.

8.2 Why learn about functions?

The purpose of writing functions is to make selected functionality easily reusable throughout the code. In this way one avoids code duplication, and brings more structure and transparency into the software.

8.3 Defining new functions

In Python, the definition of every new function begins with the keyword def, but one moreover has to add round brackets and the colon ’:’ at the end of the line. The following sample function adds two numbers and returns the result:

  def add(a, b):
      ~~~
      This function adds two numbers.
      ~~~
      return a + b

The round brackets in the function definition are mandatory even if the function does not have any input parameters, but the return statement can be omitted if not needed. Lines 2 - 4 contain a docstring. Docstrings are optional, but providing one with every function is very important. We will explain why in the following Subsection 8.4.

Note that the code above contains a function definition only – the function is not called. In other words, if you run that program, nothing will happen. In order to call the function and add some numbers with it, one needs to write one additional line such as:

  print(5 + 3 is, add(5, 3))

This will produce the following output:

  5 + 3 is 8

In most cases, functions are defined to process some input parameters and to return some output values. However, this does not apply always. The following function does not take any arguments and it does not return anything:

  def print_hello():
      ~~~
      This function displays ’Hello!’
      ~~~
      print(Hello!)

8.4 Docstrings and function help

Every function should have a docstring – concise and accurate description of what it does. This is useful not only for other people who look at your software, but also for you. There is a famous saying that when you see your own code after several moths, it might as well be written by somebody else – and there is something to it. Showing other people a code where functions do not have docstrings will lead them to think that you probably are not a very experienced programmer.

In addition, Python has a built-in function help which displays the docstrings. This is incredibly useful in larger software projects where usually you do not see the source code of all functions that you are using. Here is a simple example:

  def print_hello():
      ~~~
      This function displays ’Hello!’
      ~~~
      print(Hello!)
  
  help(print_hello)

  Help on function print_hello:
  
  print_hello()
      This function displays ’Hello!’

8.5 Function parameters vs. arguments

The words "parameter" and "argument" are often confused by various authors, so let’s clarify them. When we talk about the definition of a function, such as

  def add(a, b):
      ~~~
      This function adds two numbers.
      ~~~
      return a + b

then we say that the function add has two parameters a and b. But when we are talking about calling the function, such as

  c = add(x, y)

then we say that the function was called with the arguments x and y.

8.6 Names of functions should reveal their purpose

You already know from Section 5 that Python is a dynamically (weakly) typed language. This means that one does not have to specify the types of newly created variables.

Analogously, one does not have to specify the types of function parameters. This means that the above function add(a, b) will work not only for numbers but also for text strings, lists, and any other objects where the operation ’+’ is defined. Let’s try this:

  def add(a, b):
      ~~~
      This function adds two numbers.
      ~~~
      return a + b
  
  word1 = Good 
  word2 = Evening!
  print(add(word1, word2))

  Good Evening!

Or this:

  def add(a, b):
      ~~~
      This function adds two numbers.
      ~~~
      return a + b
  
  L1 = [1, 2, 3]
  L2 = [’a’, ’b’, ’c’]
  print(add(L1, L2))

  [1, 2, 3, ’a’, ’b’, ’c’]

In the last two examples, the use of the function was incompatible with its original purpose which is described in the docstring. Statically typed languages such as C, C++ or Java are more strict and this is not possible. As usual, Python gives you plenty of freedom. Do not abuse it. Reading your code should not be an intellectual exercise. On the contrary – by looking at your code, the reader should effortlessly figure out what the code is doing. For this reason, instead of using a "smart" universal function such as the function add defined above, it would be better to define separate functions add_numbers, add_strings and add_lists whose names clearly state what they are meant to do.

8.7 Functions returning multiple values

Python functions can return multiple values which often comes very handy. For example, the following function time decomposes the number of seconds into hours, minutes and seconds, and returns them as three values:

  def time(s):
      ~~~
      This function converts a number of seconds to hours,
      minutes and seconds.
      ~~~
      h = s // 3600
      s %= 3600
      m = s // 60
      s %= 60
      return h, m, s

Technically, the returned comma-separated values h, m, s are a tuple, so the function could be written with (h, m, s) on the last line:

  def time(s):
      ~~~
      This function converts a number of seconds to hours,
      minutes and seconds.
      ~~~
      h = s // 3600
      s %= 3600
      m = s // 60
      s %= 60
      return (h, m, s)

The tuple which is returned from the function can be stored in a single variable which then becomes a tuple:

  t = time(3666)
  print(t)

  (1, 1, 6)

Or the returned tuple can be stored in three separate variables:

  v1, v2, v3 = time(3666)
  print(v1, v2, v3)

  1  1  6

8.8 What is a wrapper?

Sometimes, when looking for some answers on the web, you may come across the word wrapper. By a wrapper one usually means a function that "wraps" around another function – meaning that it does not do much besides just calling the original function, but adding or changing a few things. Wrappers are often used for functions which one cannot or does not want to change directly. Let’s show an example.

In Subsection 4.33 you learned how to use the ctime function of the time library to extract the current date and time from the system. The 24-character-long text string contains a lot of information:

  Sat May 12 13:20:27 2018

But what if one only needs to extract the date? Here is a wrapper for the function time.ctime which does that:

  def date():
      ~~~
      This function returns date in the form ’May 12, 2018’.
      ~~~
      txt = time.ctime()
      return txt[4:10] + ’,’ + txt[-5:]
  
  import time
  print(date())

  May 12, 2018

Another example of a simple wrapper will be shown in the next subsection. Later, in Subsection 18.1, we will take wrappers to the next level.

8.9 Using parameters with default values

Have you ever been to Holland? It is the most bicycle friendly place in the world. Imagine that you work for the Holland Census Bureau. Your job is to ask 10000 people how they go to work, and enter their answers into a database. The program for entering data into the database was written by one of your colleagues, and it can be used as follows:

  add_database_entry(John, Smith, walks)

or

  add_database_entry(Louis, Armstrong, bicycle)

or

  add_database_entry(Jim, Bridger, horse)

etc. Since you are in Holland, it can be expected that 99% of people are using the bicycle. In principle you could call the function add_database_entry() to enter each answer, but with 9900 bicyclists out of 10000 respondents you would have to type the word "bicycle" many times.

Fortunately, Python offers a smarter way to do this. One can define a new function

  def enter(first, last, transport=bicycle):
      ~~~
      This function calls enter_into_database with
      transport=’bicycle’ by default.
      ~~~
      enter_into_database(first, last, transport)

This is a simple (thin) wrapper to the function add_database_entry() that allows us to omit the third argument in the function call and autocomplete it with a default value which is ~bicycle~. In other words, now we do not have to type ~bicycle~ for all the bicyclists:

  enter(Louis, Armstrong)

Only if we meet a rare someone who uses a car, we can type

  enter(Niki, Lauda, car)

A few things to keep in mind:

(1) Parameters with default values need to be introduced after standard (non-default) parameters. In other words, the following code will result into an error:

  def add(a=5, b):
      ~~~
      This function adds a, b with default value a=5.
      ~~~
      return a + b

  on line 1:
  SyntaxError: non-default argument follows default argument

(2) When any of the optional arguments are present in the function call, it is a good practise to use their names to avoid ambiguity and make your code easier to read:

  def add(x, a=2, b=3):)
      ~~~
      This function adds x, a, b with default values a=2 and b=3.
      ~~~
      return x + a + b
  
  print(add(1, b=5)

In this case the value 1 will be assigned to x, a will take the default value 2, and b will be 5. The output will be

  8

8.10 Functions accepting a variable number of arguments

The following function will multiply two numbers:

  def multiply(x, y):
      ~~~
      This function multiplies two values.
      ~~~
      return x * y
  
  print(multiply(2, 3))

  6

But what if we wanted a function that can by called as multiply(2, 3, 6) or multiply(2, 3, 6, 9), multiplying a different number of values each time?

Well, the following could be done using a list:

  def multiply(L):
      ~~~
      This function multiplies all values coming as a list.
      ~~~
      result = 1
      for v in L:
          result *= v
      return result
  
  print(multiply([2, 3, 6]))

  36

But then one would have to use extra square brackets when calling the function. For two values, the code multiply([2, 3]) would not be backward compatible with the original code multiply(2, 3).

This problem can be solved using *args. Using *args is very similar to using a list, but one does not need to use the square brackets when calling the function afterwards:

  def multiply(*args):
      ~~~
      This function multiplies an arbitrary number of values.
      ~~~
      result = 1
      for v in args:
          result *= v
      return result
  
  print(multiply(2, 3, 6))

  36

8.11 *args is not a keyword

As a remark, let us mention that args is just a name for a tuple, and any other name would work as well:

  def multiply(*values):
      ~~~
      This function multiplies an arbitrary number of values.
      ~~~
      result = 1
      for v in values:
          result *= v
      return result
  
  print(multiply(2, 3, 6))

  36

However, using the name args in functions with a variable number of arguments is so common that using a different name might confuse a less experienced programmer. Therefore, we recommend to stay with args.

8.12 Passing a list as *args

When using *args, then args inside the function is a tuple (immutable type), not a list. So, using *args is safer from the point of view that the function could change the list, but it cannot change the args.

When we have many numbers which we don’t want to type one after another, or when we want to pass a list L as *args for another reason, it can be done by passing *L to the function instead of L. The following example multiplies 1000 numbers:

  def multiply(*args):
      ~~~
      This function multiplies an arbitrary number of values.
      ~~~
      result = 1
      for v in args:
          result *= v
      return result
  
  L = []
  for i in range(1000):
      L.append(1 + i/1e6)
  print(multiply(*L))

  1.6476230382011265

8.13 Obtaining the number of *args

For some tasks one needs to know the number of the arguments which are passed through *args. Since args is a tuple inside the function, one can normally use the function len. This is illustrated on the function average below which calculates the arithmetic average of an arbitrary number of values:

  def average(*args):
      ~~~
      This function calculates the average of an arbitrary
      number of values.
      ~~~
      n = len(args)
      s = 0
      for v in args:
          s += v
      return s / n
  
  average(2, 4, 6, 8, 10)

  6.0

8.14 Combining standard arguments and *args

Let’s say that we need a function which not only calculates the average of an arbitrary number of values, but in addition increases or decreases the result by a given offset. The easiest way is to put the standard parameter first, and *args second:

  def average(offset, *args):
      ~~~
      This function calculates the average of an arbitrary
      number of values, and adds an offset to it.
      ~~~
      n = len(args)
      s = 0
      for v in args:
          s += v
      return offset + s / n
  
  average(100, 2, 4, 6, 8, 10)

  106.0

You could also do it the other way round – put *args first and the offset second – but this is not natural and people would probably ask why you did it. When calling the function, you would have to keyword the standard argument to avoid ambiguity:

  def average(*args, offset):
      ~~~
      This function calculates the average of an arbitrary
      number of values, and adds an offset to it.
      ~~~
      n = len(args)
      s = 0
      for v in args:
          s += v
      return offset + s / n
  
  average(2, 4, 6, 8, 10, offset=100)

  106.0

8.15 Using keyword arguments *kwargs

The name kwargs stands for "keyword arguments". You already know from Subsection 8.10 that args is a tuple inside the function. Analogously, kwargs is a dictionary. In other words, the **kwargs construct makes it possible to pass a dictionary into a function easily, without using the full dictionary syntax. In Subsection 8.11 we mentioned that args is just a name and one can use any other name instead. The same holds about kwargs.

For illustration, the following function displaydict accepts a standard dictionary D and displays it, one item per line:

  def displaydict(D):
      ~~~
      This function displays a dictionary, one item per line.
      ~~~
      for key, value in D.items():
          print(key, value)
  
  displaydict({’Jen’:7023744455, ’Julia’:7023745566,
  ’Jasmin’:7023746677})

  Jen 7023744455
  Julia 7023745566
  Jasmin 7023746677

The code below does the same using **kwargs. Notice that instead of the colon : one uses the assignment operator =. Also notice that the keys are not passed as text strings anymore – they are converted to text strings implicitly:

  def displaydict(**kwargs):
      ~~~
      This function displays a list of keyword arguments,
      one item per line.
      ~~~
      for key, value in kwargs.items():
          print(key, value)
  
  displaydict(Jen=7023744455, Julia=7023745566,
  Jasmin=7023746677)

  Jen 7023744455
  Julia 7023745566
  Jasmin 7023746677

8.16 Passing a dictionary as **kwargs

In Subsection 8.12 you have seen how one can pass a list to a function through *args. Analogously, it is possible to pass a dictionary to a function through **kwargs, as we show in the following example:

  def displaydict(**kwargs):
      ~~~
      This function displays a list of keyword arguments,
      one item per line.
      ~~~
      for key, value in kwargs.items():
          print(key, value)
  
  D = {’Jen’:7023744455, ’Julia’:7023745566, ’Jasmin’:7023746677}
  displaydict(**D)

  Jen 7023744455
  Julia 7023745566
  Jasmin 7023746677

8.17 Defining local functions within functions

In Subsections 5.13 - 5.15 you learned about the importance of using local (as well as not using global) variables in functions. In short, defining variables on the global scope pollutes the entire code because the name of a global variable can clash with the definition of a function or variable defined elsewhere.

Using local functions is less common than using local variables. But if one needs to use some functionality more than once within the body of a function, and not elsewhere in the code, then one should create a local function for it. Typically, the local function would be a small, one-purpose function while the outer function might be rather large and complex. Here is a bit artificial example whose sole purpose is to illustrate the mechanics of this:

  def split_numbers(L):
      ~~~
      This function splits a list of integers
      into two lists of even and odd numbers.
      ~~~
  
      def is_even(v):
          ~~~
          This function returns True if the number n
          is even, and False otherwise.
          ~~~
          return v%2 == 0
  
      E = []
      O = []
      for n in L:
          if is_even(n):
              E.append(n)
          else:
              O.append(n)
      return E, O

  nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  enums, onums = split_numbers(nums)
  print(~Even:~, enums)
  print(~Odd:~, onums)

  Even: [2, 4, 6, 8, 10]
  Odd: [1, 3, 5, 7, 9]

We will not copy the entire code once more here, but if we tried to call the function is_even outside the function split_numbers, the interpreter would throw an error message similar to this one:

  on line 28:
  NameError: name ’is_even’ is not defined

8.18 Anonymous lambda functions

Python has a keyword lambda which makes it possible to define anonymous one-line functions. The most important thing about these functions is that they are expressions (in contrast to standard functions defined using the def statement). Therefore, lambda functions can be used where statements are not allowed. The following example defines a lambda function which for a given value of x returns x**2, and then calls it with x = 5:

  g = lambda x: x**2
  g(5)

  25

Note that the keyword lambda is followed by the name of the independent variable (here x), a colon, and then an expression containing the independent variable.

Also note that the anonymous function is assigned to a variable named g, by which it loses its anonymity. This negates the purpose of the keyword lambda and it’s not how lambda functions are normally used. We did it here for simplicity – just to be able to call the function and display the result without making things more complex right away.

Before getting to more realistic applications of anonymous functions, let’s mention that (in rare cases) they can be defined without parameters:

  import time
  day = lambda : time.ctime()[:3]
  day()

  Wed

And they can be defined with more than one parameter if needed:

  plus = lambda x, y: x + y
  plus(3, 4)

  7

8.19 Creating multiline lambdas

Although it is possible to create multi-line lambda functions, this is against the philosophy of Python and strongly discouraged. If you really must do it, search the web and you will find your example.

8.20 Using lambdas to create a live list of functions

Let’s say that you have a large data file D, and for each value v in it you need to calculate v**2, cos(v), sin(v), exp(v) and log(v). Here is a very small sample of your data file:

  D = [2.1, 1.9, 3.2]

Then an elegant approach is to define a list containing the five functions,

  import numpy as np
  F = [lambda x: x**2, lambda x: np.cos(x), \
  lambda x: np.sin(x), lambda x: np.exp(x), lambda x: np.log(x)]

and use a pair of nested for loops to apply all functions to all data points:

  for v in D:
      for f in F:
          print(round(f(v), 5), end= ’)
      print()

Output:

  4.41 -0.50485 0.86321 8.16617 0.74194
  3.61 -0.32329 0.9463 6.68589 0.64185
  10.24 -0.99829 -0.05837 24.53253 1.16315

As you could see, in this example the five lambda functions were truly anonymous – their names were not needed because they were stored in a list.

8.21 Using lambdas to create a live table of functions

Imagine that you want to create a table (dictionary) of functions where on the left will be their custom names and on the right the functions themselves as executable formulas. This can be done as follows:

  import numpy as np
  
  def addfns(d, **kwargs):
      ~~~
      Add into dictionary d an arbitrary number of functions.
      ~~~
      for name, f in kwargs.items():
          d[name] = f
  
  F = {}
  addfns(F, square=lambda x: x**2, cube=lambda x: x**3, \
  exp=lambda x: np.exp(x))
  
  print(F[’square’](2))
  print(F[’cube’](2))
  print(F[’exp’](2))

  4
  8
  7.38905609893065

8.22 Using lambdas to create a live table of logic gates

Recall that there are seven types of basic logic gates:

  • AND(a, b) = a and b,
  • OR(a, b) = a or b,
  • NOT(a) = not a,
  • NAND(a, b) = not(AND(a, b)),
  • NOR(a, b) = not(OR(a, b)),
  • XOR(a, b) = (a and not b) or (not a and b),
  • XNOR(a, b) = not(XOR(a, b)).

The previous example can easily be adjusted to create their table:

  def addfns(d, **kwargs):
      ~~~
      Add into dictionary d an arbitrary number of functions.
      ~~~
      for name, f in kwargs.items():
          d[name] = f
  
  LG = 
  addfns(LG, AND=lambda a, b: a and b, OR=lambda a, b: a or b, \
        NOT=lambda a: not a, NAND=lambda a, b: not (a and b), \
        NOR=lambda a, b: not (a or b), \
        XOR=lambda a, b: (a and not b) or (not a and b), \
        XNOR=lambda a, b: not((a and not b) or (not a and b)))
  
  x = True
  y = True
  print(LG[’AND’](x, y))
  print(LG[’OR’](x, y))
  print(LG[’NOT’](x))
  print(LG[’NAND’](x, y))
  print(LG[’NOR’](x, y))
  print(LG[’XOR’](x, y))
  print(LG[’XNOR’](x, y))

  True
  True
  False
  False
  False
  False
  True

8.23 Using lambdas to create a factory of functions

We have not talked yet about functions which create and return other functions. This is a very natural thing in Python which has many applications. To mention just one - it allows Python to be used for functional programming. This is an advanced programming paradigm which is very different from both procedural and object-oriented programming. We will not discuss it in this course, but if you are interested, you can read more on Wikipedia (https://en.wikipedia.org/wiki/Functional_programming).

The following example defines a function quadfun(a, b, c) which returns a function ax2 + bx + c , and shows two different ways to call it:

  def quadfun(a, b, c):
      ~~~
      Return a quadratic function ax^2 + bx + c.
      ~~~
      return lambda x: a*x**2 + b*x + c
  
  f = quadfun(1, -2, 3)
  print(f(1))
  print(quadfun(1, -2, 3)(1))

  2
  2

Standard functions which are defined locally within other functions can be returned in the same way.

8.24 Variables of type ’function’

In the previous example, we created a quadratic function  2
x  - 2x + 3  and assigned it to a variable named f. Hence this variable is callable (has type ’function’). You already know from Subsection 5.11 how to check the type of a variable using the built-in function type:

  print(type(f))

  <class ’function’>

Python has a built-in function callable which can be used to check at runtime whether an object is a function:

  if callable(f):
      print(’f is a function.’)
  else:
      print(’f is not a function.’)

  f is a function.

8.25 Inserting conditions into lambda functions

Look at the sample functions min, even and divisible:

  min = lambda a, b: a if a < b else b
  even = lambda n: True if n%2 == 0 else False
  divisible = lambda n, m: True if n%m == 0 else False

Here, if-else is not used as a statement, but as a conditional expression (ternary operator). We will talk more about this in Subsection 10.10. For now, the above examples give you an idea of how to incorporate conditions into lambda functions.

Notice that we named the anonymous functions right after creating them, which contradicts their purpose. We just did this here to illustrate the inclusion of the conditional statement without making things unnecessarily complex.

8.26 Using lambdas to customize sorting

As you know from Subsection 7.22, calling L.sort() will sort the list L in place. From Subsection 7.23 you also know that Python has a built-in function sorted which returns a sorted copy of the list L while leaving L unchanged. Both sort and sorted accept an optional argument key which is an anonymous function.

This function, when provided, is applied to all items in the list L prior to sorting. It creates a helper list. The helper list is then sorted, and the original list L simultaneously with it. This makes it possible to easily implement many different sorting criteria.

Let’s look at an example that sorts a list of names in the default way:

  L = [’Meryl Streep’, ’Nicole Kidman’, ’Julia Roberts’]
  S = sorted(L)
  print(S)

  [’Julia Roberts’, ’Meryl Streep’, ’Nicole Kidman’]

But it would be much better to sort the names according to the last name. This can be done by defining a key which splits each text string into a list of words, and picks the last item:

  L = [’Meryl Streep’, ’Nicole Kidman’, ’Julia Roberts’]
  S = sorted(L, key=lambda w: w.split()[-1])
  print(S)

  [’Nicole Kidman’, ’Julia Roberts’, ’Meryl Streep’]

8.27 Obtaining useful information about functions from their attributes

Functions in Python are objects, which means that they have their own attributes (more about this will be said in Section 14). Three of the most widely used ones are __name__ which contains the function’s name, __doc__ which contains the docstring (if defined), and __module__ which contains the module where the function is defined.

Let’s look, for example, at the built-in function sorted:

  fn = sorted
  print(fn.__name__)
  print(’-----------’)
  print(fn.__module__)
  print(’-----------’)
  print(fn.__doc__)

  sorted
  -----------
  builtins
  -----------
  Return a new list containing all items from the iterable in
  ascending order.
  
  A custom key function can be supplied to customize the sort
  order, and the reverse flag can be set to request the result
  in descending order.

This works in the same way for your custom functions (as long as the docstring is defined).


Table of Contents

Created on August 6, 2018 in Python I,   Python II.
Add Comment
0 Comment(s)

Your Comment

By posting your comment, you agree to the privacy policy and terms of service.