Note: Click on "Kernel" > "Restart Kernel and Clear All Outputs" in JupyterLab before reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it in the cloud .
In Chapter 1 , we simply typed the code to calculate the average of the even numbers in a list of whole numbers into several code cells. Then, we executed them one after another. We had no way of reusing the code except for either executing cells multiple times. And, whenever we find ourselves doing repetitive manual work, we can be sure that there must be a way of automating what we are doing.
This chapter shows how Python offers language constructs that let us define functions ourselves that we may then call just like the built-in ones. Also, we look at how we can extend our Python installation with functionalities written by other people.
Python comes with plenty of useful functions built in, some of which we have already seen before (e.g., print() , sum()
, len()
, or id()
). The documentation
has the full list. Just as core Python itself, they are mostly implemented in C and thus very fast.
Below, sum() adds up all the elements in the
numbers
list while len() counts the number of elements in it.
numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
sum(numbers)
78
len(numbers)
12
sum
and len
are no keywords like
for
or if
but variables that reference objects in memory. Often, we hear people say that "everything is an object in Python" (e.g., this question ). While this phrase may sound abstract in the beginning, it simply means that the entire memory is organized with "bags" of 0s and 1s, and there are even bags for the built-in functions. That is not true for many other languages (e.g., C or Java) and often a source of confusion for people coming to Python from another language.
The built-in id() function tells us where in memory a particular built-in function is stored.
id(sum)
139940703477088
id(len)
139940703476048
type(sum)
builtin_function_or_method
type(len)
builtin_function_or_method
Python's object-oriented nature allows us to have functions work with themselves. While seemingly not useful from a beginner's point of view, that enables a lot of powerful programming styles later on.
id(id)
139940703475648
type(type)
type
To execute a function, we call it with the call operator ()
as shown many times in Chapter 1 and above.
If we are unsure whether a variable references a function or not, we can verify that with the built-in callable() function.
Abstractly speaking, any object that can be called with the call operator ()
is a so-called callable. And, objects of type builtin_function_or_method
are just one kind of examples thereof. We will see another one already in the next sub-section.
callable(sum)
True
callable(len)
True
list
objects, for example, are not callable.
callable(numbers)
False
The list of built-in functions in the documentation should really be named a list of built-in callables.
Besides the built-in functions, the list also features constructors for the built-in types. They may be used to cast (i.e., "convert") any object as an object of a given type.
For example, to "convert" a float
or a str
into an int
object, we use the int() built-in. Below, new
int
objects are created from the 7.0
and "7"
objects that are newly created themselves before being processed by int() right away without ever being referenced by a variable.
int(7.0)
7
int("7")
7
Casting an object as an int
is different from rounding with the built-in round() function!
int(7.99)
7
round(7.99)
8
int(-7.99)
-7
Not all conversions are valid and runtime errors may occur as the ValueError
shows.
int("seven")
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[18], line 1 ----> 1 int("seven") ValueError: invalid literal for int() with base 10: 'seven'
float(7)
7.0
str(7)
'7'
Constructors are full-fledged objects as well.
id(int)
94623097764960
id(float)
94623097758720
They are of type type
, which is different from builtin_function_or_method
above.
type(int)
type
type(float)
type
As already noted, constructors are callables. In that regard, they behave the same as built-in functions. We may call them with the call operator ()
.
callable(int)
True
callable(float)
True
The attentive student may already have discovered that we refer to builtin_function_or_method
objects as "built-in functions" and type
objects as just "built-ins." For a beginner, that difference is not so important. But, the ambitious student should already be aware that such subtleties exist.
Next, let's look at a third kind of callables.
We may create so-called user-defined functions with the def
statement (cf., reference ). To extend an already familiar example, we reuse the introductory example from Chapter 1
in its final Pythonic version and transform it into the function
average_evens()
below. We replace the variable name numbers
with integers
for didactical purposes in the first couple of examples.
A function's name must be chosen according to the same naming rules as ordinary variables since Python manages function names like variables. In this book, we further adopt the convention of ending function names with parentheses ()
in text cells for faster comprehension when reading (i.e., average_evens()
vs. average_evens
). These are not part of the name but must always be written out in the def
statement for syntactic reasons.
Functions may define an arbitrary number of parameters as inputs that can then be referenced within the indented code block: They are listed within the parentheses in the def
statement (i.e., integers
below).
The code block is also called a function's body, while the first line starting with def
and ending with a colon is the header.
Together, the name and the list of parameters are also referred to as the function's signature (i.e.,
average_evens(integers)
below).
A function may specify an explicit return value (i.e., "result" or "output") with the return
statement (cf., reference ): Functions that have one are considered fruitful; otherwise, they are void. Functions of the latter kind are still useful because of their side effects. For example, the built-in print()
function changes what we see on the screen. Strictly speaking, print()
and other void functions also have an implicit return value, namely the
None
object.
A function should define a docstring that describes what it does in a short subject line, what parameters it expects (i.e., their types), and what it returns, if anything. A docstring is a syntactically valid multi-line string (i.e., type str
) defined within triple-double quotes """
. Strings are covered in depth in Chapter 6 . Widely adopted standards for docstrings are PEP 257
and section 3.8 of Google's Python Style Guide
.
def average_evens(integers):
"""Calculate the average of all even numbers in a list.
Args:
integers (list of int's): whole numbers to be averaged
Returns:
average (float)
"""
evens = [n for n in integers if n % 2 == 0]
average = sum(evens) / len(evens)
return average
Once defined, a function may be referenced just like any other variable by its name (i.e., without the parentheses).
average_evens
<function __main__.average_evens(integers)>
This works as functions are full-fledged objects. So, average_evens
is just a name referencing an object in memory with an identity, a type, namely function
, and a value. In that regard, average_evens
is no different from the variable numbers
or the built-ins' names.
id(average_evens)
139940571731424
type(average_evens)
function
Its value may seem awkward at first: It consists of a location showing where the function is defined (i.e., __main__
here, which is Python's way of saying "in this notebook") and the signature wrapped inside angle brackets <
and >
.
The angle brackets are a convention to indicate that the value may not be used as a literal (i.e., typed back into another code cell). Chapter 11 introduces the concept of a text representation of an object, which is related to the semantic meaning of an object's value as discussed in Chapter 1
, and the angle brackets convention is one such way to represent an object as text. When executed, the angle brackets cause a
SyntaxError
because Python expects the <
operator to come with an operand on both sides (cf., Chapter 3 ).
<function __main__.average_evens(numbers)>
Cell In[31], line 1 <function __main__.average_evens(numbers)> ^ SyntaxError: invalid syntax
average_evens
is, of course, callable. So, the function
type is the third kind of callable in this chapter.
callable(average_evens)
True
The built-in help() function shows a function's docstring.
Whenever we use code to analyze or obtain information on an object, we say that we introspect it.
help(average_evens)
Help on function average_evens in module __main__: average_evens(integers) Calculate the average of all even numbers in a list. Args: integers (list of int's): whole numbers to be averaged Returns: average (float)
In JupyterLab, we can just as well add a question mark ?
to a function's name to achieve the same.
average_evens?
Signature: average_evens(integers) Docstring: Calculate the average of all even numbers in a list. Args: integers (list of int's): whole numbers to be averaged Returns: average (float) File: /tmp/ipykernel_152540/3598721284.py Type: function
Two question marks ??
show a function's source code.
average_evens??
Signature: average_evens(integers) Source: def average_evens(integers): """Calculate the average of all even numbers in a list. Args: integers (list of int's): whole numbers to be averaged Returns: average (float) """ evens = [n for n in integers if n % 2 == 0] average = sum(evens) / len(evens) return average File: /tmp/ipykernel_152540/3598721284.py Type: function
help() and the
?
s also work for built-ins.
help(sum)
Help on built-in function sum in module builtins: sum(iterable, /, start=0) Return the sum of a 'start' value (default: 0) plus an iterable of numbers When the iterable is empty, return the start value. This function is intended specifically for use with numeric values and may reject non-numeric types.
Once defined, we may call a function with the call operator ()
as often as we wish. The formal parameters are then filled in by passing expressions (e.g., literals or variables) as arguments to the function within the parentheses.
average_evens([7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4])
7.0
average_evens(numbers)
7.0
A function call's return value is commonly assigned to a variable for subsequent use. Otherwise, we lose access to the returned object right away.
result = average_evens(numbers)
result
7.0
The parameters listed in a function's definition (i.e., integers
in the example) and variables created inside it during execution (i.e., evens
and average
) are local to that function. That means they only reference an object in memory while the function is being executed and are dereferenced immediately when the function call returns. We say they go out of scope. That is why we see the NameError
s below.
integers
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[41], line 1 ----> 1 integers NameError: name 'integers' is not defined
evens
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[42], line 1 ----> 1 evens NameError: name 'evens' is not defined
average
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[43], line 1 ----> 1 average NameError: name 'average' is not defined
PythonTutor visualizes what happens in memory: To be precise, in the exact moment when the function call is initiated and
numbers
passed in as the integers
argument, there are two references to the same list
object (cf., steps 4-5 in the visualization). We also see how Python creates a new frame that holds the function's local scope (i.e., "internal names") in addition to the global frame. Frames are nothing but namespaces to isolate the names of different scopes from each other. The list comprehension
[n for n in integers if n % 2 == 0]
constitutes yet another frame that is in scope as the list
object assigned to evens
is being created (cf., steps 6-20). When the function returns, only the global frame is left (cf., last step).
On the contrary, while a function is being executed, it may reference the variables of enclosing scopes (i.e., "outside" of it). This is a common source of semantic errors. Consider the following stylized and incorrect example average_wrong()
. The error is hard to spot with eyes: The function never references the integers
parameter but the numbers
variable in the global scope instead.
def average_wrong(integers):
"""Calculate the average of all even numbers in a list.
Args:
integers (list of int's): whole numbers to be averaged
Returns:
average (float)
"""
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return average
numbers
in the global scope is, of course, not changed by merely defining average_wrong()
.
numbers
[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
Sometimes a function may return a correct solution for some inputs ...
average_wrong(numbers) # correct by accident
7.0
... but still be wrong in general.
average_wrong([123, 456, 789])
7.0
PythonTutor is again helpful at visualizing the error interactively: Creating the
list
object evens
eventually references takes 16 computational steps, namely two for managing the list comprehension, one for setting up an empty list
object, twelve for filling it with elements derived from numbers
in the global scope (i.e., that is the error), and one to make evens
reference it (cf., steps 6-21).
The frames logic shown by PythonTutor is the mechanism with which Python not only manages the names inside one function call but also for many potentially simultaneous calls, as revealed in Chapter 4 . It is the reason why we may reuse the same names for the parameters and variables inside both
average_evens()
and average_wrong()
without Python mixing them up. So, as we already read in the Zen of Python , "namespaces are one honking great idea" (cf.,
import this
), and a frame is just a special kind of namespace.
Code gets even more confusing when variables by the same name from different scopes collide. In particular, what should we expect to happen if a function "changes" a globally defined variable in its body?
average_evens()
below works like average_evens()
above except that it rounds the numbers in integers
with the built-in round() function before filtering and averaging them. round()
returns
int
objects independent of its argument being an int
or a float
object. On the first line in its body, average_evens()
introduces a local variable numbers
whose name collides with the one defined in the global scope.
def average_evens(integers):
"""Calculate the average of all even numbers in a list.
Args:
integers (list of int's/float's): numbers to be averaged;
if non-whole numbers are provided, they are rounded
Returns:
average (float)
"""
numbers = [round(n) for n in integers]
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return average
As a good practice, let's first "verify" that average_evens()
is "correct" by calling it with inputs for which we can calculate the answer in our heads. Treating a function as a "black box" (i.e., input-output specification) when testing is also called unit testing and plays an important role in modern software engineering.
average_evens([40.0, 41.1, 42.2, 43.3, 44.4])
42.0
Such tests are often and conveniently expressed with the assert
statement (cf., reference ): If the expression following
assert
evaluates to True
, nothing happens.
assert average_evens([40.0, 41.1, 42.2, 43.3, 44.4]) == 42.0
However, if the expression evaluates to False
, an AssertionError
is raised.
assert average_evens([40.0, 41.1, 42.2, 43.3, 44.4]) == 87.0
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) Cell In[51], line 1 ----> 1 assert average_evens([40.0, 41.1, 42.2, 43.3, 44.4]) == 87.0 AssertionError:
Calling average_evens()
leaves numbers
in the global scope unchanged.
numbers
[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
To add to the confusion, let's also pass the global numbers
list as an argument to average_evens()
. The return value is the same as before.
average_evens(numbers)
7.0
In summary, Python is smart enough to keep all the involved numbers
variables apart. So, the global numbers
variable is still referencing the same list
object as before.
numbers
[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
The reason why everything works is that every time we (re-)assign an object to a variable inside a function's body with the =
statement, this is done in the local scope by default. There are ways to change variables existing in an outer scope from within a function, but this is a rather advanced topic.
PythonTutor shows how two
numbers
variables exist in different scopes referencing different objects (cf., steps 14-25) when we execute average_evens([40.0, 41.1, 42.2, 43.3, 44.4])
.
Variables whose names collide with the ones of variables in enclosing scopes - and the global scope is just the most enclosing scope - are said to shadow them.
While this is not a problem for Python, it may lead to less readable code for humans and should be avoided if possible. But, as the software engineering wisdom goes, "naming things" is often considered a hard problem as well, and we have to be prepared to encounter shadowing variables.
Shadowing also occurs if a parameter in the function definition goes by the same name as a variable in an outer scope. Below, average_evens()
is identical to the first version in this chapter except that the parameter integers
is now called numbers
as well.
def average_evens(numbers):
"""Calculate the average of all even numbers in a list.
Args:
numbers (list of int's): whole numbers to be averaged
Returns:
average (float)
"""
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return average
average_evens(numbers)
7.0
numbers
[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
PythonTutor reveals that in this example there are two
numbers
variables in different scope referencing the same list
object in memory (cf., steps 4-23).
So far, we have specified only one parameter in each of our user-defined functions. In Chapter 1 , however, we saw the built-in divmod()
function take two arguments. And, the order in which they are passed in matters! Whenever we call a function and list its arguments in a comma separated manner, we say that we pass in the arguments by position or refer to them as positional arguments.
divmod(42, 10)
(4, 2)
divmod(10, 42)
(0, 10)
For many functions, there is a natural order to the arguments: For example, for any kind of division passing the dividend first and the divisor second seems intuitive. But what if that is not the case in another setting? For example, let's create a close relative of the above average_evens()
function that also scales the resulting average by a factor. What is more natural? Passing in numbers
first? Or scalar
? There is no obvious way and we continue with the first alternative for no concrete reason.
def scaled_average_evens(numbers, scalar):
"""Calculate the scaled average of all even numbers in a list.
Args:
numbers (list of int's/float's): numbers to be averaged;
if non-whole numbers are provided, they are rounded
scalar (float): multiplies the average
Returns:
scaled_average (float)
"""
numbers = [round(n) for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return scalar * average
As with divmod() , we may pass in the arguments by position.
scaled_average_evens(numbers, 2)
14.0
Now, this function call is a bit harder to understand as we always need to remember what the 2
means. This becomes even harder with more parameters.
Luckily, we may also pass in arguments by name. Then, we refer to them as keyword arguments.
scaled_average_evens(numbers=numbers, scalar=2)
14.0
When passing all arguments by name, we may do so in any order.
scaled_average_evens(scalar=2, numbers=numbers)
14.0
We may even combine positional and keyword arguments in the same function call.
scaled_average_evens(numbers, scalar=2)
14.0
Unfortunately, there are ways to screw this up with a SyntaxError
: If positional and keyword arguments are mixed, the keyword arguments must come last.
scaled_average_evens(numbers=numbers, 2)
Cell In[65], line 1 scaled_average_evens(numbers=numbers, 2) ^ SyntaxError: positional argument follows keyword argument
Similarly, we must always pass in the right number of arguments. Otherwise, a TypeError
is raised.
scaled_average_evens(numbers)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[66], line 1 ----> 1 scaled_average_evens(numbers) TypeError: scaled_average_evens() missing 1 required positional argument: 'scalar'
scaled_average_evens(numbers, 2, 3)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[67], line 1 ----> 1 scaled_average_evens(numbers, 2, 3) TypeError: scaled_average_evens() takes 2 positional arguments but 3 were given
Defining average_evens()
and scaled_average_evens()
as above leads to a repetition of most of their code. That is not good as such a redundancy makes a code base hard to maintain in the long run: Whenever we change the logic in one function, we must not forget to do so for the other function as well. And, most likely, we forget about such issues in larger projects.
Below, three of four lines in the functions' bodies are identical!
def average_evens(numbers):
""" ... ... ... """
numbers = [round(n) for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return average
def scaled_average_evens(numbers, scalar):
""" ... ... ... """
numbers = [round(n) for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return scalar * average
A better way is to design related functions in a modular fashion such that they reuse each other's code.
For example, as not scaling an average is just a special case of scaling it with 1
, we could redefine the two functions like below: In this version, the function resembling the special case, average_evens()
, forwards the call to the more general function, scaled_average_evens()
, passing a scalar
argument of 1
. As the name scaled_average_evens
within the body of average_evens()
is looked up each time the function is being executed, we may define average_evens()
before scaled_average_evens()
.
def average_evens(numbers):
""" ... ... ... """
return scaled_average_evens(numbers, scalar=1)
def scaled_average_evens(numbers, scalar):
""" ... ... ... """
numbers = [round(n) for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return scalar * average
After refactoring the functions, it is a good idea to test them again.
assert average_evens(numbers) == 7.0
assert scaled_average_evens(numbers, 2) == 14.0
Assuming that scaling the average occurs rarely, it may be a good idea to handle both cases in one function definition by providing a default argument of 1
for the scalar
parameter.
def average_evens(numbers, scalar=1):
"""Calculate the average of all even numbers in a list.
Args:
numbers (list of int's/float's): numbers to be averaged;
if non-whole numbers are provided, they are rounded
scalar (float, optional): multiplies the average; defaults to 1
Returns:
scaled_average (float)
"""
numbers = [round(n) for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return scalar * average
Now, we call the function with or without passing a scalar
argument.
If scalar
is not passed in, it automatically takes the value 1
.
average_evens(numbers)
7.0
If scalar
is passed in, this may be done as either a positional or a keyword argument. Which of the two calls where scalar
is 2
is faster to understand in a larger program?
average_evens(numbers, 2)
14.0
average_evens(numbers, scalar=2)
14.0
Because we assumed that scaling occurs rarely, we would prefer that our new version of average_evens()
be called with a keyword argument whenever scalar
is passed in. Then, a function call is never ambiguous when reading the source code.
Python offers a keyword-only syntax when defining a function that forces a caller to pass the scalar
argument by name if it is passed in at all: To do so, we place an asterisk *
before the arguments that may only be passed in by name. Note that the keyword-only syntax also works without a default argument.
def average_evens(numbers, *, scalar=1):
"""Calculate the average of all even numbers in a list.
Args:
numbers (list of int's/float's): numbers to be averaged;
if non-whole numbers are provided, they are rounded
scalar (float, optional): multiplies the average; defaults to 1
Returns:
scaled_average (float)
"""
numbers = [round(n) for n in numbers]
evens = [n for n in numbers if n % 2 == 0]
average = sum(evens) / len(evens)
return scalar * average
As before, we may call average_evens()
without passing in an argument for the scalar
parameter.
average_evens(numbers)
7.0
If we call average_evens()
with a scalar
argument, we must use keyword notation.
average_evens(numbers, scalar=2)
14.0
If instead we pass in scalar
as a positional argument, we get a TypeError
.
average_evens(numbers, 2)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[81], line 1 ----> 1 average_evens(numbers, 2) TypeError: average_evens() takes 1 positional argument but 2 were given
The def
statement is a statement because of its side effect of creating a new name that references a new function
object in memory.
We can thus think of it as doing two things atomically (i.e., either both of them happen or none). First, a function
object is created that contains the concrete 0s and 1s that resemble the instructions we put into the function's body. In the context of a function, these 0s and 1s are also called byte code . Then, a name referencing the new
function
object is created.
Only this second aspect makes def
a statement: Merely creating a new object in memory without making it accessible for later reference does not constitute a side effect because the state the program is not changed. After all, if we cannot reference an object, how do we know it exists in the first place?
Python provides a lambda
expression syntax that allows us to only create a function
object in memory without making a name reference it (cf., reference ). It starts with the keyword
lambda
followed by an optional listing of comma separated parameters, a mandatory colon, and one expression that serves as the return value of the resulting function
object. Because it does not create a name referencing the object, we effectively create "anonymous" functions with it.
In the example, we create a function
object that adds 3
to the only argument passed in as the parameter x
and returns that sum.
lambda x: x + 3
<function __main__.<lambda>(x)>
If you think this is rather pointless to do, you are absolutely correct!
We created a function
object, dit not call it, and Python immediately forgot about it. So what's the point?
To inspect the object created by a lambda
expression, we use the simple =
statement and assign it to the variable add_three
, which is really add_three()
as per our convention from above.
add_three = lambda x: x + 3 # we could and should use def instead
type() and callable()
confirm that
add_three
is indeed a callable function
object.
type(add_three)
function
callable(add_three)
True
Now we may call add_three()
as if we defined it with the def
statement.
add_three(39)
42
Alternatively, we could call an function
object created with a lambda
expression right away (i.e., without assigning it to a variable), which looks quite weird for now as we need two pairs of parentheses: The first one serves as a delimiter whereas the second represents the call operator.
(lambda x: x + 3)(39)
42
The main point of having functions without a reference to them is to use them in a situation where we know ahead of time that we use the function only once.
Popular applications of lambda expressions occur in combination with the map-filter-reduce paradigm (cf., Chapter 8 ).