Session 3

Published

October 5, 2023

Topics Covered
  • For Loop
  • Lists
  • Reading Files
  • List Comprehensions
  • Iteration patterns

For Loop

names = ["alice", "bob", "charlie", "dave"]
for name in names:
    print("Hello", name)
Hello alice
Hello bob
Hello charlie
Hello dave

Lists

x = ["a", "b", "c", "d"]
x
['a', 'b', 'c', 'd']
x[0]
'a'
x[1]
'b'

How to get the last element?

x[len(x)-1]
'd'
x[-1]
'd'
+----+----+----+----+
|  a |  b |  c |  d |
+----+----+----+----+
|  0 |  1 |  2 |  3 | --> regular index
+----+----+----+----+
| -4 | -3 | -2 | -1 | <-- negative index
+----+----+----+----+
def get_last_word(sentence):
    return sentence.split()[-1]
get_last_word("one two three")
'three'

Modifying & Growing Lists

x = ["a", "b", "c", "d"]
x[0] = "aa"
x
['aa', 'b', 'c', 'd']
x.append("e")
x
['aa', 'b', 'c', 'd', 'e']

Please note that the append method modifies the list in place and doesn’t return anything.

x = ['a', 'b', 'c', 'd']
x = x.append('e')
print(x)
None

It is None because the append method doesn’t return the list back.

x = [1, 2, 3]
y = x
x.append(4)

print(x)
print(y)
[1, 2, 3, 4]
[1, 2, 3, 4]
x = [1, 2, 3]
y = x
x = [4, 5, 6]

print(x)
print(y)
[4, 5, 6]
[1, 2, 3]

There are two different kind of modifications.

  1. The variable is getting reassigned to a new value (rebinding)
  2. The value is getting modified (mutation)
x = 1
y = x
x = 2 # rebinding
print(x, y)
2 1
x = [1, 2, 3]
y = x
x.append(4) # mutation

print(x)
print(y)
[1, 2, 3, 4]
[1, 2, 3, 4]

List Slicing

x = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight"]
x[0:2] # from index 0 to index 2 (end is not included)
['zero', 'one']
x[:2] # up to index 2
['zero', 'one']
x[2:] #  from index 2 onwards
['two', 'three', 'four', 'five', 'six', 'seven', 'eight']
x[1:6] # from index 1 to 6
['one', 'two', 'three', 'four', 'five']
x[1:6:2] # from index 1 to 6, take every second element
['one', 'three', 'five']

What about all elements, except the last one?

x[:-1]
['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven']

How to get the last two elements?

x[-2:]
['seven', 'eight']

What if we want the list in the reverse order?

x[::-1]
['eight', 'seven', 'six', 'five', 'four', 'three', 'two', 'one', 'zero']
x[:] # copy of  x
['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight']
list(x) # make a new list with all elements of x
['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight']

Example: echo.py

In the previous classes, we have written an echo.py that printed just the first argument. Can we improve it to print all arguments, just like the echo command.

!echo hello world
hello world
!echo 1 2 3 4
1 2 3 4
%%file echo.py
import sys

args = sys.argv[1:]

print(" ".join(args))
Overwriting echo.py
!python echo.py hello world
hello world
!python echo.py 1 2 3
1 2 3

Reading Files

!ls files
abc          empty.txt  numbers  words2.txt  zen-of-python.txt
bumper-stickers.txt  five.txt   ten.txt  words.txt
%%file three.txt
one
two
three
Writing three.txt
open("three.txt").read()
'one\ntwo\nthree\n'
open("three.txt").readlines()
['one\n', 'two\n', 'three\n']
print(open("three.txt").read())
one
two
three
for line in open("three.txt").readlines():
    print(line)
one

two

three

Do you know why there is gap between the lines?

The line has a new line character at the end and print is adding one more.

We can fix it either by striping the new line from the line or asking print to not add a newline.

for line in open("three.txt").readlines():
    print(line.strip("\n"))
one
two
three
for line in open("three.txt").readlines():
    print(line, end="")
one
two
three

Problem: Head

%load_problem head
Problem: Head

Write a program head.py to print the first 5 lines of a file.

$ python head.py files/ten.txt
1
2
3
4
5

You can verify your solution using:

%verify_problem head

%%file head.py
# your code here
print(1)

Writing head.py
!python head.py files/ten.txt
1
!head -5 files/ten.txt
1
2
3
4
5
filename = "ten.txt"
for line in filename:
    print(line)
t
e
n
.
t
x
t
for c in "hello":
    print(c)
h
e
l
l
o

Solution:

%%file head.py
import sys
filename = sys.argv[1]
lines = open(filename).readlines()
lines = lines[:5] # take first 5 lines

for line in lines:
    print(line, end="")
Overwriting head.py
!python head.py files/ten.txt
1
2
3
4
5
%verify_problem head
✓ python head.py /opt/files/ten.txt
✓ python head.py /opt/files/zen-of-python.txt
✓ python head.py /opt/files/words.txt
✓ python head.py /opt/files/empty.txt
✓ python head.py /opt/files/leading-space.txt
🎉 Congratulations! You have successfully solved problem head!!

List Comprehensions

Computes squares of a list of numbers.

def squares(numbers):
    result = []
    for n in numbers:
        result.append(n*n)
    return result
squares([1, 2, 3, 4, 5])
[1, 4, 9, 16, 25]
numbers = [1, 2, 3, 4, 5]
[n*n for n in numbers]
[1, 4, 9, 16, 25]
result = [n*n for n in numbers]
result
[1, 4, 9, 16, 25]
[n*n for n in numbers if n%2 == 0]
[4, 16]
# compute the sum of squares of all even numbers below one million
sum([n*n for n in range(1000000) if n%2 == 0])
166666166667000000

Let’s unpack them a bit.

result = [expr for a_var in a_list]
result = [expr for a_var in a_list if some_condition]

That is equivalant to:

result = []
for a_var in a_list:
    result.append(expr)

result = []
for a_var in a_list:
    if some_condition:
        result.append(expr)

Find all python files in the current directory.

os.listdir(".") # all files
['index.qmd',
 'assignments.qmd',
 'assignment-01.qmd',
 '_quarto.yml',
 '_book',
 'course.qmd',
 'references.qmd',
 '.ipynb_checkpoints',
 'three.txt',
 'head.py',
 'date.py',
 'args.py',
 'styles.css',
 'echo.py',
 'session3.ipynb',
 'syllabus.qmd',
 'countdown.py',
 'session2.ipynb',
 'square.py',
 '.quarto',
 'lab.qmd',
 'files',
 'script.js',
 'session1.ipynb.invalid',
 'schedule.qmd',
 'session1.ipynb',
 'hello.py',
 'live-notes.qmd',
 'schedule.ipynb',
 'references.bib']
[f for f in os.listdir(".") if f.endswith(".py")]
['head.py',
 'date.py',
 'args.py',
 'echo.py',
 'countdown.py',
 'square.py',
 'hello.py']
os.path.getsize("head.py")
152
!ls -l head.py
-rw-r--r-- 1 jupyter-anand jupyter-anand 152 Oct  5 04:50 head.py

What if we want to find the size of all the python files in the current directory.

[os.path.getsize(f) for f in os.listdir(".") if f.endswith(".py")]
[152, 35, 28, 55, 12, 62, 23]
sum([os.path.getsize(f) for f in os.listdir(".") if f.endswith(".py")])
367

Problem: Evens

%load_problem evens
Problem: Evens

Write a function evens that takes a list of numbers as argument and returns a new list containing only the even numbers out of it.

>>> evens([1, 2, 3, 4, 5, 6])
[2, 4, 6]

You can verify your solution using:

%verify_problem evens

# your code here


Problem: Sum of Arguments

%load_problem sum-of-arguments
Problem: Sum of Arguments

Write a program sum.py that takes one or more numbers as command-line argument and prints their sum.

$ python sum.py 1 2 3 4 5
15

You can verify your solution using:

%verify_problem sum-of-arguments

%%file sum.py
import sys
args = sys.argv[1:]
numbers = [int(a) for a in args]
print(sum(numbers))
Overwriting sum.py
!python sum.py 1 2 3 4 5
15

Iteration Patterns

Iterating over a list

x = ['a', 'b', 'c', 'd']
for a in x:
    print(a, a.upper())
a A
b B
c C
d D
[a.upper() for a in x]
['A', 'B', 'C', 'D']
[(a, a.upper()) for a in x]
[('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')]

Iterating over a sequence of numbers

for i in range(5):
    print(i*i)
0
1
4
9
16
[i*i for i in range(5)]
[0, 1, 4, 9, 16]

Iterating over two lists together

Let’s say we have names of students and their scores in two seperate lists. How can we print name and score for each one.

names = ["a", "b", "c", "d"]
scores = [10, 20, 30, 40]
for i in range(len(names)):
    print(names[i], scores[i])
a 10
b 20
c 30
d 40

This is correct, but not Pythonic!

If you are ever accessing elements of a list in a loop using index, you are probably doing it the wrong way.

zip(names, scores)
<zip at 0x7f77e56d2280>

You can’t see it becasue it is iterable object. We can convert it to a list to see what is inside.

list(zip(names, scores))
[('a', 10), ('b', 20), ('c', 30), ('d', 40)]
for name, score in zip(names, scores):
    print(name, score)
a 10
b 20
c 30
d 40
for a, b in zip(["a", "b", "c"], [1, 2]):
    print(a, b)
a 1
b 2

When the lists are not of the same size, zip stops when the smallest one ends.

Iterating over the index and the element together

dates = [
    "Sep 21",
    "Sep 25",
    "Oct 05",
    "Oct 09"]
for i in range(len(dates)):
    print(f"Session {i}: {dates[i]}")
Session 0: Sep 21
Session 1: Sep 25
Session 2: Oct 05
Session 3: Oct 09

This is not very Pythonic!

for i, date in enumerate(dates):
    print(f"Session {i}: {date}")
Session 0: Sep 21
Session 1: Sep 25
Session 2: Oct 05
Session 3: Oct 09
for i, date in enumerate(dates):
    print(f"Session {i+1}: {date}")
Session 1: Sep 21
Session 2: Sep 25
Session 3: Oct 05
Session 4: Oct 09
for i, date in enumerate(dates, start=1):
    print(f"Session {i}: {date}")
Session 1: Sep 21
Session 2: Sep 25
Session 3: Oct 05
Session 4: Oct 09

What if we want only dates for the even sessions?

[date for i, date in enumerate(dates, start=1) if i%2 == 0]
['Sep 25', 'Oct 09']

Example: Count Increased

%load_problem count-increased
Problem: Count Increased

Given a list of numbers, count the number of times a number in the list increases from the previous number.

For example:

Given the list of numbers as [3, 5, 4, 9, 2, 8]

3 (N/A - no previous number)
5 (increased)
4 (decreased)
9 (increased)
2 (decreased)
8 (increased)

So the number of times a number increased from the previous number is 3.

Write a function count_increased that takes a list of numbers as arguments and returns the number of times a number in that list is increased from the previous one.

>>> count_increased([3, 5, 4, 9, 2, 8])
3

Credits: this problem is modelled after Day 1 problem of Advent of Code 2021.

You can verify your solution using:

%verify_problem count-increased

numbers = [3, 5, 4, 9, 2, 8]
3 5 4 9 2 8
- 3 4 5 9 2
0 1 0 1 0 1
[a>b for a, b in zip(numbers[1:], numbers)]
[True, False, True, False, True]
list(zip(numbers[1:], numbers))
[(5, 3), (4, 5), (9, 4), (2, 9), (8, 2)]
sum([a>b for a, b in zip(numbers[1:], numbers)])
3