Monday, February 10th¶

Project 1: Prime or not prime¶

  • Due Friday at 11:59 PM
  • Tasks to be completed:
    • Find the first 20 false primes within a reasonable runtime.
      • It will help to make a is_false_prime function that uses both is_prime and is_prime_like.
      • You will need to use a while loop to find 20 false primes, since we don't know how high of integers we will need to check before finding 20.
      • It would make sense to have an argument for the number of false primes to be found with a default value n=20.
    • Write a function primary that finds primary decompositions (including multiplicity).
      • First, write a function that just finds a list of prime factors (ignoring multiplicty).
      • Second, think of how to account for multiplicity.
        • It will be helpful to use a while loop here to see how many times a prime factor divides the number.

List slicing¶

In [1]:
my_list = ['a','b','c','d','e','f','g','h']
In [2]:
my_list[0]
Out[2]:
'a'
In [3]:
my_list[-1]
Out[3]:
'h'

It is often useful to get a subset of a list. Suppose we want a sublist consisting of the index 3, 4, 5, and 6 elements:

In [4]:
my_sublist = []

for i in range(len(my_list)):
    if i >= 3 and i <= 6:
        my_sublist.append(my_list[i])
In [5]:
my_sublist
Out[5]:
['d', 'e', 'f', 'g']

A more elegant way to accomplish this task is by using list slicing. The syntax is:

  • my_list[start_index:stopping_index], which returns a sublist with indices greater than or equal to start_index and strictly less than stopping_index.
In [6]:
my_list[3:7]
Out[6]:
['d', 'e', 'f', 'g']
In [15]:
my_list[0:5]
Out[15]:
['a', 'b', 'c', 'd', 'e']
In [17]:
my_list[3:-1]
Out[17]:
['d', 'e', 'f', 'g']

We can also use slicing in the following ways:

  • my_list[start_index:] will start the slice at start_index and go the end of the list.
  • my_list[:stopping_index] will start the list at the beginning (i.e. 0) and proceed until one less than stopping_index.
  • my_list[:] will return the full list
  • my_list[start_index:-1] will start the list at start_index and end at the last index (excluding it)
In [19]:
my_list[:-3]
Out[19]:
['a', 'b', 'c', 'd', 'e']
In [20]:
my_list[3:]
Out[20]:
['d', 'e', 'f', 'g', 'h']

Just like the range function, we can use a third input as a step size for slices:

In [22]:
my_list
Out[22]:
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
In [21]:
my_list[1:-1:2]
Out[21]:
['b', 'd', 'f']
In [24]:
my_list[0:7:3]
Out[24]:
['a', 'd', 'g']
In [25]:
my_list[::4]
Out[25]:
['a', 'e']
In [26]:
integers = []

for i in range(100):
    integers.append(i)
In [32]:
integers[1::2]
Out[32]:
[1,
 3,
 5,
 7,
 9,
 11,
 13,
 15,
 17,
 19,
 21,
 23,
 25,
 27,
 29,
 31,
 33,
 35,
 37,
 39,
 41,
 43,
 45,
 47,
 49,
 51,
 53,
 55,
 57,
 59,
 61,
 63,
 65,
 67,
 69,
 71,
 73,
 75,
 77,
 79,
 81,
 83,
 85,
 87,
 89,
 91,
 93,
 95,
 97,
 99]

Wednesday, February 12th¶

We can also check whether a list contains a particular element using the in operator:

In [7]:
vowels = ['a','e','i','o','u']
In [8]:
'a' in vowels
Out[8]:
True
In [9]:
'b' in vowels
Out[9]:
False
In [11]:
for letter in 'buffalo':
    if letter not in vowels:
        print(letter)
b
f
f
l

The zip function¶

The zip function gives a way to simultaneously iterate through a pair of lists in parallel.

In [15]:
list1 = [0,1,2,3,4,5]
list2 = ['a','b','c','d','e','f']

A pair of nested for loops will iterate through every possible combination of elements of each list.

In [16]:
for item1 in list1:
    for item2 in list2:
        print(item1, item2)
0 a
0 b
0 c
0 d
0 e
0 f
1 a
1 b
1 c
1 d
1 e
1 f
2 a
2 b
2 c
2 d
2 e
2 f
3 a
3 b
3 c
3 d
3 e
3 f
4 a
4 b
4 c
4 d
4 e
4 f
5 a
5 b
5 c
5 d
5 e
5 f

In other situations, the elements of two lists might be in correspondence with one another. This is where we can use the zip function to glue the two lists together in parallel and iterate through the corresponding pairs:

In [17]:
for item1, item2 in zip(list1, list2):
    print(item1, item2)
0 a
1 b
2 c
3 d
4 e
5 f

For example, this may be useful in project 1 if you a list of false primes and a separate list of their corresponding prime factorizations.

List comprehensions¶

Suppose we want to generate a list of square numbers 1**2, 2**2, ..., N**2.

In [18]:
N = 10

squares = []
for n in range(1,N+1):
    square = n**2
    squares.append(square)
    
squares
Out[18]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

We can accomplish this same task in one line by using list comprehension:

In [19]:
squares = [n**2 for n in range(1,N+1)]
squares
Out[19]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

We can also use if statement within a list comprehension:

In [20]:
[n**2 for n in range(1,N+1) if n**2 % 3 == 1]
Out[20]:
[1, 4, 16, 25, 49, 64, 100]
In [21]:
[square for square in squares if square % 3 == 1]
Out[21]:
[1, 4, 16, 25, 49, 64, 100]

Immutable vs mutable objects¶

In [22]:
n = 3
m = n

print(n)
print(m)
3
3
In [23]:
n = n + 5
In [24]:
print(n)
print(m)
8
3
In [25]:
list1 = [1,2,3,4]
list2 = list1

print(list1)
print(list2)
[1, 2, 3, 4]
[1, 2, 3, 4]
In [26]:
list1[0] = 99
In [27]:
print(list1)
print(list2)
[99, 2, 3, 4]
[99, 2, 3, 4]

What's going on here? Setting m = n did not cause changes in n to propogate back to m, but in the list example, settings list2=list1 did cause changes to list1 to propogate to list2.

The difference is that integers immutable objects while lists are mutable. For mutable objects, variable assigments point to a place in memory that stores the obect (in this case a list). That is, list1 and list2 point to the same location in memory. Changes to one affect the other.

  • Immutable objects:
    • Integers
    • Strings
    • Floats
    • Booleans
    • Tuples
  • Mutable objects:
    • Lists
    • Dictionaries
    • Sets
    • NumPy arrays

We can use the id function to check whether two objects share the same space in memory:

In [28]:
list1 = [1,2,3,4]
list2 = list1
In [29]:
id(list1)
Out[29]:
2457623177728
In [30]:
id(list2)
Out[30]:
2457623177728
In [32]:
list3 = [1,2,3,4]
In [33]:
id(list3)
Out[33]:
2457623176512

Note: list1 and list3 contain the same values in their list:

In [34]:
list1 == list3
Out[34]:
True

On the other hand, their contents reside in different memory positions. We can use the is operator to check whether two objects point to the same memory location:

In [35]:
list1 is list3
Out[35]:
False

Since they point to different memory locations, changes to one do not affect the other.

In [37]:
list1[0] = 99

print(list1)
print(list2)
print(list3)
[99, 2, 3, 4]
[99, 2, 3, 4]
[1, 2, 3, 4]
In [38]:
list1 is list2
Out[38]:
True

Sometimes, we may want to copy a list to a new variable but sever this link. That is, have the new variable point to a new location in memory. We can use the .copy() method:

In [39]:
list1 = [1,2,3,4]
list2 = list1.copy()
In [41]:
list1 == list2
Out[41]:
True
In [40]:
list1 is list2
Out[40]:
False
In [42]:
list1[-1] = 99

print(list1)
print(list2)
[1, 2, 3, 99]
[1, 2, 3, 4]

Plotting in Python¶

We will the pyplot submodule from the matplotlib module for our plotting needs. Typically, we will import this submodule and assign it the name plt.

In [65]:
import matplotlib.pyplot as plt

The plt.plot function can be used for plotting:

In [67]:
x_list = [x for x in range(0,11)]
y_list = [x**2 for x in x_list]
In [68]:
plt.plot(x_list,y_list)
Out[68]:
[<matplotlib.lines.Line2D at 0x23c38694eb0>]

By default, the plot function will connect datapoints with a polygonal line. We can change this behavior by supplying a supplying a string that can control markers, color, and linestyle:

In [73]:
plt.plot(x_list,y_list,'c')
Out[73]:
[<matplotlib.lines.Line2D at 0x23c388e08b0>]

There are many color characters available:

  • 'r': red
  • 'g': green
  • 'b': blue
  • 'k': black
  • 'm': magenta
  • 'c': cyan
  • 'y': yellow

We can also specify a marker type:

In [74]:
plt.plot(x_list,y_list,'o')
Out[74]:
[<matplotlib.lines.Line2D at 0x23c38944850>]
In [75]:
plt.plot(x_list,y_list,'.')
Out[75]:
[<matplotlib.lines.Line2D at 0x23c389a59d0>]
In [76]:
plt.plot(x_list,y_list,'*')
Out[76]:
[<matplotlib.lines.Line2D at 0x23c38a0b9d0>]
In [77]:
plt.plot(x_list,y_list,'+')
Out[77]:
[<matplotlib.lines.Line2D at 0x23c38aaf9d0>]
In [78]:
plt.plot(x_list,y_list,'s')
Out[78]:
[<matplotlib.lines.Line2D at 0x23c38b118b0>]
In [79]:
plt.plot(x_list,y_list,'mD')
Out[79]:
[<matplotlib.lines.Line2D at 0x23c38b777f0>]

We can also choose a linestyle (polygonal lines, dashed lines, etc.)

In [80]:
plt.plot(x_list,y_list,'o-')
Out[80]:
[<matplotlib.lines.Line2D at 0x23c39b68850>]
In [81]:
plt.plot(x_list,y_list,'r*-')
Out[81]:
[<matplotlib.lines.Line2D at 0x23c39bcb8b0>]
In [82]:
plt.plot(x_list,y_list,'r*--')
Out[82]:
[<matplotlib.lines.Line2D at 0x23c39c2a700>]

Instead of supplying this string formatter, we can use keyword arguments to specify a color, a marker, linestyle:

In [84]:
plt.plot(x_list,y_list,color='r', marker='D', linestyle='dashdot')
Out[84]:
[<matplotlib.lines.Line2D at 0x23c39e8d130>]

We can use plt.xlabel and plt.ylabel to add axis labels. These labels can include LaTeX:

In [85]:
plt.plot(x_list,y_list,color='r', marker='D', linestyle='dashdot')
plt.xlabel('$x$')
plt.ylabel('$y = x^2$')
Out[85]:
Text(0, 0.5, '$y = x^2$')

We can use plt.title to add a title the plot:

In [86]:
plt.plot(x_list,y_list,color='r', marker='D', linestyle='dashdot')
plt.xlabel('$x$')
plt.ylabel('$y = x^2$')
plt.title('My plot')
Out[86]:
Text(0.5, 1.0, 'My plot')
In [87]:
x_list = [x for x in range(0,11)]
y_list1 = [x**2 for x in x_list]
y_list2 = [100 - x**2 for x in x_list]


plt.plot(x_list,y_list1,color='r', marker='D', linestyle='dashdot')
plt.plot(x_list,y_list2,color='b', marker='s', linestyle='solid')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.title('My plot')
Out[87]:
Text(0.5, 1.0, 'My plot')

We can add labels to plots using the label keyword argument, and then add a legend using the plt.legend() function.

In [88]:
x_list = [x for x in range(0,11)]
y_list1 = [x**2 for x in x_list]
y_list2 = [100 - x**2 for x in x_list]


plt.plot(x_list,y_list1,color='r', marker='D', linestyle='dashdot', label='$y = x^2$')
plt.plot(x_list,y_list2,color='b', marker='s', linestyle='solid', label='$y = 100 - x^2$')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.title('My plot')
plt.legend()
Out[88]:
<matplotlib.legend.Legend at 0x23c3b42dc70>

We can change the horizontal and vertical limits using the plt.xlim and plt.ylim function:

In [90]:
x_list = [x for x in range(0,11)]
y_list1 = [x**2 for x in x_list]
y_list2 = [100 - x**2 for x in x_list]

plt.xlim(4,8)
plt.ylim(20,80)

plt.plot(x_list,y_list1,color='r', marker='D', linestyle='dashdot', label='$y = x^2$')
plt.plot(x_list,y_list2,color='b', marker='s', linestyle='solid', label='$y = 100 - x^2$')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.title('My plot')
plt.legend()
Out[90]:
<matplotlib.legend.Legend at 0x23c3b207400>

We can use plt.grid() to add grid lines to the interior of the plot:

In [92]:
x_list = [x for x in range(0,11)]
y_list1 = [x**2 for x in x_list]
y_list2 = [100 - x**2 for x in x_list]

plt.xlim(4,8)
plt.ylim(20,80)

plt.plot(x_list,y_list1,color='r', marker='D', linestyle='dashdot', label='$y = x^2$')
plt.plot(x_list,y_list2,color='b', marker='s', linestyle='solid', label='$y = 100 - x^2$')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.title('My plot')0
plt.legend()

plt.grid()