Today: python copying/nesting, dictionary introduction, dict-count algorithm

Python = And Copies

What is the memory drawing for this code, i.e. what does = do?

>>> lst1 = [1, 2, 3, 4]
>>> lst2 = lst1

Answer: In general, a = sets a to point to the same thing as b. It does not make a copy.

So for the above, there is one list, and now both variables point to it. alt: two vars point to one list

We can see this is the case by modifying the list through one variable, seeing the change on the other variable.

>>> lst1.append(13)
>>> lst1
[1, 2, 3, 4, 13]
>>> lst2
[1, 2, 3, 4, 13]

Remember: = does not copy the data structure, instead it changes what the variable to the left of the = points to.


Nested - An Introduction

Next week we'll pursue examples where structures nested inside of other structures, such as lists inside of lists.

We'll introduce a couple basic techniques today to get started down this path.

Outer List of Lists

Suppose I have a list variable "outer". Inside this list are lists of numbers.

The list .append() function can add anything to the end of a list, including another list.

>>> outer = []
>>> outer.append([1, 2])
>>> outer.append([3, 4])
>>> outer.append([5])
>>> outer
[[1, 2], [3, 4], [5]]
outer -> [[1, 2], [3, 4], [5]]

Q1: How to Refer to the nested [5] list?

How to obtain a reference to the list [5] in outer?

A: What index is it at within outer? 2

So access that list as outer[2]

>>> outer
[[1, 2], [3, 4], [5]]
>>> outer[2]
[5]

Q2: What if we set a var nums = outer[2]?

The expression outer[2] is a reference to the [5] list.

>>> outer = [[1, 2], [3, 4], [5]]
>>> nums = outer[2]
>>> 
>>> nums
[5]

The expression outer[2] is a reference to the nested list [5]. The line nums = outer[2] sets the nums variable to point to the nested list.

Key Technique We will very often set a variable in this way, pointing to the nested structure, before doing operations on it. alt: outer points to list, nums points to nested [5] list

Q3: How To Append 6 on the [5]?

The variable nums is pointing to the nested list [5]. Call nums.append(6) to append, changing the nested list. Looking at the original outer list, we see its contents are changed too. This shows that the nums variable really was pointing to the list nested inside of the outer list.

>>> nums.append(6)
>>> nums
[5, 6]
>>> outer
[[1, 2], [3, 4], [5, 6]]
>>>
>>> # Could access list as outer[2] also:
>>> outer[2].append(7)
>>> outer
[[1, 2], [3, 4], [5, 6, 7]]
>>>

(optional) Exercise: add_99()

The code in this problem builds on the basic outer/nums example above. > add_99()

add_99(outer): Given a list "outer" which contains lists of numbers. Add 99 to the end of each list of numbers. Return the outer list.

The nest1 section on the server has introductory problems with nested structures.


Python dict - Hash Table - Fast

For more details sees the chapter in the Guide: Dict

Restaurant - From Chaos to Order

The dict lets us choose a key to organize the incoming data.

Suppose you are out ordering dinner at a restaurant, and the order is proceeding in a chaotic way, with the people throwing out their orders out in random order:

Alice: I'd like to start with a cup of gazpacho
Bob:   I like beignets for dessert
Alice: Then a ceaser salad
Zoe:   I'll have lasagna
Bob:   Actually two orders of beignets
Alice: Then I'll have tacos
Bob:   And a hot dog
...

People mention the parts of their order piece by piece in no organized order - fine.

For Dict, we get to choose the key to organize the data. Here, we choose the name as the key and file each bit of the oder under the name.

Using each name as the key, we get the data organized like this:

Alice: gazpacho, ceasar, tacos
Bob: hot dog, two orders of beignets
...

This is what the dictionary gives us - data comes in randomly and the dict can organize it by a chosen "key" part of the data.


Dict Basics

alt:python dict key/value pairs 'a'/'alpha' 'g'/'gamma' 'b'/'beta'

Dict-1 - Set key:value into Dict

>>> d = {}             # Start with empty dict {}
>>> d['a'] = 'alpha'   # Set key/value
>>> d['g'] = 'gamma'
>>> d['b'] = 'beta'
>>> # Now we have built the picture above
>>> # Python can input/output a dict using
>>> # the literal { .. } syntax.
>>> d
{'a': 'alpha', 'g': 'gamma', 'b': 'beta'}
>>>

Dict-2 - Get value out of Dict

>>> s = d['g']         # Get by key
>>> s
'gamma'
>>> d['b']
'beta'
>>> d['a'] = 'apple'   # Overwrite 'a' key
>>> d['a']
'apple'
>>>
>>> # += modify str value
>>> d['a'] += '!!!'
>>> d['a']
'apple!!!'
>>>
>>> d
{'a': 'apple!!!', 'g': 'gamma', 'b': 'beta'}
>>>

Dict-3 - Get Error / "in" Test

>>> # Can initialize dict with literal
>>> d = {'a': 'alpha', 'g': 'gamma', 'b': 'beta'}
>>>
>>> val = d['x']         # Key not in -> Error
Error:KeyError('x',)
>>>
>>> 'a' in d             # "in" key tests
True
>>> 'x' in d
False
>>> 
>>> # Guard pattern (else ..)
>>> if 'x' in d:
      val = d['x']
>>>

Dict Logic - Always With Key not Value

The get/set/in logic of the dict is always by key. The key for each key/value pair is how it is set and found. The value is actually just stored without being looked at, just so it can be retrieved later. In particular get/set/in logic does not use the value.

>>> d = {'a': 'alpha', 'g': 'gamma', 'b': 'beta'}
>>>
>>> d['a']          # Get value by key
'alpha'
>>> 'g' in d        # Check key in
True
>>>
>>> d['alpha']      # NO Get by value
KeyError: 'alpha'
>>> 
>>> 'alpha' in d    # NO Check value in
False
>>>

Summary of Dict: Set, Get, in, It's fast


Dict Meals Structure

The dictionary is like memory - put something in, later can retrieve it.

Problems below use a "meals" dict to remember what food was eaten under the keys 'breakfast', 'lunch', 'dinner'.

>>> meals = {}
>>> meals['breakfast'] = 'apple'
>>> meals['lunch'] = 'donut'
>>>
>>> # time passes, other lines run
>>>
>>> # what was lunch again?
>>> meals['lunch']
'donut'
>>> 
>>> # did I have breakfast and dinner yet?
>>> 'breakfast' in meals
True
>>> 'dinner' in meals
False
>>>

Basic Dict Code Examples - Meals

Look at the dict1 "meals" exercises on the experimental server

> dict1 meals exercises

With the "meals" examples, the keys are 'breakfast', 'lunch', 'dinner' and the values are like 'hot dot' and 'bagel'. A key like 'breakfast' may or may not be in the dict, so need to "in" check first. No loops in these.

Foreshadowing: Think About dict[key]

Often pulling up a value by its key

val = d[key]      # Warning: error if key not in d

Think first - what happens if the key is not in there? May need separate code to handle the case where the wanted key is not in the dict, like this"

if key not in d:  # Handle not-in case
    return xxxx

val = d[key]      # Here [key] works

1. bad_start()

> bad_start()

bad_start(meals): Return True if there is no 'breakfast' key in meals, or the value for 'breakfast' is 'candy'. Otherwise return False.

Try running code without the "in" check - see the KeyError.

bad_start() Solution Code

Question: is the meals['breakfast'] == 'candy' line safe? Yes. The earlier if-statement guards the [ ].

def bad_start(meals):
    if 'breakfast' not in meals:
        return True
    if meals['breakfast'] == 'candy':
        return True
    return False
    # Can be written with "or" / short-circuiting
    # if 'breakfast' not in meals or meals['breakfast'] == 'candy':

2. enkale()

> enkale()

enkale(meals): If the key 'dinner' is in the dict with the value 'candy', change the value to 'kale'. Otherwise leave the dict unchanged. Return the dict in all cases.

enkale() Solution Code

Demo: work out the code, see key error

Cannot access meals['dinner'] in the case that dinner is not in the dict, so need logic to avoid that case.

Here is an outer if-statement to check first that the key is in, only accessing the key if it is in.

def enkale(meals):
    if 'dinner' in meals:
        if meals['dinner'] == 'candy':
            meals['dinner'] = 'kale'
    return meals

Nested if-statements like that in effect require both tests to be True. So equivalently, we could write it with a single if statement, combining the 2 tests with "and":

def enkale(meals):
    if 'dinner' in meals and meals['dinner'] == 'candy':
        meals['dinner'] = 'kale'
    return meals

This is similar to the "guard" pattern we saw with string-loops. The and/short-circuiting processes the left-right, stopping as soon as there's a False.

Exercise: is_boring()

> is_boring()

is_boring(meals): Given a "meals" dict. We'll say the meals dict is boring if lunch and dinner are both present and are the same food. Return True if the meals dict is boring, False otherwise.

Idea: could solve without worrying about the KeyError first. Then put in the needed "in" guard checks.


Dict-Count Algorithm

Say we have this list of strings:

strs = ['a', 'b', 'a', 'c', 'b']

Run the dict-count algorithm to compute this counts dict:

counts == {'a': 2, 'b': 2, 'c': 1}

1. Have a key for each distinct value in the data

2. Its value is the count of occurrences of that key in the data

On the experimental server, see the dict2-Count exercises

Dict-Count Algorithm Steps

Do the following for each s in strs. At the end, counts dict is built.

Dict-Count abacb

Go through these strs
strs = ['a', 'b', 'a',  'c',  'b']

Sketch out counts dict here:

Counts dict ends up as {'a': 2, 'b': 2, 'c': 1}:

alt: counts a 2 b 2 c 1

1. str-count1() - if/else

> str_count1()

str_count1 demo, canonical dict-count algorithm

str_count1() Solution

def str_count1(strs):
    counts = {}
    for s in strs:
        # s not seen before?
        if s not in counts:
            counts[s] = 1   # s first time
        else:
            counts[s] +=1   # s later times
    return counts

2. str-count2() - Unified/Invariant Version, no else

> str_count2()

Standard Dict-Count Code - Unified/Invariant Version

def str_count2(strs):
    counts = {}
    for s in strs:
        # fix counts/s if not seen before
        if s not in counts:
            counts[s] = 0
        # Unified: now s is in counts one way or
        # another, so this works for all cases:
        counts[s] += 1
    return counts

Int Count - Exercise

> int_count()

Apply the dict-count algorithm to a list of int values, return a counts dict, counting how many times each int value appears in the list.

Char Count - Exercise

May get to this one, or students do on their own.

> char_count()

Apply the dict-count algorithm to chars in a string. Build a counts dict of how many times each char, converted to lowercase, appears in a string so 'Coffee' returns {'c': 1, 'o': 1, 'f': 2, 'e': 2}.


We likely will not have time to get to this today - a bit of drawing code.

(optional) Flex Arrow Example

Download the flex-arrow.zip to work this fun little drawing example.

Recall: floating point values passed into draw_line() etc. work fine.

Draw Arrow Output

Ultimately we want to produce this output: alt: 2 arrows

The "flex" parameter is 0..1.0: the fraction of the arrow's length used for the arrow heads. The arms of the arrow will go at a 45-degree angle away from the horizontal.

Starter Code: Left Arrow + flex

Specify flex on the command line so you can see how it works. Close the window to exit the program. You can also specify larger canvas sizes.

$ python3 flex-arrow.py -arrows 0.25
$ python3 flex-arrow.py -arrows 0.15
$ python3 flex-arrow.py -arrows 0.1 1200 600

draw_arrow() Starter Code

Look at the draw_arrow() function. It is given x,y of the left endpoint of the arrow and the horizontal length of the arrow in pixels. The "flex" number is between 0 .. 1.0, giving the head_len - the horizontal extent of the arrow head - called "h" in the diagram. Main() calls draw_arrow() twice, drawing two arrows in the window.

This starter code the first half of the drawing done.

def draw_arrow(canvas, x, y, length, flex):
    """
    Draw a horizontal line with arrow heads at both ends.
    It's left endpoint at x,y, extending for length pixels.
    "flex" is 0.0 .. 1.0, the fraction of length that the
    arrow heads should extend horizontally.
    """
    # Compute where the line ends, draw it
    x_right = x + length - 1
    canvas.draw_line(x, y, x_right, y)

    # Draw 2 arrowhead lines, up and down from left endpoint
    head_len = flex * length
    # what goes here?

Code For Left Arrow Head

alt: work out arrow-head math

With head_len computed - what the two lines to draw the left arrow head? This is a nice visual algorithmic problem.

Code to draw left arrow head:

    # Draw 2 arrowhead lines, up and down from left endpoint
    head_len = flex * length
    canvas.draw_line(x, y, x + head_len, y - head_len)  # up
    canvas.draw_line(x, y, x + head_len, y + head_len)  # down

Exercise/Demo - Right Arrowhead

Here is the diagram again. alt: work out arrow-head math

Add the code to draw the head on the right endpoint of the arrow. The head_len variable "h" in the drawing. This is a solid, CS106A applied-math exercise. In the function, the variable x_right is the x coordinate of the right endpoint of the line:
x_right = x + length - 1

When that code is working, this should draw both arrows (or use the flex-arrow-solution.py):

$ python3 flex-arrow.py -arrows 0.1 1200 600

draw_arrow() Solution

def draw_arrow(canvas, x, y, length, flex):
    """
    Draw a horizontal line with arrow heads at both ends.
    It's left endpoint at x,y, extending for length pixels.
    "flex" is 0.0 .. 1.0, the fraction of length that the arrow
    heads should extend horizontally.
    """
    # Compute where the line ends, draw it
    x_right = x + length - 1
    canvas.draw_line(x, y, x_right, y)

    # Draw 2 arrowhead lines, up and down from left endpoint
    head_len = flex * length
    canvas.draw_line(x, y, x + head_len, y - head_len)  # up
    canvas.draw_line(x, y, x + head_len, y + head_len)  # down

    # Draw 2 arrowhead lines from the right endpoint
    # your code here
    pass
    canvas.draw_line(x_right, y, x_right - head_len, y - head_len)
    canvas.draw_line(x_right, y, x_right - head_len, y + head_len)

Arrow Observations


A Little Mind Warping

Now we're going to show you something a little beyond the regular CS106A level, and it's a teeny bit mind warping.

Arrow Trick Mode

alt: negative h arrow head

-trick Mode

Run the code with -trick like this, see what you get.

$ python3 flex-arrow.py -trick 0.1 1000 600