Functions#
What you will learn in this lesson:
Defining and calling functions
Parameters and arguments
Return values
Scope and lifetime of variables
Introduction#
Functions take input, perform a specific task and, optionally, produce an output. They contain a block of code to do their work.
Functions can return a single value, multiple values, or even no value at.
Why do we use functions?
Code economy
With functions, you can keep your code short and concise. Once a function is defined, it can be used as many times as needed, which is great to not need to write the same code over and over. In addition, functions help your code be more readable. For example, if you give a function a well-chosen name, anyone could read your code, and already infer what it does.
Other forms of code economy is through modules and packages, which is you a way of grouping your code (e.g. functions).
Parametrization
Functions accept parameters. Therefore, one can study different function’s behaviors by changing the different parameters.
Production
We can use functions and the fact that they return one or multiple values for production.
Built-in functions#
Python provides many built-in functions. You can find a complete list here: Python built-in functions
We have already seen examples of some built-in functions, such asprint()
, id()
, isinstance()
, enumerate()
and zip()
.
Another example is bool()
, which takes an argument (e.g. a variable) and returns True
or False
# set a variable and pass into a conditional statement
x = 3
bool(x < 4)
True
bool(x >= 4)
False
Or the function help()
, which provides a help information of any passed object:
help(bool)
Help on class bool in module builtins:
class bool(int)
| bool(x) -> bool
|
| Returns True when the argument x is true, False otherwise.
| The builtins True and False are the only two instances of the class bool.
| The class bool is a subclass of the class int, and cannot be subclassed.
|
| Method resolution order:
| bool
| int
| object
|
| Methods defined here:
|
| __and__(self, value, /)
| Return self&value.
|
| __or__(self, value, /)
| Return self|value.
|
| __rand__(self, value, /)
| Return value&self.
|
| __repr__(self, /)
| Return repr(self).
|
| __ror__(self, value, /)
| Return value|self.
|
| __rxor__(self, value, /)
| Return value^self.
|
| __xor__(self, value, /)
| Return self^value.
|
| ----------------------------------------------------------------------
| Static methods defined here:
|
| __new__(*args, **kwargs)
| Create and return a new object. See help(type) for accurate signature.
|
| ----------------------------------------------------------------------
| Methods inherited from int:
|
| __abs__(self, /)
| abs(self)
|
| __add__(self, value, /)
| Return self+value.
|
| __bool__(self, /)
| True if self else False
|
| __ceil__(...)
| Ceiling of an Integral returns itself.
|
| __divmod__(self, value, /)
| Return divmod(self, value).
|
| __eq__(self, value, /)
| Return self==value.
|
| __float__(self, /)
| float(self)
|
| __floor__(...)
| Flooring an Integral returns itself.
|
| __floordiv__(self, value, /)
| Return self//value.
|
| __format__(self, format_spec, /)
| Default object formatter.
|
| __ge__(self, value, /)
| Return self>=value.
|
| __getattribute__(self, name, /)
| Return getattr(self, name).
|
| __getnewargs__(self, /)
|
| __gt__(self, value, /)
| Return self>value.
|
| __hash__(self, /)
| Return hash(self).
|
| __index__(self, /)
| Return self converted to an integer, if self is suitable for use as an index into a list.
|
| __int__(self, /)
| int(self)
|
| __invert__(self, /)
| ~self
|
| __le__(self, value, /)
| Return self<=value.
|
| __lshift__(self, value, /)
| Return self<<value.
|
| __lt__(self, value, /)
| Return self<value.
|
| __mod__(self, value, /)
| Return self%value.
|
| __mul__(self, value, /)
| Return self*value.
|
| __ne__(self, value, /)
| Return self!=value.
|
| __neg__(self, /)
| -self
|
| __pos__(self, /)
| +self
|
| __pow__(self, value, mod=None, /)
| Return pow(self, value, mod).
|
| __radd__(self, value, /)
| Return value+self.
|
| __rdivmod__(self, value, /)
| Return divmod(value, self).
|
| __rfloordiv__(self, value, /)
| Return value//self.
|
| __rlshift__(self, value, /)
| Return value<<self.
|
| __rmod__(self, value, /)
| Return value%self.
|
| __rmul__(self, value, /)
| Return value*self.
|
| __round__(...)
| Rounding an Integral returns itself.
|
| Rounding with an ndigits argument also returns an integer.
|
| __rpow__(self, value, mod=None, /)
| Return pow(value, self, mod).
|
| __rrshift__(self, value, /)
| Return value>>self.
|
| __rshift__(self, value, /)
| Return self>>value.
|
| __rsub__(self, value, /)
| Return value-self.
|
| __rtruediv__(self, value, /)
| Return value/self.
|
| __sizeof__(self, /)
| Returns size in memory, in bytes.
|
| __sub__(self, value, /)
| Return self-value.
|
| __truediv__(self, value, /)
| Return self/value.
|
| __trunc__(...)
| Truncating an Integral returns itself.
|
| as_integer_ratio(self, /)
| Return integer ratio.
|
| Return a pair of integers, whose ratio is exactly equal to the original int
| and with a positive denominator.
|
| >>> (10).as_integer_ratio()
| (10, 1)
| >>> (-10).as_integer_ratio()
| (-10, 1)
| >>> (0).as_integer_ratio()
| (0, 1)
|
| bit_count(self, /)
| Number of ones in the binary representation of the absolute value of self.
|
| Also known as the population count.
|
| >>> bin(13)
| '0b1101'
| >>> (13).bit_count()
| 3
|
| bit_length(self, /)
| Number of bits necessary to represent self in binary.
|
| >>> bin(37)
| '0b100101'
| >>> (37).bit_length()
| 6
|
| conjugate(...)
| Returns self, the complex conjugate of any int.
|
| to_bytes(self, /, length=1, byteorder='big', *, signed=False)
| Return an array of bytes representing an integer.
|
| length
| Length of bytes object to use. An OverflowError is raised if the
| integer is not representable with the given number of bytes. Default
| is length 1.
| byteorder
| The byte order used to represent the integer. If byteorder is 'big',
| the most significant byte is at the beginning of the byte array. If
| byteorder is 'little', the most significant byte is at the end of the
| byte array. To request the native byte order of the host system, use
| `sys.byteorder' as the byte order value. Default is to use 'big'.
| signed
| Determines whether two's complement is used to represent the integer.
| If signed is False and a negative integer is given, an OverflowError
| is raised.
|
| ----------------------------------------------------------------------
| Class methods inherited from int:
|
| from_bytes(bytes, byteorder='big', *, signed=False)
| Return the integer represented by the given array of bytes.
|
| bytes
| Holds the array of bytes to convert. The argument must either
| support the buffer protocol or be an iterable object producing bytes.
| Bytes and bytearray are examples of built-in objects that support the
| buffer protocol.
| byteorder
| The byte order used to represent the integer. If byteorder is 'big',
| the most significant byte is at the beginning of the byte array. If
| byteorder is 'little', the most significant byte is at the end of the
| byte array. To request the native byte order of the host system, use
| `sys.byteorder' as the byte order value. Default is to use 'big'.
| signed
| Indicates whether two's complement is used to represent the integer.
|
| ----------------------------------------------------------------------
| Data descriptors inherited from int:
|
| denominator
| the denominator of a rational number in lowest terms
|
| imag
| the imaginary part of a complex number
|
| numerator
| the numerator of a rational number in lowest terms
|
| real
| the real part of a complex number
Creating and calling a function#
Let’s show how to create and call functions with an example. In this case, the function compares a list of values against a threshold.
def vals_greater_than_or_equal_to_threshold(vals, thresh):
'''
PURPOSE: Given a list of values, compare each value against a threshold
INPUTS
vals list of ints or floats
thresh int or float
OUTPUT
bools list of booleans
'''
bools = [val >= thresh for val in vals] # List comprehension
return bools
This function exhibits the common components of a function. Let’s break them down in detail:
- The function definition starts with
def
, followed by name, one or more parameters in parenthesis, and then a colon. - Then it comes the body of the function. This should be indented, so Python knows that the following piece of code belongs to the function. The body of a function usually consists of:
- First, a
docstring
to provide annotation (optional). - Second, the bulk of function.
- Lastly, a
return
statement (optional).
After its definition, we can call the function to use it. To do so, we simply write the function name followed by the required arguments in parentheses.
Let’s test our function:
# validate that it works for ints
x = [3, 4]
thr = 4
vals_greater_than_or_equal_to_threshold(x, thr)
[False, True]
# validate that it works for floats
x = [3.0, 4.2]
thr = 4.2
vals_greater_than_or_equal_to_threshold(x, thr)
[False, True]
This gives correct results and does exactly what we want.
Arguments and parameters#
Parameters and arguments are both inputs of the functions, but is there any difference between them?
In theory, parameters are the variables listed inside the parentheses in the function definition, whereas arguments are the actual values that you pass into the function when calling it.
For example, in vals_greater_than_or_equal_to_threshold
, “vals” and “thresh” would be the parameters, while [3.0, 4.2]
and 4.2
, as shown in one of the examples above, are the arguments.
At the end of the day, people often use both terms interchangeably, so don’t worry too much about this technicality :)
Guidelines when passing arguments:#
Functions need to be called with the correct number of parameters.
## function requiring 2 parameters
def fcn_bad_args(x, y):
return x+y
This function requires two parameters, so calling the function in the following way will give an error:
# function call with only 1 of the 2 arguments
fcn_bad_args(10)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 2
1 # function call with only 1 of the 2 arguments
----> 2 fcn_bad_args(10)
TypeError: fcn_bad_args() missing 1 required positional argument: 'y'
When calling a function, the order of arguments matters. These types of arguments are called positional arguments.
x = 1
y = 2
# function with order of x then y
def fcn_swapped_args(x, y):
out = 5 * x + y
return out
# call function in correct order
print('fcn_swapped_args(x,y) =', fcn_swapped_args(x,y))
# call function in incorrect order
print('fcn_swapped_args(y,x) =', fcn_swapped_args(y,x))
fcn_swapped_args(x,y) = 7
fcn_swapped_args(y,x) = 11
Generally it’s best to keep parameters in order.
foo = 1
bar = 2
fcn_swapped_args(foo, bar)
# works even though function was written as fcn_swapped_arg(x, y)
7
You can pass arguments in any order if you use keyword arguments. In this case, you specify the parameter name followed by the value it should take.
x1 = 1
y1 = 2
# call parameter names in function call.
fcn_swapped_args(y=y1, x=x1)
7
4.You can combine positional and keyword arguments, but positional arguments must come before keyword arguments in the function call.
# All positional arguments
print(fcn_swapped_args(1, 2))
# All keyword arguments
print(fcn_swapped_args(y=2, x=1))
# One positional and one keyword argument
print(fcn_swapped_args(1, y=2))
7
7
7
# This will fail, because keywork argument precedes a positional argument
print(fcn_swapped_args(x=1, 2))
Cell In[13], line 2
print(fcn_swapped_args(x=1, 2))
^
SyntaxError: positional argument follows keyword argument
(Advanced) You can use an asterisk (
*
) before certain parameters to force them to be passed as keyword-only arguments.
# The same function as above, but forcing y to be passed as keyword argument
def fcn_force_keywords(x, *, y):
out = 5 * x + y
return out
# This will fail
print(fcn_force_keywords(1, 2))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[15], line 2
1 # This will fail
----> 2 print(fcn_force_keywords(1, 2))
TypeError: fcn_force_keywords() takes 1 positional argument but 2 were given
# We need to pass y as a keyword
print(fcn_force_keywords(1, y=2))
7
# x can be passed also as keyword, but it is optional
print(fcn_force_keywords(x=1, y=2))
7
Default Arguments#
Default arguments set the value when an argument is unspecified.
def show_results(precision, printing=True):
precision = round(precision, 2)
if printing:
print('precision =', precision)
return precision
pr = 0.912
# The function call didn't specify `printing`, so it defaulted to True.
res = show_results(pr)
precision = 0.91
# We can specify it to False here
res = show_results(pr, False)
Default arguments must follow non-default arguments. This causes trouble:
def show_results(precision, printing=True, uhoh):
precision = round(precision, 2)
if printing:
print('precision =', precision)
return precision
Cell In[21], line 1
def show_results(precision, printing=True, uhoh):
^
SyntaxError: non-default argument follows default argument
Packing and Unpacking arguments#
The *
operator (for tuples) and **
operator (for dictionaries) can be used to pack and unpack arguments when calling a function.
Packing#
Packing allows us to pass an arbitrary number of values to a function.
For example, with tuples:
def show_arg_expansion(*models):
print("models :", models)
print("input arg type :", type(models))
print("input arg length:", len(models))
print("-----------------------------")
for mod in models:
print(mod)
Now you can pass a tuple of an arbitrary number of values to the function…
show_arg_expansion("logreg", "naive_bayes")
models : ('logreg', 'naive_bayes')
input arg type : <class 'tuple'>
input arg length: 2
-----------------------------
logreg
naive_bayes
show_arg_expansion("logreg", "naive_bayes", "gbm")
models : ('logreg', 'naive_bayes', 'gbm')
input arg type : <class 'tuple'>
input arg length: 3
-----------------------------
logreg
naive_bayes
gbm
Example with print
function, which accepts any arbitrary number of arguments to print.
help(print)
Help on built-in function print in module builtins:
print(*args, sep=' ', end='\n', file=None, flush=False)
Prints the values to a stream, or to sys.stdout by default.
sep
string inserted between values, default a space.
end
string appended after the last value, default a newline.
file
a file-like object (stream); defaults to the current sys.stdout.
flush
whether to forcibly flush the stream.
print("Hello class.", "This is your instructor.", "We are in DS", 1002)
Hello class. This is your instructor. We are in DS 1002
With dictionary, you should use **
def show_entries(**entries):
# kwargs is a dict
print(type(entries))
# Printing dictionary items
for key in entries:
print("%s = %s" % (key, entries[key]))
# Somebody's info:
show_entries(name="Javier", ID="1234", language="Python")
<class 'dict'>
name = Javier
ID = 1234
language = Python
# Again, we can pass an arbitrary number of entries
show_entries(name="Javier", ID="1234", language="Python", team = "Athletic Club")
<class 'dict'>
name = Javier
ID = 1234
language = Python
team = Athletic Club
Unpacking#
You can use the *
operator to unpack list-like objects when passing them to a function that specifies its arguments. In this case, the arguments are passed as positional arguments.
def arg_expansion_example(x, y):
return x**y
my_args = [2, 8]
arg_expansion_example(*my_args)
256
But, the passed object must be the right length.
my_args2 = [2, 8, 5]
arg_expansion_example(*my_args2)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[30], line 2
1 my_args2 = [2, 8, 5]
----> 2 arg_expansion_example(*my_args2)
TypeError: arg_expansion_example() takes 2 positional arguments but 3 were given
And since these are positional arguments, be aware of the order!
my_args = [2, 8]
print(arg_expansion_example(*my_args))
my_args = [8, 2]
print(arg_expansion_example(*my_args))
256
64
You can also use the **
operator to unpack a dictionary of arguments. In this case, the arguments are passed as keyword arguments.
my_args_dict = {"x": 2, "y": 8}
arg_expansion_example(**my_args_dict)
256
Returning Values#
Functions are not required to have return statement. If there is no return statement, function returns
None
object.Functions can return no value (
None
object), one value, or many.Any Python object can be returned.
Functions may have no return statement
# returns None, and prints.
def fcn_nothing_to_return(x, y):
out = 'nothing to see here!'
print(out)
r = fcn_nothing_to_return(1, 1)
print(r)
nothing to see here!
None
For clarity purposes, it’s generally a good idea to include return statements, even if not returning a value. You can use return
or return None
.
# returns None, and prints.
def fcn_nothing_to_return(x, y):
out = 'nothing to see here!'
print(out)
return None
r = fcn_nothing_to_return(1, 1)
print(r)
nothing to see here!
None
Functions may return more than one output
# returns three values
def negate_coords(x, y, z):
return -x, -y, -z
a,b,c = negate_coords(10,20,30)
print('a=', a)
print('b=', b)
print('c=', c)
a= -10
b= -20
c= -30
_
d,e,_ = negate_coords(10,20,30)
print('d=', d)
print('e=', e)
d= -10
e= -20
Functions can contain multiple return statements
# For non-negative values, the first `return` is reached.
# For negative values, the second `return` is reached.
def absolute_value(num):
if num >= 0:
return num
return -num
absolute_value(-4)
4
absolute_value(4)
4
Variable Scope#
A variable’s scope refers to the part of a program where it is visible.
In this context, visible means available or usable.
If a variable is in scope within a function, it is visible to the function.
If it is out of scope for a function, it is not visible to the function.
When a variable is defined inside a function, it is not visible outside of that function.
We say such variables are local to the function.
These variables are also removed from memory when the function completes.
def show_scope(x):
x = 10*x
z = 4
print('z inside function =', z)
print('memory address of z inside function =', hex(id(z)))
return x
# This code recognizes z from inside the function.
show_scope(6)
z inside function = 4
memory address of z inside function = 0x87f448
60
# Calling it from outside, where it isn't defined, throws an error.
print('z =', z)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[45], line 2
1 # Calling it from outside, where it isn't defined, throws an error.
----> 2 print('z =', z)
NameError: name 'z' is not defined
If we define z
and call the function, the update to z
won’t pass outside the function.
z = 2
print('z outside:', hex(id(z)))
out = show_scope(6)
print('z = ', z)
z outside: 0x87f408
z inside function = 4
memory address of z inside function = 0x87f448
z = 2
Local versus Global Variables#
It’s important to have a good understanding of local versus global variables; otherwise, you may encounter unexpected behavior and confusion. Let’s explore this with several examples.
Example 1: Variable defined outside function, used inside function
In the code below:
x
is global and seen from inside the function.
r
is local to the function.
x = 10
def fcn(r):
out = x + r
return(out)
print(fcn(6)) # works
16
print(r)
None
Example 2: Variable defined outside function, updated and used inside function
fcn
uses the local version of x
x = 10
def fcn(a):
x = 20
suma = x + a
print(f"x from fcn: {x}")
return(suma)
print(f"fcn(6): {fcn(6)}")
print("x outside fcn: {}".format(x))
x from fcn: 20
fcn(6): 26
x outside fcn: 10
Example 3: Variable defined outside function. Inside function, print variable, update, and use
This one may be confusing. It fails!
Python treats x
inside function as the local x
.
The print
occurs before x
is assigned, so it can’t find x
.
x = 10
def fcn(a):
print('x from fcn, before update:', x)
x = 20
out = x + a
print('x from fcn, after update:', x)
return(out)
print('fcn(6):', fcn(6))
print('x outside fcn:', x)
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Cell In[53], line 10
7 print('x from fcn, after update:', x)
8 return(out)
---> 10 print('fcn(6):', fcn(6))
11 print('x:', x)
Cell In[53], line 4, in fcn(a)
3 def fcn(a):
----> 4 print('x from fcn, before update:', x)
5 x = 20
6 out = x + a
UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
The error can be fixed by referencing x
as global
inside the function.
This is only necessary if we wish to reassign the variable.
It can also be useful when we want several functions to operate on the same variable.
However, be cautious with this approach, as it may affect the behavior of your program globally.
x = 10
def fcn(a):
global x # add this to reference global x outside function
print('x from fcn, before update:', x)
x = 20
out = x + a
print('x from fcn, after update:', x)
return(out)
print('fcn(6):', fcn(6))
print('x outside fcn:', x)
x from fcn, before update: 10
x from fcn, after update: 20
fcn(6): 26
x outside fcn: 20
Docstring#
A
docstring
is a string that occurs as first statement in module, function, class, or method definitionSaved in
__doc__
attributeNeeds to be indented
'''enclosed in triple quotes like this'''
We gave functions a descriptive docstring to: (1) explain its purpose, and (2) name each input and output, and give their data types
Let’s look at an example from the beginning of this lesson to see how this works:
def vals_greater_than_or_equal_to_threshold(vals, thresh):
'''
PURPOSE: Given a list of values, compare each value against a threshold
INPUTS
vals list of ints or floats
thresh int or float
OUTPUT
bools list of booleans
'''
bools = [val >= thresh for val in vals] # List comprehension
return bools
# Here is the documentation
vals_greater_than_or_equal_to_threshold.__doc__
'\n PURPOSE: Given a list of values, compare each value against a threshold\n\n INPUTS\n vals list of ints or floats\n thresh int or float\n\n OUTPUT\n bools list of booleans\n '
To print this info in a readable way, use print
print(vals_greater_than_or_equal_to_threshold.__doc__)
PURPOSE: Given a list of values, compare each value against a threshold
INPUTS
vals list of ints or floats
thresh int or float
OUTPUT
bools list of booleans
help
function can also give you this info and more:
help(vals_greater_than_or_equal_to_threshold)
Help on function vals_greater_than_or_equal_to_threshold in module __main__:
vals_greater_than_or_equal_to_threshold(vals, thresh)
PURPOSE: Given a list of values, compare each value against a threshold
INPUTS
vals list of ints or floats
thresh int or float
OUTPUT
bools list of booleans
Tips for creating good functions#
Design a function to do one thing.
Keep your function as simple as possible. This makes it more understandable, easier to maintain, and reusable.
Give your function a meaningful name. What makes a good function name?
- It should clearly describe the action it performs.
- Be consistent with naming conventions.
- For example, a name like `compute_variances_sort_save_print` suggests the function is doing too much!
- If a function like `compute_variances` also generates plots and updates variables, it can cause confusion.
- Always aim to provide your function with a docstring.
In the future, as you advance in your Python skills, consider including variable and function type annotations. This is optional but useful (see more info https://docs.python.org/3/library/typing.html).
Practice exercises#
1.1 Write a function with these requirements:
It should have a sensible name.
It should contain a docstring.
It should take two inputs: a string, and an integer.
It should return True if the string length is equal to the integer, otherwise False.
1.2 Call the function, passing inputs:
- "is this text the right length?" for the string.
- 30 for the integer.
Verify the output is True.
Test other combinations.
# Start here your answers
Write a function with these requirements:
It should take *args for the input argument, so we can later pass as many arguments as we want.
It should squares each argument, printing the value. You can use a for loop for this.
It should not return any value
Next, call the function, passing at least two integers. Try other combinations too.
# Start here your answers