Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
| Download

This repository contains the course materials from Math 157: Intro to Mathematical Software.

Creative Commons BY-SA 4.0 license.

Views: 3037
License: OTHER
Kernel: Python 3 (Ubuntu Linux)

Math 157: Intro to Mathematical Software

UC San Diego, winter 2018

January 19, 2018: Introduction to Python (part 3 of 3)

Administrivia:

  • Homework 1 due Friday, January 19 at 8pm. Remember, put all your work in the "assignments/2018-01-19" folder; it will be collected automatically after the deadline passes.

  • For problem 5b, I will give full credit if your list l1 contains four objects. Hint: a dictionary key must be unchangeable (or in Python jargon "immutable"), and if it is a composite type then its components must also be unchangeable.

  • For problem 6, xx and yy are supposed to be integers.

  • The waitlist will be frozen at the end of the day. Thereafter, I will manually add students from the waitlist as space becomes available.

Mutability

There is a serious subtlety with how Python lists (and certain other types, see below) work. This is a frequent source of errors for new Python users.

In Python, objects are copied by reference, not by value. That is, when you perform the assignment b = a, Python does not assign a copy of the value of a to the new variable b; rather, a and b are both references to the same object in memory. (This is different from MATLAB, for example.)

This is already true for basic objects like integers, but isn't really an issue.

a = 3 b = a # Now a and b point to the same object in memory, a copy of the integer 3. b += 4 # This creates a new integer and makes b point to it instead, so a remains intact. print(a,b)
3 7
print(id(a), id(b)) # This proves that a and b are now pointing to different places in memory
10919488 10919616

By contrast, basic operations on lists change the original list, rather than creating a new one. This can lead to unexpected side effects.

a = [3,5] b = a b.append(7) print(a)
[3, 5, 7]
print(id(a), id(b))
139768555546440 139768555546440

Different programming languages make very subtle and different design choices regarding this and other aspects of programming (see homework).

The copy by reference choice of Python makes lists potentially subtle. For example:

v = [1,2,3] w = [v, v, [1,2,3]] w
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
w[0][0] = 'sage'

Now what the heck is w equal to? (Wait until lots of people in class think this through.)

print(w)
[['sage', 2, 3], ['sage', 2, 3], [1, 2, 3]]
[id(x) for x in w]
[139768552017672, 139768552017672, 139768552017608]

Moral: Don't judge an object by its cover (how it prints). Just because something prints out at [[1, 2, 3], [1, 2, 3], [1, 2, 3]] doesn't mean you know what that something is! In Python, things can be very subtle.

If you don't get this point, you might hate Python. If you do get it, you might start to see huge power and flexibility in what Python offers... and understand why Python might be the most popular programming language in data science (and why DS10 is taught in Python).

Shallow and deep copies

In light of the previous issue, let's take a closer look at how copying works. Let's try the copy module from Python's standard library: https://docs.python.org/2/library/

import copy # this is how you import a module in Python # Let's do the tab completion here to see what new functions we got
v = [1,2,3] w = [v, v, [1,2,3]] w1 = copy.copy(w) # how to use a function defined in a module w1[0][0] = 'hi' print("w1 = ", w1) print("w = ", w) # not much of a copy!!! It just copies the top level references
w1 = [['hi', 2, 3], ['hi', 2, 3], [1, 2, 3]] w = [['hi', 2, 3], ['hi', 2, 3], [1, 2, 3]]
# Question: will this change w? w1[0] = 'hi' print(w) # what do you expect print(w1)
[['hi', 2, 3], ['hi', 2, 3], [1, 2, 3]] ['hi', ['hi', 2, 3], [1, 2, 3]]
print(id(w),id(w1))
139768551735560 139768550996552

Another approach to the shallow copy uses slicing, which is actually more general: given a list, you can use it to produce a new list consisting of some subrange of objects from the first list. The trivial case, where the subrange is the whole list, is equivalent to shallow copy.

v = [1,2,3,4,5,6] print(v[2:4]) # Same endpoint conventions as for range... print(v[3:-1]) # ... except that negative indices are allowed! print(v[2:]) # Can also omit one endpoint... print(v[:]) # ... or both.
[3, 4] [4, 5] [3, 4, 5, 6] [1, 2, 3, 4, 5, 6]
v = [1,2,3] w = [v, v, [1,2,3]] x = w[:] # Another form of shallow copying.

A shallow copy is to be distinguished from a deep copy, which can be modified with no side effects on the original.

# We already imported copy, no need to do it again. v = [1,2,3] w = [v, v, [1,2,3]] w1 = copy.deepcopy(w) # uses more memory; causes more work -- and that's why we want options w1[0][0] = 'hi' print("w1 = ", w1) print("w = ", w) # yep, not touched by changing w
w1 = [['hi', 2, 3], ['hi', 2, 3], [1, 2, 3]] w = [[1, 2, 3], [1, 2, 3], [1, 2, 3]]

MORAL: Python is a real programming language designed (and very occasionally modified) by software engineers. As a result, it takes into account important subtleties about programming to which mathematicians tend to be oblivious.

By contrast, most legacy mathematical software systems were built by mathematicians (or physicists) whose expertise and interests lay far away from programming. The interfaces with these systems are themselves programming languages, but were designed by people without much familiarity with software engineering; this tends to cause problems in the long run.

Tuples

Tuples are a data structure that look quite similar to lists.

  • Like Python lists, except you use parentheses instead of square brackets to create them.

  • Tuples are immutable in the sense that you can't change the number of entries or what they reference. However, you might be able to change the referenced thing itself (e.g., if it is a list).

v = [['x','y'], 2, 5] # a list t = (['x', 'y'], 2, 5) # a tuple print("v =", v) # The input to a function is implicitly a tuple! print("t =", t)
v = [['x', 'y'], 2, 5] t = (['x', 'y'], 2, 5)
t[1] = 3 # not allowed!
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-31-08a06e0316d9> in <module>() ----> 1 t[1] = 3 # not allowed! TypeError: 'tuple' object does not support item assignment
v[1] = 3
v.append("something") # lists allow for lots of exciting ways of changing them v
[['x', 'y'], 3, 5, 'something']
del v[3] v
[['x', 'y'], 3, 5]
t.append(4) # not so for tuples
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-35-ee693b63bed6> in <module>() ----> 1 t.append(4) # not so for tuples AttributeError: 'tuple' object has no attribute 'append'
# However, and this is critical -- you can change v[0] or t[0] itself! v[0].append('z') v
[['x', 'y', 'z'], 3, 5]
t[0].append('z') # this doesn't change t -- # think of t as references to 3 specific objects -- you are changing one of the referenced objects, not t itself!
a = (1,[2,3],4) a[1].append(5) print(a)
t

Try this now: Write a function that takes as input a tuple and returns the sorted version of the tuple.

# Hints to make this really easy: v = (1,7, 4) sorted(v) # returns sorted list obtained from a tuple (or another suitable data type)
[1, 4, 7]
tuple([4,7,8]) # tuple of a list makes a tuple
(4, 7, 8)
list((2,3,4))
[2, 3, 4]
def sorted_tuple(x): return tuple(sorted(x))
sorted_tuple((7,3,5)) # test your function!
(3, 5, 7)

Dictionaries

A Python dictionary is the Python version of what computer scientists might call an "associative array" or a "lookup table".

It's what us mathematicians call a "function" from one finite set to another (not to be confused with a Python "function", which is really a subroutine).

d = {5:25, 10:100, 3:8, 3:'another value'}
type(d)
dict
d
{3: 'another value', 5: 25, 10: 100}
d[3]
'another value'
d[7] # should fail -- since we didn't say what 7 maps to.
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-49-bac296b2ff6b> in <module>() ----> 1 d[7] # should fail -- since we didn't say what 7 maps to. KeyError: 7

The values that you use to index a dictionary are called keys.

7 in d.keys()
False
for a in d.keys(): print(id(a))
10919712 10919488 10919552

Aside: Python has a form of flow control for catching runtime errors (exception handling).

try: print(d[7]) except KeyError: print("there was an error")
there was an error
try: print(d[10]) except KeyError: print("there was an error")
100

But you can also specify a default value in case the key doesn't exist. (For example, you might want to treat all unset values as 0.)

d.get(7, 'default_value')
'default_value'
d.get?

You can add, modify, or delete values to a dictionary after it has been created; it is therefore another mutable type, like lists.

d[10] = 7 print(d)
{10: 7, 3: 'another value', 5: 25}
d[9] = 0 print(d)
{9: 0, 10: 7, 3: 'another value', 5: 25}
del d[9] print(d)
{10: 7, 3: 'another value', 5: 25}

An object can only be used as a key if it cannot be changed. (Officially, hash(x) has to return without an error.)

d = {2:5, None:1, "334":7, 2.0:9}
d = {[2,3]: 1} # No good, lists are changeable
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-65-ed4910cf88e7> in <module>() ----> 1 d = {[2,3]: 1} # No good, lists are changeable TypeError: unhashable type: 'list'
d = {(2,3): 1} # This is okay, because tuples are unchangeable...
d = {(2,[3,4]): 1} # ... unless they contain changeable objects.
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-67-9ab58a71f613> in <module>() ----> 1 d = {(2,[3,4]): 1} # ... unless they contain changeable objects. TypeError: unhashable type: 'list'

Exercise: Which of the following are immutable, i.e., hash-able, so they can be used as keys for a dictionary?

  • the string "Foo"

  • the empty list []

  • the number 3.14

  • the dictionary {7:10, 3:8}

  • the tuple ('foo', 'bar')

Make a dictionary that has all the immutable objects above as keys... and anything you want (hashable or not) as values.

Here's a dictionary with a Python function as a value:

def length(self): return sqrt(self['x']^2 + self['y']^2) self = {'x':10, 'y':25, 'length':length} self

Using dicts with data and functions, you could try to model mathematical objects and work with them. But the notation is very awkward!

Python classes do much the same thing, with vastly nicer notation. We will see these in action when we start working with Sage.

class Vector: def __init__(self, x, y): self.x = x self.y = y print("I just initialized myself") def length(self): print("hi this is length") return (self.x**2 + self.y**2)**0.5 def __add__(self, right): return Vector(self.x+right.x, self.y + right.y) # change to Vector3 def __repr__(self): return "(%s, %s)"%(self.x, self.y)
Vector(2, 3)
I just initialized myself
(2, 3)
u = Vector(2, 3) v = Vector(4, 5) u + v
I just initialized myself I just initialized myself I just initialized myself
(6, 8)
u.length()
hi this is length
3.605551275463989