3. Introduction to Functions

In the same way that variables make it possible to symbolically manipulate values using names, it is possible to use a name to refer to a series of computation. Such a series of computations is called a function and can have various effects, depending on the context and on values given to them. We have already seen a few examples of pre defined functions such as print or isinstance.

Having (well-chosen) names for values and computations makes it easier to re-use and combine them to form more complex values and computations. This is an essential tool to build a program.

3.1. Creating and Calling Functions

Here is the general syntax for creating a function:

def function_name(arguments):
    # computation using arguments

Note

Everything after the # character on a line of Python code is ignored at execution. This is called a comment, and can be used to explain what the code is meant to do.

The arguments are optional and multiple arguments must be comma-separated.

The computation performed by the function, or function body, consists in Python code that is indented by one level with respect to the level to which the def statement belongs. To indicate the end of the function body, just write in the level of the def statement again:

def function_name(arguments):
    # code belonging to the function body

# some code not belonging to the function body

Note that def is a statement that works in a similar way to the assignment operator. When def is executed, a function object is created and an object reference with the specified name is created and set to refer to the function object. Since functions are objects, they can be stored in collection data types and passed as arguments to other functions, as we will see in Dive into Functions.

The def statement has no other effects than creating the function and making it accessible via its reference name. To actually execute the instructions it contains, the function should be called. We can call functions (execute the code in the function) by appending parentheses to the function name, with arguments between the parentheses, if needed:

function_name(arguments)

Every Python function has a return value.

We can capture this value by binding it to a variable:

result = function_name(arguments)

By default, this value is None, a special value indicating the absence of a more concrete value.

Here is an example in which we create and use a function that has no explicit result:

>>> def print_something(something):
...     print(something)
...
>>> result = print_something("I'm the text to print")
I'm the text to print
>>> result is None
True

The effect of the above function, as its name suggests, is to display something on the screen. We capture its result by binding it to a variable (that we call result, but we could have chosen any valid variable identifier). And we can indeed check that the result is None, using the is special keyword.

If we want a non-default result, we need to return it from the function using the syntax return value. The return value has to be just one value. In case we need to return more values, we can return an object containing several values.

Here is an example in which we create and use a function with an explicit result:

>>> def return_one():
...     return 1
...
>>> result = return_one()
>>> result
1

In the above example, the function takes no argument. We can of course also have at the same time arguments and a return value:

>>> def square(number):
...     return number * number
...
>>> square(2)
4
>>> square(return_one())
1

Note

In the interactive interpreter, we actually do not need to capture the result if all we want to do is test the function and see its result.

As we have already seen, just typing a value (or a reference to a value), or a function returning a value, has the effect of displaying that value.

In a program, this does not happen. One needs to explicitly use code that displays values (typically via the print function) to have them printed on screen.

The return value can be ignored by the caller, in which case it is simply thrown away (although it can be displayed in interactive mode, as explained in the above note).

An argument can have a default value, set using = when defining the function:

>>> def print_something(something="Hello!"):
...     print(something)
...
>>> print_something()
Hello!
>>> print_something("Bye!")
Bye!

When the function is called with an argument, the corresponding value substitutes the default one.

Note

By the way, as with any object reference, we can re-bind a new function to an identifier. Here, we just did this for the print_something identifier.

Finally, here is an example with more than one argument (and an explicit return value):

>>> def hello_someone(first_name, last_name):
...     return "Hello {0}, {1}".format(first_name, last_name)
...
>>> print(hello_someone("Jeffrey", "Lebowski"))
Hello Jeffrey, Lebowski

The return statement causes the function to exit, preventing the execution of further code in the function:

>>> def useless_print():
...     print("I'm printed.")
...     return
...     print("I'm never printed.")
...
>>> useless_print()
I'm printed.
>>>

(A yield statement exists that produces a result without exiting the function. This will not be covered here [prog_in_python3]).

3.2. Names and Docstrings

Using good names for a function and its parameters goes a long way towards making the purpose and use of the function clear to other programmers and to ourselves some time after we have created the function. Here are a few rules of thumb that you might like to consider.

  • Use a naming scheme, and use it consistently (see Style Guide for Python Code for naming convention).

  • For all names, avoid abbreviations, unless they are both standardized and widely used.

  • Be proportional with variable and parameter names: x is a perfectly good name for an x-coordinate and i is fine for a loop counter, but in general the name should be long enough to be descriptive. The name should describe the meaning of the data rather than its type (e.g., amount_due rather than money), unless the use is generic to a particular type (see, for example, the text parameter in the shorten example, later).

  • Functions and methods should have names informative about what they do or what they return (depending on their emphasis), but never about how they do it, since that might change.

Here are a few naming examples:

def find(l, s, i=0):  # BAD
def linear_search(l, s, i=0):  # BAD
def first_index_of(sorted_name_list, name, start=0):  # GOOD

All three functions return the index position of the first occurrence of a name in a list of names, starting from the given starting index and using an algorithm that assumes the list is already sorted.

  • The first one is bad because the name gives no clue as to what will be found, and its parameters (presumably) indicate the required types (list, string, integer) without indicating what they mean.

  • The second one is bad because the function name describes the algorithm originally used — it might have been changed since. This may not matter to users of the function, but it will probably confuse maintainers (those who update the code of fix bugs) if the name implies a linear search, but the algorithm implemented has been changed to a binary search.

  • The third one is good because the function name says what is returned, and the parameter names clearly indicate what is expected.

None of the functions have any way of indicating what happens if the name isn’t found. Do they return, say, -1, or do they raise an exception? Somehow, such information needs to be documented for users of the function.

There are several possibilities to document Python code. For simple and small projects, we can add documentation to any function by using a docstring. This is simply a string that comes immediately after the def line, and before the function code proper begins. For example, here is a shorten function:

def shorten(text, length=25, indicator="..."):
  """Return *text* or a truncated copy with the indicator added.

  *text* is any string; *length* is the maximum length of the returned
  string (including any indicator); *indicator* is the string added at
  the end to indicate that the text has been shortened.

  >>> shorten("Second Variety")
  'Second Variety'
  >>> shorten("Voices from the Street", 17)
  'Voices from th...'
  >>> shorten("Radio Free Albemuth", 10, "*")
  'Radio Fre*'
  """
  if len(text) > length:
     text = text[:length - len(indicator)] + indicator
  return text

Note

The above docstring is written using pairs of triple quotes. Text between pairs of triple quotes can contain new lines and single quotes. The computation itself is based on an if control structure, and on string manipulation operators. We will see these notions later.

It is not unusual for a function or method’s documentation to be longer than the function itself. One convention is to make the first line of the docstring a brief one-line description, then have a blank line followed by a full description, and then to reproduce some examples as they would appear if typed in interactively. Examples in function documentation can be used to provide unit tests, through automated checks that code behaves as intended.

For bigger projects, sphinx, a Python Document Generator can be used. It is powerful and simple. The “learning curve” is not too steep and is a good way to provide users and developers a full documentation of the project in different formats: web site, pdf, epub, … A lot of Python project use sphinx: python, NumPy, … This course has been written using sphinx.

3.3. Functions are objects

As mentioned earlier, in Python everything is an object, so functions are objects. They are callable objects. Since functions are objects, they can be handled as all other objects.

functions are object
>>> isinstance(return_one, object)
True
>>> def global_func():
...     return "global_func is a global function"
...
>>> other_func = global_func
>>> other_func()
'global_func is a global function'