P05: Functions¶
Concepts: abstraction, functions, scope, mutable/immutable
Python functions: writing, scope, pass by reference implications
Intro to functions¶
A function is a re-usable piece of code that performs operations on a specified set of variables, and returns the result.
If you ever find yourself writing out (or copying/pasting) the same bit of code more than once, you should consider making it a function that you can call whenever you need to perform a given operation…
Saves a ton of time and reduces the chance of making an error
Modular Programming
Modular programming is an approach to programming that focuses on building programs from independent modules (‘pieces’).
defining a function
def
return
executing a function
parameters
local and global namespaces
Functions for Modular Programming¶
Functions allow us to flexibly re-use pieces of code
Each function is independent of every other function, and other pieces of code
Functions are the building blocks of programs, and can be flexibly combined and executed in specified orders
This allows us to build up arbitrarily complex, well-organized programs
# you've seen functions before
# recall that built in functions like print()
# will turn green when you type them
my_var = [3, 4, 5]
# print the second item to the end
print(my_var[1:])
[4, 5]
Creating your own function¶
When you use
def
you are creating a user-defined function.After
def
you give the function a name (something meaningful)Then you have
()
where you can pass arguments into the functionThese input arguments contain information that the function needs to produce the desired output
Remember to
return
the output of the function! Sometimes a function can modify an object in place, but far more often the function will do some operation and then you need to return the output of that operation so that you can use it elsewhere in your codeRemember to run the code cell that contains the function…if you don’t run the code cell then it won’t be available to call from other code cells in your code.
# define a function named double_value
# it will take a number and double the value
# num is the input parameter for the function
def double_value(num):
# double the value of num
doubled = num + num
# return output from function
# this is super important!
# if you don't return the value
# then nothing will actually
# happen
return doubled
Keyword arguments when excecuting a function¶
Note that built-in python functions turn green when you type them, but your custom made function will not
You can specify the input using keyword arguments where you explicitly assign the input argument by name (e.g.
double_value(num=6)
where you explicitly definenum=6
)
double_value(num=6)
12
Positional arguments when excecuting a function¶
You can also pass arguments by position (positional arguments) in which case the order of entry determines what values get assigned to each input argument.
Here we only have one argument (
num
) so there is no chance for confusion, but if you have multiple arguments in a more complex function then it is essential that you get the input order correct when using positional arguments and it is a good habit to use keyword arguments…more on this below
double_value(6)
12
Creating a function with multiple inputs¶
Something slightly more interesting than just adding a value with itself
This function will add two numbers together and then it will
return
the outputNote that it has two input arguments
num1
andnum2
Remember to run the function so that you
def add_two_numbers(num1, num2):
# Add the two input variables together
# and assign to the varible answer
answer = num1 + num2
# Return answer
return answer
Call the function using keyword arguments¶
output = add_two_numbers(num1=1, num2=2)
print(output)
3
Call the function using positional arguments¶
output = add_two_numbers(-1, 4)
print(output)
3
Positional vs. Keyword Arguments…be careful!¶
If you have multiple input arguments and you pass by position, then you have to get the order of the arguments correct…otherwise unexpected errors can happen (and they might not explicitly throw an error, so they can be hard to spot!)
Thus, if you have multiple input arguments it is good practice to pass by keyword instead of position
When passing by keyword, the order doesn’t matter, so long as you have the correct keywords
def exp(num, exponent):
return num ** exponent
Call by positional arguments: 2**3, so expected output is 8¶
exp(2, 3)
8
Call by keyword arguments: expected value is 8¶
exp(num=2, exponent=3)
8
Call by positional arguments: expected value is 8 but suppose you forgot that num comes first and exponent comes second…expected result is still 8 but you get 9 instead (i.e. 3**2 = 9)¶
exp(3, 2)
9
So even though its bad practice to flip the order of parameters, passing by keyword will save you and produce the expected result¶
exp(exponent=3, num=2)
8
Note: once you have a keyword argument, you can’t have other positional arguments afterwards¶
exp(num=2, 3)
File "/var/folders/8y/scw4yk6d3g99kqbs49wp8q3r0000gn/T/ipykernel_22073/4058479434.py", line 1
exp(num=2, 3)
^
SyntaxError: positional argument follows keyword argument
Setting a default value for parameters¶
You sometimes want to set a default value for a parameter…this is allowed during function definition
def exp(num, exponent=2):
return num ** exponent
Multiple operations in a single function¶
We aren’t limited to a single operation within a function.
Instead, we can use multiple operations and all of the concepts we’ve used previously (including loops and conditionals).
# determine if a value is even or odd
def even_odd(value):
if (value % 2 == 0):
out = "even"
else:
out = "odd"
return out
# Execute our function
even_odd(-1)
'odd'
With functions, the logic behind our code no longer requires it to be executed from top to bottom of the notebook.
The cost of potential confusion is definitely offset by the benefits of writing functions and using modular code.
Function Properties¶
Functions are defined using
def
followed by:
, which opens a code-block that comprises the functionRunning code with a
def
block defines the function (but does not execute it)
Functions are executed using parentheses -
()
This is when the code inside a function is actually run
Inside a function, there is code that performs operations on the available variables
Functions use the special operator
return
to exit the function, passing out any specified variables
When you use a function, you can assign the output (whatever is
return
ed) to a variable
Global and local namespaces¶
Functions have their own namespace, called the local namespace, and they only have access to variables explicitly passed into them
If a variable is declared or assigned inside of function, that variable only exists during the executation of the function.
These local variables are not visible or usable outside of the function
In contrast, variables declared outside of a function are in the global namespace. This is what we’ve been doing thus far in the course, and global variables are accessible from anywhere in your notebook
# Remember, you can check defined variables with `%whos`
%whos
Variable Type Data/Info
---------------------------------------
add_two_numbers function <function add_two_numbers at 0x7fbfaa0200d0>
double_value function <function double_value at 0x7fbfaa020040>
even_odd function <function even_odd at 0x7fbfaa0201f0>
exp function <function exp at 0x7fbfaa020310>
my_var list n=3
output int 3
Names defined inside a function only exist within the function¶
In this example, we’re using a function that has a local variable
x
# write a function that subtracts two numbers
def sub(num1, num2):
# assign the difference to the variable x
x = num1 - num2
# return x
return x
Now let’s define a variable x
in a normal code cell¶
Since
x
is defined outside of a fucntion, it is a global variable that can be seen or accessed from any other regular code cell in our notebookImportant Even though we are using a variable
x
in our function, it will not overwrite or change the value of the global variablex
because the local variablex
only exists within the function
# assign two integers - these are def
x = 10
y = 17
# pass them into out 'sub' function
d = sub(x, y)
# d will be assigned the difference between x and y
print(f'Output of the function is: {d}')
# Note that when we print the value of our
# global variable x it is unchanged even though
# we also used a variable named x in our function
print(f'Global variable x is unchanged: {x}')
Output of the function is: -7
Global variable x is unchanged: 10
Variable number of positional arguments¶
You can specify a variable number of positional arguments to a function with the special *
prefix:
the function definition def func(a, b, *args)
will assign the first (positional) argument to a, the second to b, and all the rest will be elements of the tuple args
.
def myfunction(a, b, *args):
print('a', a)
print('b', b)
print('vars', args)
myfunction(1, 2)
myfunction(1, 2, 'something else')
myfunction(1, 2, 3, 4, 'five!')
a 1
b 2
vars ()
a 1
b 2
vars ('something else',)
a 1
b 2
vars (3, 4, 'five!')
More useful examples using *args¶
square all elements of a set of numbers…
def squ_nums(*args):
out = []
for i in args:
out.append(i**2)
return out
print(squ_nums(9,10,12))
[81, 100, 144]
Find the max value of a set of numbers¶
def my_max(*args):
# check to make sure all elements are numbers
# if there are non-numerical values (like a string)
# then return a message to user letting them know
for i in args:
if not isinstance(i, int) and not isinstance(i, float):
return 'non numerical input'
# start by assigning first input
# value to a variable that stores the
# max (m)
m = args[0]
# then loop over all the remaining
# elements
for i in args[1:]:
# if the current element is
# bigger than the previous max
# then reassign
if i > m:
m = i
return m
my_max(0,7,100,2.0)
100
my_max(0,7,100,'2.0')
'non numerical input'
Variable number of keyword arguments¶
You can specify a variable number of keyword arguments to a function with the special
**
prefix:The function definition
def func(a, b, **kwargs)
will assign the first (positional) argument to a, the second to b, and all the rest will be elements of the dictionarykwargs
.Important: You need to the keyword argument notation that we covered above (e.g. variable_name = value)
def myfunction(a, b, **kwargs):
print('a', a)
print('b', b)
print('kwargs', kwargs)
# omitting the kwargs are ok...
myfunction(1, 2)
# here is a version with two positional
# arguments and three keyword arguments
myfunction(1, 2, x=4, y=5, z=6)
# but notice that **kwargs does not capture positional arguments!
# you have to assign by keyword
myfunction(1, 2, 3)
a 1
b 2
kwargs {}
a 1
b 2
kwargs {'x': 4, 'y': 5, 'z': 6}
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/8y/scw4yk6d3g99kqbs49wp8q3r0000gn/T/ipykernel_22073/2915692804.py in <module>
14 # but notice that **kwargs does not capture positional arguments!
15 # you have to assign by keyword
---> 16 myfunction(1, 2, 3)
TypeError: myfunction() takes 2 positional arguments but 3 were given
Note that this definition captures all the positional arguments first, then captures the remaining keyword arguments. For this reason there may be no positional arguments after a keyword argument.¶
myfunction(1, 2, 3, x=4, y=5, z=6, 7)
File "/var/folders/8y/scw4yk6d3g99kqbs49wp8q3r0000gn/T/ipykernel_22073/2997981550.py", line 1
myfunction(1, 2, 3, x=4, y=5, z=6, 7)
^
SyntaxError: positional argument follows keyword argument
A more useful example of **kwargs¶
Imagine you run a bike shop or some other store.
You want to collect as much information as possible about each customer, but some people don’t share much and others share a lot of stuff…
Pass all of the user input into a function that creates a dictionary with all provided information.
If you use kwargs then you can flexibly accomodate different amounts of info about each user.
Also introduce docstrings where you can write helper info about your function so that others can quickly figure out what the function does, what kinds of input you’re expecting, and what the function returns.
def define_user(name, **info):
"""Accept user input for database
input:
name - first name of user
**info - kwargs...other relevant info in form of key:value pairs
returns:
dictionary with user info
"""
# init a blank dictionary
user_info = {}
user_info['name'] = name
# then loop over kwargs
for k, v in info.items():
user_info[k] = v
return user_info
# see the docstring so we know how to use the function
# define_user?
usr_info = define_user('john', bike='aethos', car='tacoma', address='24895 Long Valley Rd')
usr_info
{'name': 'john',
'bike': 'aethos',
'car': 'tacoma',
'address': '24895 Long Valley Rd'}
Example of abstracting a common operation into a function¶
Recall last week we wrote some code to find a string of text in a book and then we kept N lines of text following the target string
Using functions, we can write a general purpose block of code that will take a target string and the number of lines to keep as input parameters. Now we can apply this function to any book and it will do this job for us.
def find_str(book,search_target,keep_lines):
# loop over each line in the book using enumerate
# enumerate will automatically create and increment a counter
# that we'll call 'cnt' and it will give you each line
# in the book just like a normal for loop.
for cnt, line in enumerate(book):
# test to see if the current line has search_target in it
if search_target in line:
# grab the line where chapter 5 starts
start_index = cnt
# exit the loop by calling 'break'
break
# now use slicing to just keep the first keep_lines lines
# after the search_target
book = book[start_index:start_index + keep_lines]
return book
Now we can call the function…¶
And the key thing here is that this should work for any book or list object with strings of text as elements…i.e. it is now a general purpose function and we don’t have to type that code again!
# open our file for reading...
with open('frankenstein.txt', 'r') as f:
# read the entire file...with each line
# returned as a string in a list
# we'll call the list 'book'
book = f.readlines()
# now call the function...
book_trimmed = find_str(book, 'It was on a dreary night', 10)
print(book_trimmed)
['It was on a dreary night of November that I beheld the accomplishment\n', 'of my toils. With an anxiety that almost amounted to agony, I\n', 'collected the instruments of life around me, that I might infuse a\n', 'spark of being into the lifeless thing that lay at my feet. It was\n', 'already one in the morning; the rain pattered dismally against the\n', 'panes, and my candle was nearly burnt out, when, by the glimmer of the\n', 'half-extinguished light, I saw the dull yellow eye of the creature\n', 'open; it breathed hard, and a convulsive motion agitated its limbs.\n', '\n', 'How can I describe my emotions at this catastrophe, or how delineate\n']
Functions: Review¶
def
defines a functionfunction_name()
- parentheses are required to execute a functionfunction_name(input1)
- input parameters are specified within the function parenthesesfunction_name(input1, input2)
- functions can take multiple parameters as inputsinput1
andinput2
can then be used within your function when it executes
To store the output from a function, you’ll need a
return
statementLocal variables exist only within the scope of a function and cannot be accessed outside of the function
Global variables exist outside the scope of a function and can be seen in all code cells in the notebook
A variable number of positional or keyword arguments can be passed using the
*
and**
syntax
More advanced stuff - not covered in class but useful to read¶
The catch-all function¶
Because you can pass in a variable number of parameters by position (
*
) or by keyword (**
) you could write a function that would take just about anything as input (see below)But please don’t do this - it will drive people crazy because they won’t know what kind of input the function expects, and it is almost always better to have well defined functions with a specific signature.
def myfunction(*args, **kwargs):
print('args', args)
print('kwargs',kwargs)
print('\nmyfunction()')
myfunction()
print('\nmyfunction(1, 2, x=4, y=5, z=6)')
myfunction(1, 2, x=4, y=5, z=6)
print('\nmyfunction(1, 2, 3)')
myfunction(1, 2, 3)
print('\nmyfunction(1, 2, 3, x=4, y=5, z=6)')
myfunction(1, 2, 3, x=4, y=5, z=6)
myfunction()
args ()
kwargs {}
myfunction(1, 2, x=4, y=5, z=6)
args (1, 2)
kwargs {'x': 4, 'y': 5, 'z': 6}
myfunction(1, 2, 3)
args (1, 2, 3)
kwargs {}
myfunction(1, 2, 3, x=4, y=5, z=6)
args (1, 2, 3)
kwargs {'x': 4, 'y': 5, 'z': 6}
Calling functions within other functions¶
Suppose we wrote a function called
is_odd()
which takes an inputvalue
,
def is_odd(value):
if value % 2 != 0:
answer = True
else:
answer = False
return answer
To use the function, we can execute is_odd(value)
out = is_odd(6)
out
False
Later on, if you wanted to use that function within another function you still have to pass an input to the function.
def new_function(my_list):
output = []
for val in my_list:
# here we can call our other function `is_odd`
if is_odd(val):
output.append('yay!')
return output
new_function([1,2,3,4])
['yay!', 'yay!']
Note about function signatures and type annotation¶
Although it is possible to define all functions as a catchall function: def f(*args, **kwargs):
that would be terrible in practice, because then it is nearly impossible to figure out what kind of input the function expects. Consequently you ought to define functions in the most restrictive manner possible. If you expect one argument, define the function accordingly.
In Python you can also add type annotations to your function definitions. These are not used directly by the python interpreter, but are often used by the IDE to help you write code. This is also good practice as it makes it clear what kind of arguments your function expects and returns.
def transformString(string: str) -> str:
return string.lower()
More formal explanation of local/global namespaces¶
Variables in python are passed to functions by assignment.
Recall that we can assign two names to the same object:
a = 'hello'
b = a # b takes on the value of a
# since they are both referring to the same
# value ('hello') they have the same id...
# we can also say that they b is a `view` of a...
print(id(a))
print(id(b))
140461163333616
140461163333616
when we pass something to a function, we are effectively assigning a new (local) name to that same object.
def func(x):
print(id(x))
a = "hello"
print(id(a))
func(a)
140461163333616
140461163333616
This is nice, because we don’t have to allocate more memory to store a copy of that object when we pass it into a function. However, weird things might happen, depending on what we do to that object.
def func(x):
print('enter function')
print(' x:', x)
print(' id(x)', id(x))
x = 'boo'
print(' x:', x)
print(' id(x)', id(x))
print('exit function')
a = "hello"
print('a:', a)
print('id of a globally', id(a))
func(a)
print('a:', a)
print('id of a globally', id(a))
a: hello
id of a globally 140461163333616
enter function
x: hello
id(x) 140461163333616
x: boo
id(x) 140461146096496
exit function
a: hello
id of a globally 140461163333616
So what happened here is that
x
inside the function started off pointing to the same object asa
globally, but then, when we reassigned a new string tox
, these two variables pointed to different objects, and the local changes tox
did not alter the object referred to by the global namea
.This happens because we reasigned a value to
x
.However, some objects we can alter in place, and as a consequence, local changes will also carry over to the global variable.
def func(x):
print('enter function')
print(' x:', x)
print(' id(x)', id(x))
x.append('boo')
print(' x:', x)
print(' id(x)', id(x))
print('exit function')
a = ["hello"]
print('a:', a)
print('id of a globally', id(a))
func(a)
print('a:', a)
print('id of a globally', id(a))
a: ['hello']
id of a globally 140461167716864
enter function
x: ['hello']
id(x) 140461167716864
x: ['hello', 'boo']
id(x) 140461167716864
exit function
a: ['hello', 'boo']
id of a globally 140461167716864
Here, we used a list variable, which is mutable, and we called a method of that object that mutates that variable in place. The consequence is that we changed the object, that the global and local variables pointed to, and the changes we made inside the function persisted outside the function.
If we instead changed the variable inside the function via reassignment, then those changes would stay local. (This is a case in which x += [a]
behaves differently from x = x + [a]
because the +=
operator uses an inplace method to append).
def func(x):
print('enter function')
print(' x:', x)
print(' id(x)', id(x))
x = x + ['boo']
print(' x:', x)
print(' id(x)', id(x))
print('exit function')
a = ["hello"]
print('a:', a)
print('id of a globally', id(a))
func(a)
print('a:', a)
print('id of a globally', id(a))
a: ['hello']
id of a globally 140461167717376
enter function
x: ['hello']
id(x) 140461167717376
x: ['hello', 'boo']
id(x) 140461167717632
exit function
a: ['hello']
id of a globally 140461167717376