from IPython.display import IFrame
from IPython.display import Markdown
# Additional styling ; should be moved into helpers
from IPython.core.display import display, HTML
HTML('<style>{}</style>'.format(open('rise.css').read()))

Class 7A: Introduction to Programming in Python I¶

We will begin soon! Until then, feel free to use the chat to socialize, and enjoy the music!

October 18, 2021
Firas Moosvi

Announcements¶

  1. Welcome Back!! Hope you had a good week off

  1. Grades and feedback for Labs 1-3 and feedback for LL1 - LL4 is now released (grades to come much later); if any major issues, submit regrade request on Gradescope.

  1. Milestone 3 due next week!

  1. Test 2 window will start this Friday at 6 PM!

  1. Lab 5 is due this week.

  1. Reminder: my Student Hours are after class on M,W,F and the Project TA Student hours are Wednesdays from 1-2 PM

Mid-Course Feedback¶

../../../_images/01_structure.png ../../../_images/02_lectures.png ../../../_images/02B_pace.png ../../../_images/03_logs.png ../../../_images/04_labs.png ../../../_images/05_feedback.png ../../../_images/06_new.png ../../../_images/07_overall.png

Feedback from Stop / Start / Continue¶

  1. Go Slower in class.

Sorry - Message received! Will work on pacing to slow down and allow people to follow along

  1. Clearer project instructions

Hopefully at some point this week I will go over Milestone 3 requirements and add some clarity

  1. Tests not on the weekend

Okay, I will open the remaining Tests starting at 6 AM on Friday mornings. This will not affect most people, but some people want the weekend completely off.

  1. Faster feedback release

Will do our best! The TAs for this course are students as well.

  1. More pract ice questions for Python

Done! Added to the course website.

  1. Test results before Bonus Tests

Sorry, there’s no way I can promise that!

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.

Class Outline¶

  • Comments (0 min)

  • Why Python? (0 min)

  • Loops (15 min)

  • Comprehensions (5 min)

  • Functions intro (10 min)

  • DRY principle (15 min)

  • Keyword arguments (5 min)

Attribution¶

Why Python? (0 min)¶

  • Why did we choose Python in Data 301?

    • Extremely popular in DS (and beyond!)

    • Relatively easy to learn

    • Good documentation

    • Huge user community

      • Lots of Stack Overflow and other forums

      • Lots of useful packages (more on this, later this week)

Comments in python (0 min)¶

x = 1 # why am I doing this? this is a comment
"""
this is a string, which does nothing
and can be used as a comment

I can Write anything I want here
"""

7


x = 1

Loops (10 min)¶

  • Loops allow us to execute a block of code multiple times.

  • We will focus on for loops

for n in [2, 7, -1, 5]:
    print(f"The number is {n} and its square is {n**2}")
    # this is inside the loop
    print(n)
    
# this is outside the loop
n
The number is 2 and its square is 4
2
The number is 7 and its square is 49
7
The number is -1 and its square is 1
-1
The number is 5 and its square is 25
5
5

The main points to notice:

  • Keyword for begins the loop

  • Colon : ends the first line of the loop

  • We can iterate over any kind of iterable: list, tuple, range, string. In this case, we are iterating over the values in a list

  • Block of code indented is executed for each value in the list (hence the name “for” loops, sometimes also called “for each” loops)

  • The loop ends after the variable n has taken all the values in the list

"abc" + "def"
'abcdef'
word = "Python"
for letter in word:
    print("Gimme a " + letter + "!")

print("What's that spell?!! " + word + "!")
Gimme a P!
Gimme a y!
Gimme a t!
Gimme a h!
Gimme a o!
Gimme a n!
What's that spell?!! Python!
  • A very common pattern is to use for with range.

  • range gives you a sequence of integers up to some value.

for i in range(0,10):
    print(i)
0
1
2
3
4
5
6
7
8
9

We can also specify a start value and a skip-by value with range:

for i in range(1,101,10):
    print(i*2)
2
22
42
62
82
102
122
142
162
182

We can write a loop inside another loop to iterate over multiple dimensions of data. Consider the following loop as enumerating the coordinates in a 3 by 3 grid of points.

for x in [1,2,3]:
    for y in ["a","b","c"]:
        print((x,y))
(1, 'a')
(1, 'b')
(1, 'c')
(2, 'a')
(2, 'b')
(2, 'c')
(3, 'a')
(3, 'b')
(3, 'c')
list_1 = [1,2,3]
list_2 = ["a","b","c"]
for i in range(len(list_1)):
    print(list_1[i], list_2[i])
1 a
2 b
3 c

We can loop through key-value pairs of a dictionary using .items():

courses = {300 : "awesome",
           301 : "naptime!",
           302 : "riveting",
}

for course_num, description in courses.items():
    print(f"DATA {course_num} is {description}")
DATA 300 is awesome
DATA 301 is naptime!
DATA 302 is riveting
for course_nu m in courses:
    print(course_num, courses[course_num])
  File "/tmp/ipykernel_2007/3169721264.py", line 1
    for course_nu m in courses:
                  ^
SyntaxError: invalid syntax

Above: the general syntax is for key, value in dictionary.items():

Optional while loops (rarely used)¶

  • We can also use a while loop to excute a block of code several times.

  • In reality, I rarely use these.

  • Beware! If the conditional expression is always True, then you’ve got an infintite loop!

    • (Use the “Stop” button in the toolbar above, or Ctrl-C in the terminal, to kill the program if you get an infinite loop.)

n = 10
while n > 0:
    print(n)
    n = n - 1

print("Blast off!")

Comprehensions (5 min)¶

Comprehensions allow us to build lists/tuples/sets/dictionaries in one convenient, compact line of code.

words = ["hello", "goodbye", "the", "antidisestablishmentarianism"]

y = list()
for word in words:
    y.append(word[-1])
y
['o', 'e', 'e', 'm']
y = [word[-1] for word in words]  # list comprehension
y
['o', 'e', 'e', 'm']
word_lengths = {word : len(word) for word in words} # dictionary comprehension
word_lengths
{'hello': 5, 'goodbye': 7, 'the': 3, 'antidisestablishmentarianism': 28}
word_lengths = {}

for word in words:
    word_lengths[word] = len(word)
    
word_lengths
{'hello': 5, 'goodbye': 7, 'the': 3, 'antidisestablishmentarianism': 28}

Functions intro (5 min)¶

  • 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 parameter n and returns the square n**2.

def square(n):
    n_squared = n**2
    return n_squared
square(5)
25
square(100)
square(12345)
  • 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

Side effects¶

  • 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!

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(0))

DRY principle, designing good functions (15 min)¶

  • DRY: Don’t Repeat Yourself

  • See Wikipedia article

  • Consider the task of, for each element of a list, turning it into a palindrome

    • e.g. “mike” –> “mikeekim”

names = ["milad", "rodolfo", "tiffany","Firas"]
name = "mike"
name[::-1]
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
  • Above: this is gross, terrible, yucky code

    1. It only works for a list with 3 elements

    2. It only works for a list named names

    3. If we want to change its functionality, we need to change 3 similar lines of code (Don’t Repeat Yourself!!)

    4. 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

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

make_palindromes(names)
  • 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
names_backwards_2 = list()

for name in names2:
    names_backwards_2.append(name + name[::-1])
    
names_backwards_2
names_backwards_3 = list()

for name in names3:
    names_backwards_3.append(name + name[::-1])
    
names_backwards_3

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)
make_palindromes(names2)
make_palindromes(names3)
  • 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))

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")
  • 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 names]

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 (5 min)¶

  • 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 repeat_string(s, n=2):
    return s*n
repeat_string("mds", 2)
repeat_string("mds-", 5)
repeat_string("mds") # do not specify `n`; it is optional

Sane 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 sane 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,3,4)

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) 

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 non-optional arguments by order, and 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. here

  • Do 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