Lesson 2

Overview

  1. Stuff I forgot to teach
  • Type casting
  1. Nesting
  • Nested loops
  • Nested lists/tuples/dicts/etc.
  1. References and scope
  • References
    • is
    • Copying
  • Scope
  1. List index

  2. PA1 Review

1. Stuff I forgot to teach

1.1 Type casting

Converting between types

Use the name of the type as a function to convert a variable to that type

a = 5.0
a
5.0
int(a)
5
type(int(a))
int
str(a)
'5.0'
type(str(a))
str
a = '123'
a
'123'
int(a)
123
type(int(a))
int
float(a)
123.0
type(float(a))
float

2. Nesting

2.1 Nested loops

A loop inside a loop

NOTE 1: Be careful about indents!

NOTE 2: You can include if and/or while statements inside nested loops

for i in range(3):
    for j in range(3):
        print(f'i: {i}, j: {j}')
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
i: 1, j: 1
i: 1, j: 2
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
for i in range(3):
    for j in range(3):
        print(f'i: {i}, j: {j}')
    print('outside of j loop')
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
outside of j loop
i: 1, j: 0
i: 1, j: 1
i: 1, j: 2
outside of j loop
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
outside of j loop

2.2 Nested lists/tuples/dicts/etc.

The elements of the main list are lists - access sublists using brackets, then elements of sublists using brackets

nested_list = [
    ['a11', 'a12', 'a13'],
    ['a21', 'a22', 'a23', 'a24'],
    ['a31', 'a32', 'a33']
]
print(nested_list)
[['a11', 'a12', 'a13'], ['a21', 'a22', 'a23', 'a24'], ['a31', 'a32', 'a33']]
print(nested_list[1])
['a21', 'a22', 'a23', 'a24']
print(nested_list[1][2])
a23
nested_list = [
    ['a11', 'a12', 'a13'],
    ['a21', 'a22', 'a23', 'a24'],
    ['a31', 'a32', 'a33'],
    'fjls',
    134,
    {'a': {'c': [1, 2, 3], 'd': 123}, 'b': 2}
]
print(nested_list)
[['a11', 'a12', 'a13'], ['a21', 'a22', 'a23', 'a24'], ['a31', 'a32', 'a33'], 'fjls', 134, {'a': {'c': [1, 2, 3], 'd': 123}, 'b': 2}]
print(nested_list[5])
{'a': {'c': [1, 2, 3], 'd': 123}, 'b': 2}
print(nested_list[5]['a'])
{'c': [1, 2, 3], 'd': 123}
print(nested_list[5]['a']['d'])
123

3. References and scope

What is the effect of modifying a variable?

3.1 References

a = 3
b = a
b = 4
print(f'a: {a}, b: {b}')
a: 3, b: 4

Variables that are lists (dictionaries) are actually references to where the list (dictionary) is stored in memory

a = [1, 2, 3]
b = a
b.append(4)
print('a:', a)
print('b:', b)
a: [1, 2, 3, 4]
b: [1, 2, 3, 4]
print(id(a))
print(id(b))
4597738112
4597738112

Another variable with the same values does not use the same reference

a = [1, 2, 3]
b = [1, 2, 3]
b.append(4)
print('a:', a)
print('b:', b)
a: [1, 2, 3]
b: [1, 2, 3, 4]
print(id(a))
print(id(b))
4597860864
4597853440
a = {'a': 'hello', 'b': 'goodbye'}
b = a
b['c'] = 'hellogoodbye'
print(a)
print(b)
{'a': 'hello', 'b': 'goodbye', 'c': 'hellogoodbye'}
{'a': 'hello', 'b': 'goodbye', 'c': 'hellogoodbye'}

List equality checks element-wise equality, not reference equality (warning for the future: NumPy has different behavior)

a = [1, 2, 3]
b = [1, 2, 3]
a == b
True
a = [1, 2, 3]
b = [1, 2, 4]
a == b
False
a = [[1, 2, 3], [4, 5, 6]]
b = [[1, 2, 3], [4, 5, 6]]
a == b
True
a = [[1, 2, 3], [4, 5, 6]]
b = [[1, 2, 3], [4, 5, 7]]
a == b
False

3.1.1 is

Use is to check reference equality

a = [[1, 2, 3], [4, 5, 6]]
b = [[1, 2, 3], [4, 5, 6]]
a is b
False
a = [[1, 2, 3], [4, 5, 6]]
b = a
a is b
True
a = 5
b = a
a is b
True
a = 5
b = 5
a is b
True

3.1.2 Copying

a = [1, 2, 3]
b = a.copy()
b.append(4)
print('a:', a)
print('b:', b)
a: [1, 2, 3]
b: [1, 2, 3, 4]

Sometimes .copy() isn’t an option, or doesn’t do what you expect

In particular, nested variables that use references won’t be copied, so we use copy.deepcopy() to recursively copy all nested variables

a = [[1, 2, 3], [4, 5, 6]]
b = a.copy()
b.append([7, 8, 9])
b[0][0] = 0
print(a)
print(b)
[[0, 2, 3], [4, 5, 6]]
[[0, 2, 3], [4, 5, 6], [7, 8, 9]]
import copy
a = [[1, 2, 3], [4, 5, 6]]
b = copy.deepcopy(a)
b.append([7, 8, 9])
b[0][0] = 0
print(a)
print(b)
[[1, 2, 3], [4, 5, 6]]
[[0, 2, 3], [4, 5, 6], [7, 8, 9]]

3.2 Scope

Defining new variables inside a function is a new, smaller scope

a = 5

def a_to_ten():
    a = 10
    print('Inside function:', a)

print('Before function:', a)
a_to_ten()
print('After function:', a)
Before function: 5
Inside function: 10
After function: 5
global a
a = 5

def a_to_ten():
    global a
    print('Inside function:', a)
    a = 10
    print('Inside function:', a)

print('Before function:', a)
a_to_ten()
print('After function:', a)
Before function: 5
Inside function: 5
Inside function: 10
After function: 10

Can still modify lists inside of functions (this is called in-place modification)

a = [1, 2, 3]

def append_4(a):
    a.append(4)

print('Before function:', a)
append_4(a)
print('After function:', a)
Before function: [1, 2, 3]
After function: [1, 2, 3, 4]

This does not depend on calling the function argument a

a = [1, 2, 3]

def append_4(b):
    b.append(4)

print('Before function:', a)
append_4(a)
print('After function:', a)
Before function: [1, 2, 3]
After function: [1, 2, 3, 4]

If we don’t define a inside the function, it will use a from outside the function’s scope

a = [1, 2, 3]

def append_4():
    a.append(4)

print('Before function:', a)
append_4()
print('After function:', a)
Before function: [1, 2, 3]
After function: [1, 2, 3, 4]

A variable defined inside a function is not accessible outside the function, unless it is returned

del a
def define_a():
    a = [1, 2, 3]

define_a()
a
NameError: name 'a' is not defined
def define_a():
    a = [1, 2, 3]
    return a

a = define_a()
a
[1, 2, 3]
a = [1, 2, 3]

def append_4():
    b = a.copy()
    b.append(4)

print('Before function:', a)
append_4()
print('After function:', a)
b
Before function: [1, 2, 3]
After function: [1, 2, 3]
[[0, 2, 3], [4, 5, 6], [7, 8, 9]]

These issues don’t arise with strings

a = 'hello'

def append_4(a):
    a += 'goodbye'

print('Before function:', a)
append_4(a)
print('After function:', a)
Before function: hello
After function: hello
a = 'hello'
a += 'goodbye'
a
'hellogoodbye'

If a variable name is overwritten inside a function, the original, unmodified variable still exists in the outer scope

a = [1, 2, 3]
def append_values_v1(a):
    a = np.concatenate([a, [4, 5, 6]])
    print('Inside function 1:', a)

def append_values_v2(a):
    a.extend([4, 5, 6])

print('Before function:', a)
append_values_v1(a)
print('After function 1:', a)
append_values_v2(a)
print('After function 2:', a)
Before function: [1, 2, 3]
NameError: name 'np' is not defined
a = 'hello'

def append_4(a):
    a += 'goodbye'
    return a

print('Before function:', a)
a2 = append_4(a)
print('After function:', a)
print('a2:', a2)
Before function: hello
After function: hello
a2: hellogoodbye

4. List index

Find the index of an element in a list - useful for PA2

list1 = ['a', 'b', 'c']
print(list1.index('a'))
print(list1.index('b'))
print(list1.index('c'))
0
1
2

If the element is not unique, it gives the first entry

list2 = ['a', 'b', 'c', 'a']
print(list2.index('a'))
0

To get all indices for a value

import numpy as np

print(np.nonzero((np.array(list2) == 'a').astype(int))[0])
print(np.where(np.array(list2) == 'a')[0])
[0 3]
[0 3]

5. PA1 review

Discussion and review my solution