2. Python stuff not done in MPR

2.1. List comprehensions

This is a common pattern - accumulating into a list:

[1]:
inputs = [2, 1, 3, 2, 4, 5, 6]
result = []  # Start with an empty list
for i in inputs:  # Iterate over an input list
    if i < 4:  # if some condition holds
        result.append(i**2)  # Append the result of a calculation
result
[1]:
[4, 1, 9, 4]

It’s common enough that Python includes dedicated syntax for this:

[2]:
result = [i**2 for i in inputs if i < 4]
result
[2]:
[4, 1, 9, 4]

2.2. Dictionaries

You should be familiar with lists, which are ordered container types.

[1]:
lst = [1, 2, 3]

We can retrieve elements from the list by indexing it

[4]:
lst[1]
[4]:
2

A dictionary gives us a container like a list, but the indexes can be much more general, not just numbers but strings or sympy variables (and a whole host of other types)

[5]:
dic = {'a': 100, 2: 45, 100: 45}
dic['a']
[5]:
100

When we solve an equation in sympy, the result is a dictionary

[6]:
import sympy
sympy.init_printing()
[7]:
x, y = sympy.symbols('x, y')
[8]:
solution = sympy.solve([x - y, 2 + 2*x + y], [x, y])
[9]:
solution
[9]:
$$\left \{ x : - \frac{2}{3}, \quad y : - \frac{2}{3}\right \}$$
[10]:
type(solution)
[10]:
dict

This means we can find the value of one of the answers by indexing.

[11]:
solution[x]
[11]:
$$- \frac{2}{3}$$

2.3. Tuples

You are familiar with lists:

[12]:
x = 1, 2, 3

a, b, c = x


a, b, c = 1, 2, 3



[13]:
def f(x):
    Ca, Cb, Cc = x
[14]:
l = [1, 2, 3, 4]
[15]:
type(l)
[15]:
list

Tuples are like lists, but they are created with commas:

[16]:
t = 1, 2, 3, 4
[17]:
type(t)
[17]:
tuple

In some cases it is useful to use parentheses to group tuples (but note that they are not required syntax:

[18]:
t2 = (1, 2, 3, 4)
[19]:
type(t2)
[19]:
tuple

It is important to understand that the comma, not the parentheses make tuples:

[20]:
only_one = (((((((1)))))))
[21]:
type(only_one)
[21]:
int
[22]:
only_one = 1,
[23]:
type(only_one)
[23]:
tuple
[24]:
len(only_one)
[24]:
$$1$$

The only exception to this rule is that an empty tuple is built with ():

[25]:
empty = ()
[26]:
type(empty)
[26]:
tuple
[27]:
len(empty)
[27]:
$$0$$

The differences between tuples and lists are that tuples are immutable (they cannot be changed in place)

[28]:
l.append(1)

If we were to run

t.append(1)

We would see

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-41-c860940312ad> in <module>()
----> 1 t.append(1)

AttributeError: 'tuple' object has no attribute 'append'

2.3.1. Tuple expansion

A very useful and general feature of the assignment operator in Python is that tuples will be expanded and assigned in matched patterns:

[29]:
a, b = 1, 2

This is quite sophisticated and can handle nested structures and expanded to lists:

[30]:
[(a, b,), c, d] = [(1, 2), 3, 4]

2.4. The for loop in Python

This talk is excellent for understanding the way that Python “wants” to use the for loop

2.4.1. zip

Let’s say we’re trying to calculate the credit-weighted average of a student’s marks using loops:

[31]:
credits = [8, 16, 8, 8, 16]
marks = [75, 60, 60, 75, 45]

One way is to use indexing:

[32]:
weightedsum = 0
creditsum = 0
for i in range(len(credits)):
    weightedsum += credits[i]*marks[i]
    creditsum += credits[i]

avg = weightedsum/creditsum
print(avg)
60.0

But Python supplies a method which allows us to iterate directly over the pairs:

[33]:
weightedsum = 0
creditsum = 0
for credit, mark in zip(credits, marks):
    weightedsum += credit*mark
    creditsum += credit

avg = weightedsum/creditsum
print(avg)
60.0

This zip function returns an iterator which groups its inputs into tuples, suitable for expansion in the for loop. We can see the effect if we convert to a list:

[34]:
list(zip(credits, marks))
[34]:
$$\left [ \left ( 8, \quad 75\right ), \quad \left ( 16, \quad 60\right ), \quad \left ( 8, \quad 60\right ), \quad \left ( 8, \quad 75\right ), \quad \left ( 16, \quad 45\right )\right ]$$

These pairs are then assigned out to the arguments in the for loop above

2.5. lambda

Some functions expect functions as arguments. For instance, scipy.optimize.fsolve solves equations numerically:

[35]:
def f(x):
    return x**2 - 3
[36]:
import scipy
[37]:
import scipy.optimize
[38]:
scipy.optimize.fsolve(f, 2)
[38]:
array([1.73205081])

For very simple functions, lambda allows us to construct functions in a more compact way and not give them a name:

[39]:
scipy.optimize.fsolve(lambda x: x**2 - 3, 2)
[39]:
array([1.73205081])

The function constructed by lambda works the same as the one constructed by def in most ways. My recommendation is to use lambda with caution. It is never necessary to use lambda. I include this section mostly so that you can understand what this does if you encounter it in documentation.