# 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]
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)
----> 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.