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 17, 2018: Introduction to Python (part 2 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.

Extra announcements (not in your copy of this file):

  • My office hours are Thursday 4-5 in APM 7202.

  • I will process the waitlist at the end of the week. In addition to priority on the waitlist, I will also take into account participation in the course so far (in-class plus homework).

  • There are two extra files in today's folder which you should ignore. You should have open "2018-01-17.ipynb".

Functions (carryover from last lecture)

Functions are batches of code that can be called from within other code.

A function takes zero or more parameters, and produces zero or more return values. A function may also have side effects, although it cannot change the values of variables outside of the function (that is, variables inside a function are locally scoped).

Just like assignments of variables, the definition of a function persists between cells, unless you overwrite it by assigning the same name to another function. (If you really want, you can also type del f which removes the definition of f without replacing it. This works for both variables and functions; in fact, a function is just a special type of value you can assign to a variable.)

def name_of_function(argument1, argument2): x = 8 print("the first argument is ", argument1, " and the second is ", argument2) return argument1 + argument2
# Let's see if that worked! name_of_function("foo", "bar")
the first argument is foo and the second is bar
'foobar'
output = name_of_function(15, 19) # This doesn't print the return value, but there is a side effect
the first argument is 15 and the second is 19
print(output) # Now let's see the return value
34
name_of_function('abc', 123) # This will fail -- you can't add a string and a number in Python
the first argument is abc and the second is 123
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-8-ee53d52ce0bb> in <module>() ----> 1 name_of_function('abc', 123) # This will fail -- you can't add a string and a number in Python <ipython-input-5-b6c722822b73> in name_of_function(argument1, argument2) 2 x = 8 3 print("the first argument is ", argument1, " and the second is ", argument2) ----> 4 return argument1 + argument2 TypeError: Can't convert 'int' object to str implicitly

Try this now: last time, you wrote a code block that, given an integer x, returns 1, -1, or 0 depending on whether the integer is positive, negative, or zero. Now create a function my_sign that does the same for its input.

def my_sign(x): if x > 0: return 1 if x < 0: return -1 return 0

Try this now: write a function that takes four parameters and returns their average. Try the test cases below to see if your function is working.

def avg(x1, x2, x3, x4): return (x1+x2+x3+x4)//4
u = avg(1,3,5,7) # Should return 4
type(u)
int
avg(1,2,3,4) # What do you expect to see here? Are you surprised? Should we try this in Python Tutor?
2
type(4.0)
float

Try this now:

Use a while loop to write a function largest_power_of_2 that takes as input a positive integer n and returns the largest power of 2 that is less than n. Your loop should start with pow=1 and while pow*2 is less than n replaces pow with pow*2; once the loop ends, return pow.

def largest_power_of_2(n): pow = 1 while pow*2 < n: pow *= 2 return pow #insert code here
# Test this out! for x in range(1, 18): print(x, largest_power_of_2(x))
1 1 2 1 3 2 4 2 5 4 6 4 7 4 8 4 9 8 10 8 11 8 12 8 13 8 14 8 15 8 16 8 17 16

Aside: the syntax for exponentiation in Python is 2**n rather than 2^n.

Lists

A list is a Python type consisting of an ordered list of other objects. This is similar to the concept of an array in other programming languages; however, in Python the objects of a list can be of different types (e.g., a single list can simultaneously contain numbers, strings, other lists, etc.).

One can create a list by specifying its contents enclosed in square brackets. The empty list is allowed.

If l is a list, then the objects of the list can be retrieved as l[0], l[1], ...; note that the first index is 0, not 1. The last items in a list can be retrieved as l[-1], l[-2], ....

l = [2, 3, "ab"] print(l[0]) print(l[-1])
2 ab

Here are some examples of operations on lists.

l = [2,5,3] print(len(l)) # Length of a list
3
print(4 in l) # Test for membership in a list
False
l += [6,7] # The + operator concatenates lists print(l)
[2, 5, 3, 6, 7]
del l[3] # Remove an element from a list print(l)
[2, 5, 3, 7]
print(l[:]) # This creates a new list consisting of a certain range of elements of the original list.
[2, 5, 3, 7]

Many of the available operations on lists, and on other Python objects, are accessed using a slightly different syntax than the one used so far. Look at these examples.

l.sort() # Sort the list print(l)
[2, 3, 5, 7]
l.append(8) # Add an element to a list print(l)
[2, 3, 5, 7, 8]

These kinds of functions are specific to a certain type of object, in this case lists; they are called methods of the object class. This is a symptom of the fact that Python is an object-oriented programming language; we will see later how Sage takes advantage of this architecture to implement mathematical objects.

In the meantime, I point out two ways to find out more about the methods associated to a particular class of objects.

  1. Use tab-completion: type l. and hit the Tab key to see a list of methods associated to l. If you have typed some letters after the period before hitting Tab, you'll only get objects whose names match the initial string you typed.

  2. Look at the page from the Python documentation corresponding to the type. For example, for lists this page is https://docs.python.org/2/tutorial/datastructures.html.

# Try tab-completion here! l.i

List comprehensions

Mathematicians are fond of constructing sets out of other sets. Python offers a similar syntax:

[expression(x) for x in [list]] [expression(x) for x in [list] if something(x)] [expression(x, y) for x in [list] for y in [another list] if something(x, y)]

et cetera.

[n for n in range(-3,5)] # you can give a start value for range
[-3, -2, -1, 0, 1, 2, 3, 4]
[n*2 for n in range(10)]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
[n for n in range(20) if n%2 == 0] # What does % do again?
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
[[x,y] for x in range(4) for y in ['X','Y','Z'] if x%2 == 0]
[[0, 'X'], [0, 'Y'], [0, 'Z'], [2, 'X'], [2, 'Y'], [2, 'Z']]

You can typically rewrite a list comprehension as a for loop (or nested for loops).

v = [] for n in range(10): v.append(n) print(v)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Helpful hint: The order of the for loops and the if are exactly the same as in the list comprehension.

v = [] for x in range(4): for y in ['X','Y','Z']: if x % 2 == 0: v.append([x,y]) print(v)
[[0, 'X'], [0, 'Y'], [0, 'Z'], [2, 'X'], [2, 'Y'], [2, 'Z']]

Try this now:

Construct the following list using a list comprehension and the function my_sign you wrote earlier (which should still be declared):

[[-3,'negative'], [-2,'negative'], [-1,'negative'], [0,'zero'], [1,'positive'], [2,'positive'], [3,'positive']]

If you have extra time, do the same using a for loop.

Advanced note: the definition of list comprehension does now allow for an else clause. However, there is an inline version of if/else that is sometimes useful in this context.

[(x if x < 5 else -1) for x in range(10)]
[0, 1, 2, 3, 4, -1, -1, -1, -1, -1]

Warning: lists are mutable

There is a serious subtlety with how Python lists works. 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))
140567236775816 140567236775816

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]
[140651206536008, 140651206536008, 140651206546952]

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 copy. # Let's do the tab completion here to see what new functions we got
File "<ipython-input-38-f13a981896f0>", line 2 copy. # Let's do the tab completion here to see what new functions we got ^ SyntaxError: invalid syntax
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
[['hi', 2, 3], ['hi', 2, 3], [1, 2, 3]]

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-54-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 # not so for tuples
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-58-3bf7a14c4b06> in <module>() ----> 1 t.append # 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)
(1, [2, 3, 5], 4)
t
(['x', 'y', 'z'], 2, 5)

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)
def sorted_tuple(x): return # what goes here? something
sorted_tuple((7,3,5)) # test your function!