Class 9A: Programming in Python I#
We will begin soon!
from IPython.display import IFrame
from IPython.display import Markdown
# Additional styling ; should be moved into helpers
from IPython.display import display, HTML
HTML("<style>{}</style>".format(open("rise.css").read()))
Announcements#
Grades and feedback for Labs 1-5 and feedback for LL1 - LL6 is now released; if you got a G or an R, submit a resubmission request on Ed Discussion.
Project Deadlines:
Milestone 4 due March 23rd, 2023.
Milestone 5 due April 6th, 2023.
Milestone 6 due April 11th, 2023.
You should be spending at least a few hours every week working on your project, do not leave things to the last minute!
Test 3 will be this Wednesday during class, you MUST be present in class!!
Make sure to check and read the rules carefully!
Class Outline#
Announcements (2 mins)
Functions in Python (40 min)
Programming in Python I#
In this class, we go through a notebook by a former colleague, Dr. Mike Gelbart, option co-director of the UBC-Vancouver MDS program.
If you prefer, you can also watch his recording of the same material.
Attribution#
The original version of these Python lectures were by Patrick Walls.
These lectures were delivered by Mike Gelbart and are available publicly here.
Functions in Python (40 mins)#
Define a function to re-use a block of code with different input parameters, also known as arguments.
For example, define a function called
square
which takes one input parametern
and returns the squaren**2
.
def square(n):
n_squared = n**2
n_squared
# Also possible
def square(n):
return n**2
Cell In[2], line 3
n_squared
^
IndentationError: unexpected indent
var = 50
square(n=var)
new_n_squared = square(n=var)
new_n_squared
#n_squared # not available to us because the "scope" of it is only within square(n)
2500
import numpy as np
np.mean([5, 10, 15, 20])
12.5
square(n=np.mean([5, 10, 15, 20]))
156.25
square(12345)
152399025
Begins with
def
keyword, function name, input parameters and then colon (:
)Function block defined by indentation
Output or “return” value of the function is given by the
return
keyword
Null return type#
If you do not specify a return value, the function returns None
when it terminates:
def f(x):
x + 1 # no return!
if x == 999:
return
print(f(5))
None
DRY principle, designing good functions#
DRY: Don’t Repeat Yourself
Consider the task of, for each element of a list, turning it into a palindrome
e.g. “mike” –> “mikeekim”
names = ["milad", "rodolfo", "tiffany", "Firas"]
# Aside: to reverse a string, use ::-1
name = "mike"
name[::-1]
'ekim'
names_backwards = list()
names_backwards.append(names[0] + names[0][::-1])
names_backwards.append(names[1] + names[1][::-1])
names_backwards.append(names[2] + names[2][::-1])
names_backwards.append(names[3] + names[3][::-1])
names_backwards
['miladdalim', 'rodolfooflodor', 'tiffanyynaffit', 'FirassariF']
Above: this is gross, terrible, yucky code
It only works for a list with 4 elements
It only works for a list named
names
If we want to change its functionality, we need to change 3 similar lines of code (Don’t Repeat Yourself!!)
It is hard to understand what it does just by looking at it
names_backwards = list()
for name in names:
names_backwards.append(name + "-" + name[::-1])
names_backwards
['milad-dalim', 'rodolfo-oflodor', 'tiffany-ynaffit', 'Firas-sariF']
# using list comprehensions
[f"{name}-{name[::-1]}" for name in names]
['milad-dalim', 'rodolfo-oflodor', 'tiffany-ynaffit', 'Firas-sariF']
Above: this is slightly better. We have solved problems (1) and (3).
def make_palindromes(names):
names_backwards = list()
for name in names:
names_backwards.append(name + name[::-1])
return names_backwards
# list comprehension function (equivalent)
def make_palindromes(names):
return [f"{name}-{name[::-1]}" for name in names]
make_palindromes(names)
['milad-dalim', 'rodolfo-oflodor', 'tiffany-ynaffit', 'Firas-sariF']
Above: this is even better. We have now also solved problem (2), because you can call the function with any list, not just
names
.For example, what if we had multiple lists:
names1 = ["milad", "rodolfo", "tiffany"]
names2 = ["Trudeau", "Scheer", "Singh", "Blanchet", "May"]
names3 = ["apple", "orange", "banana"]
names_backwards_1 = list()
for name in names1:
names_backwards_1.append(name + name[::-1])
names_backwards_1
['miladdalim', 'rodolfooflodor', 'tiffanyynaffit']
names_backwards_2 = list()
for name in names2:
names_backwards_2.append(name + name[::-1])
names_backwards_2
['TrudeauuaedurT', 'ScheerreehcS', 'SinghhgniS', 'BlanchettehcnalB', 'MayyaM']
names_backwards_3 = list()
for name in names3:
names_backwards_3.append(name + name[::-1])
names_backwards_3
['appleelppa', 'orangeegnaro', 'bananaananab']
Above: this is very bad also (and imagine if it was 20 lines of code instead of 2). This was problem (2). Our function makes it much better:
make_palindromes(names1)
['milad-dalim', 'rodolfo-oflodor', 'tiffany-ynaffit']
make_palindromes(names2)
['Trudeau-uaedurT',
'Scheer-reehcS',
'Singh-hgniS',
'Blanchet-tehcnalB',
'May-yaM']
make_palindromes(names3)
['apple-elppa', 'orange-egnaro', 'banana-ananab']
You could get even more fancy, and put the lists of names into a list (so you have a list of lists).
Then you could loop over the list and call the function each time:
for list_of_names in [names1, names2, names3]:
print(make_palindromes(list_of_names))
['milad-dalim', 'rodolfo-oflodor', 'tiffany-ynaffit']
['Trudeau-uaedurT', 'Scheer-reehcS', 'Singh-hgniS', 'Blanchet-tehcnalB', 'May-yaM']
['apple-elppa', 'orange-egnaro', 'banana-ananab']
Designing good functions#
How far you go with this is sort of a matter of personal style, and how you choose to apply the DRY principle: DON’T REPEAT YOURSELF!
These decisions are often ambiguous. For example:
Should
make_palindromes
be a function if I’m only ever doing it once? Twice?Should the loop be inside the function, or outside?
Or should there be TWO functions, one that loops over the other??
In my personal opinion,
make_palindromes
does a bit too much to be understandable.I prefer this:
def make_palindrome(name):
return name + "-" + name[::-1]
make_palindrome("milad")
'milad-dalim'
From here, we want to “apply
make_palindrome
to every element of a list”It turns out this is an extremely common desire, so Python has built-in functions.
One of these is
map
, which we’ll cover later. But for now, just a comprehension will do:
[make_palindrome(name) for name in names1]
['milad-dalim', 'rodolfo-oflodor', 'tiffany-ynaffit']
Other function design considerations:
Should we print output or produce plots inside or outside functions?
I would usually say outside, because this is a “side effect” of sorts
Should the function do one thing or many things?
This is a tough one, hard to answer in general
Optional & keyword arguments#
Sometimes it is convenient to have default values for some arguments in a function.
Because they have default values, these arguments are optional, hence “optional arguments”
Example:
def square(num, verbose = False):
if verbose:
print(f"You are a wonderful person, the square of the number you provided is {num**2}")
return num**2
square(7,verbose=True)
You are a wonderful person, the square of the number you provided is 49
49
def repeat_string(s, n=2):
return s * n
repeat_string("COSC301", 5)
repeat_string("mds-", 5)
repeat_string("mds") # do not specify `n`; it is optional
Sensible defaults:
Ideally, the default should be carefully chosen.
Here, the idea of “repeating” something makes me think of having 2 copies, so
n=2
feels like a sensible default.
Syntax:
You can have any number of arguments and any number of optional arguments
All the optional arguments must come after the regular arguments
The regular arguments are mapped by the order they appear
The optional arguments can be specified out of order
def example(a, b, c="DEFAULT", d="DEFAULT"):
print(a, b, c, d)
example(
1,
2,
)
Using the defaults for c
and d
:
example(1, 2)
Specifying c
and d
as keyword arguments (i.e. by name):
example(1, 2, c=3, d=4)
Specifying only one of the optional arguments, by keyword:
example(1, 2, c=3)
Or the other:
example(1, 2, d=4)
Specifying all the arguments as keyword arguments, even though only c
and d
are optional:
example(a=1, b=2, c=3, d=4)
Specifying c
by the fact that it comes 3rd (I do not recommend this because I find it is confusing):
example(1, 2, 3) # not recommended
Specifying the optional arguments by keyword, but in the wrong order (this is also somewhat confusing, but not so terrible - I am OK with it):
example(1, 2, d=4, c=3)
Specifying the non-optional arguments by keyword (I am fine with this):
example(a=1, b=2)
Specifying the non-optional arguments by keyword, but in the wrong order (not recommended, I find it confusing):
example(b=2, a=1)
Specifying keyword arguments before non-keyword arguments (this throws an error):
example(a=2, 1)
In general, I am used to calling required arguments in order, and any optional arguments by keyword.
The language allows us to deviate from this, but it can be unnecessarily confusing sometimes.
Advanced stuff (optional):#
You can also call/define functions with
*args
and**kwargs
; see, e.g. hereDo not instantiate objects in the function definition - see here under “Mutable Default Arguments”
def example(a, b=[]): # don't do this!
return 0
def example(a, b=None): # insted, do this
if b is None:
b = []
return 0
Docstrings (10 mins)#
We got pretty far above, but we never solved problem (4): It is hard to understand what it does just by looking at it
Enter the idea of function documentation (and in particular docstrings)
The docstring goes right after the
def
line.
def make_palindrome(string):
"""Turns the string into a palindrome by concatenating itself with a reversed version of itself."""
return string + string[::-1]
In IPython/Jupyter, we can use ?
to view the documentation string of any function in our environment.
make_palindrome?
print?
Docstring structure#
Single-line: If it’s short, then just a single line describing the function will do (as above).
PEP-8 style Multi-line description + a list of arguments; see here.
Scipy style: The most elaborate & informative; see here and here.
The PEP-8 style:
def make_palindrome(s):
"""
Turns the string into a palindrome by concatenating itself
with a reversed version of itself.
Arguments:
s - (str) the string to turn into a palindrome
"""
return s + s[::-1]
make_palindrome?
The scipy style:
def make_palindrome(s):
"""
Turn a string into a palindrome.
Turns the string into a palindrome by concatenating itself
with a reversed version of itself, so that the returned
string is twice as long as the original.
Parameters
----------
s : str
The string to turn into a palindrome.
Returns
-------
str
The new palindrome string.
Examples
--------
>>> make_palindrome("abc")
"abccba"
"""
return s + s[::-1]
make_palindrome("hello") # press shift-tab HERE to get docstring!!
Below is the general form of the scipy docstring (reproduced from the scipy/numpy docs):
def function_name(param1, param2, param3):
"""First line is a short description of the function.
A paragraph describing in a bit more detail what the
function does and what algorithms it uses and common
use cases.
Parameters
----------
param1 : datatype
A description of param1.
param2 : datatype
A description of param2.
param3 : datatype
A longer description because maybe this requires
more explanation and we can use several lines.
Returns
-------
datatype
A description of the output, datatypes and behaviours.
Describe special cases and anything the user needs to
know to use the function.
Examples
--------
>>> function_name(3,8,-5)
2.0
"""
Docstrings with optional arguments#
When specifying the parameters, we specify the defaults for optional arguments:
# PEP-8 style
def repeat_string(s, n=2):
"""
Repeat the string s, n times.
Arguments:
s -- (str) the string
n -- (int) the number of times (default 2)
"""
return s * n
# scipy style
def repeat_string(s, n=2):
"""
Repeat the string s, n times.
Parameters
----------
s : str
the string
n : int, optional (default = 2)
the number of times
Returns
-------
str
the repeated string
Examples
--------
>>> repeat_string("Blah", 3)
"BlahBlahBlah"
"""
return s * n
Automatically generated documentation#
By following the docstring conventions, we can automatically generate documentation using libraries like sphinx, pydoc or Doxygen.
For example: compare this documentation with this code.
Notice the similarities? The webpage was automatically generated because the authors used standard conventions for docstrings!
What makes good documentation?#
What do you think about this?
################################
#
# NOT RECOMMENDED TO DO THIS!!!
#
################################
def make_palindrome(string):
"""
Turns the string into a palindrome by concatenating itself
with a reversed version of itself. To do this, it uses the
Python syntax of `[::-1]` to flip the string, and stores
this in a variable called string_reversed. It then uses `+`
to concatenate the two strings and return them to the caller.
Arguments:
string - (str) the string to turn into a palindrome
Other variables:
string_reversed - (str) the reversed string
"""
string_reversed = string[::-1]
return string + string_reversed
This is poor documentation! More is not necessarily better!
Why?
Very verbose
Write documentation about “what it does” and not “how you did it” (that is an implementation detail)
Side effects (careful!)#
If a function changes the variables passed into it, then it is said to have side effects
Example:
def silly_sum(sri):
sri.append(0)
return sum(sri)
silly_sum([1, 2, 3, 4])
Looks good, like it sums the numbers? But wait…
lst = [1, 2, 3, 4]
silly_sum(lst)
silly_sum(lst)
lst
If you function has side effects like this, you must mention it in the documentation (later today).
In general avoid this!