Loop Tools

Recall that a for loop over a collection gets the value of each element, one at a time.

import numpy as np

x = np.array([3, 1, 4, 1, 5, 9])
for element in x:
    print(element)
3
1
4
1
5
9

In many tasks, the index of each value is needed as well, for which range() is useful.

for i in range(len(x)):
    print(i, x[i])
0 3
1 1
2 4
3 1
4 5
5 9

There are a few built-in functions that make it easy to get both the index and the value, or to loop through multiple collections simultaneously, or to loop through a collection backward.

Enumerate

The built-in enumerate() turns a collection into a stream of index-value pairs.

for i, element in enumerate(x):
    print(i, element)
0 3
1 1
2 4
3 1
4 5
5 9

Note the order: index first, then value. The above syntax assigns the index and value to separate variables; if only a single variable is used, as in for z in enumerate(x), then the loop variable z is a tuple (similar to a list) that contains the index as z[0] and the value as z[1].

By default, enumerate() counts from 0. The keyword argument start changes the starting number.

for i, element in enumerate(x, start=100):
    print(i, element)
100 3
101 1
102 4
103 1
104 5
105 9

Problem 1

A polynomial \(p(x) = a_0 + a_1 x + a_2 x^2 + a_3 x^3 + \cdots\) can be represented with a list of coefficients in ascending order of degree, so that element i is the coefficient \(a_i\) multiplying \(x^{i}\).

  • \(p(x) = 1 + 5x + 3x^2 + 9x^3\) can be represented with [1, 5, 3, 9].

  • \(p(x) = x^2\) can be represented with [0, 0, 1].

  • \(p(x) = 4\) can be represented with [4].

The derivative of a polynomial \(p(x)\) is another polynomial, \(p'(x)\):

\[\begin{split} \begin{aligned} p(x) &= a_0 + a_1 x + a_2 x^2 + a_3 x^3 + \cdots + a_{n-1} x^{n-1} = \sum_{i=0}^{n-1} a_i x^i, \\ p'(x) &= a_1 + 2a_2 x + 3a_3 x^2 + \cdots + (n - 1) a_{n-1} x^{n-2} = \sum_{i=1}^{n-1} i a_i x^{i-1}. \end{aligned} \end{split}\]

Write a function named poly_derivative() that accepts a NumPy array of \(n\) coefficients representing a polynomial \(p(x)\) and returns a NumPy array of the \(n - 1\) coefficients of the derivative \(p'(x)\). In other words, given \([a_0,a_1,a_2\ldots,a_{n-1}]\), return \([a_1, 2a_2, \ldots, (n - 1)a_{n-1}]\). Represent the zero polynomial \(p(x) = 0\) as the single-element array \([0]\).

Reminder: np.zeros(n) creates an array of n zeros.

Test Cases

# d/dx[1 + 5x + 3x^2 + 9x^3] = 5 + 6x + 27x^2
print(poly_derivative(np.array([1, 5, 3, 9])))

# d/dx[3.14 + 6.28x + x^2 - 0.6x^4] = 6.28 + 2x - 2.4x^3
print(poly_derivative(np.array([3.14, 6.28, 1, 0, -0.6])))

# d/dx[4] = 0
print(poly_derivative(np.array([4])))
[ 5.  6. 27.]
[ 6.28  2.    0.   -2.4 ]
[0.]

Zip

It is sometimes useful to loop through two or more collections simultaneously. One way to do this is to iterate through the indices with range():

x = np.array([3, 1, 4, 1, 5, 9])
y = np.array([2, 7, 1, 8, 2, 8])

for i in range(len(x)):
    print(x[i], y[i])
3 2
1 7
4 1
1 8
5 2
9 8

The zip() function provides a readable alternative, assigning a loop variable from each collection.

x = np.array([3, 1, 4, 1, 5, 9])
y = np.array([2, 7, 1, 8, 2, 8])

for xdigit, ydigit in zip(x, y):
    print(xdigit, ydigit)
3 2
1 7
4 1
1 8
5 2
9 8

If the collections passed to zip() have different lengths, the loop ends as soon as the shortest collection runs out and ignores the rest of the longer collection without reporting anything. Specifying strict=True turns this scenario into an error after the shorter collection is exhausted.

x = np.array([3, 1, 4, 1, 5, 9, 2, 6])
y = np.array([2, 7, 1, 8, 2, 8])

for xdigit, ydigit in zip(x, y, strict=True):
    print(xdigit, ydigit)
3 2
1 7
4 1
1 8
5 2
9 8
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[7], line 4
      1 x = np.array([3, 1, 4, 1, 5, 9, 2, 6])
      2 y = np.array([2, 7, 1, 8, 2, 8])
      3 
----> 4 for xdigit, ydigit in zip(x, y, strict=True):
      5     print(xdigit, ydigit)

ValueError: zip() argument 2 is shorter than argument 1

A loop can use both zip() and enumerate(), usually in the following way.

x = np.array([3, 1, 4, 1, 5, 9])
y = np.array([2, 7, 1, 8, 2, 8])

for i, (xdigit, ydigit) in enumerate(zip(x, y)):
    print(i, xdigit, ydigit)
0 3 2
1 1 7
2 4 1
3 1 8
4 5 2
5 9 8

Note carefully the use of parentheses: enumerate() yields two things, the index and the value of the collection defined by zip(), and the values of the zip() collection are pairs of values taken from the inputs x and y that should also be unpacked into separate variables.

Problem 2

A polygon can be described by a list of at least three \((x, y)\) points giving its corners, or vertices, in order: \([(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)]\). Gauss’s area formula, also known as the shoelace formula, gives the enclosed area as

\[ A = \frac{1}{2}\left| \sum_{i=1}^{n} \left( x_{i} y_{i+1} - x_{i+1} y_{i} \right) \right|, \]

where the vertex after the last one wraps back around to the first, \((x_{n+1}, y_{n+1}) = (x_1, y_1)\). For the triangle with vertices \((0,0)\), \((4,0)\), and \((1,3)\), this sum has three terms, one per edge.

../_images/triangle.png
\[ A = \frac{1}{2}\left| \big(0 \cdot 0 - 4 \cdot 0\big) + \big(4 \cdot 3 - 1 \cdot 0\big) + \big(1 \cdot 0 - 0 \cdot 3\big) \right| = \frac{1}{2}\left| 12 \right| = 6. \]

The following code plots a polygon with the vertices given in this format.

def plot_polygon(vertices):
    """Draw the polygon with the given vertices."""
    P = np.array(vertices + [vertices[0]])
    plt.plot(P[:, 0], P[:, 1], "o-", markersize=5, linewidth=1)
    plt.axis("equal")
    plt.show()

Write a function named polygon() that accepts a list of \((x, y)\) points representing the vertices of a polygon. Compute the perimeter (the sum of the lengths of the sides) and, using Gauss’s area formula, the area enclosed by the polygon. Return both the perimeter and the area, in that order.

Reminder: Use return x, y to return two values.
Hint: There are several ways to approach this problem, but whatever strategy you choose, remember to account for the edge between the last and first points in the list.

Test Cases

perimeter, area = polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
print(perimeter, area)

perimeter, area = polygon([(0, 0), (1, 0), (2, 2), (0, 1)])
print(perimeter, area)

perimeter, area = polygon([(-1, -1), (3, 0), (1, 0.5), (2, 2), (0, 3)])
print(perimeter, area)
4.0 1.0
6.47213595499958 2.0
14.346607679275937 7.25

Reversed

A standard for loop goes through the elements of a collection in order from beginning to end. Use reversed() to step through a collection from the last element to the first.

x = np.array([3, 1, 4, 1, 5, 9])

for digit in reversed(x):
    print(digit)
9
5
1
4
1
3

This does not modify x itself; it only changes the order in which the loop visits the elements.

Like zip(), reversed() can be used together with enumerate().

Quick Check

What is the output of the following code?

a = [3, 1, 8, 0]
b = [2, 1, 5, 4]

total = 0
for i, x in enumerate(reversed(a)):
    total += x * b[i]

print(total)
Check Answer
25
  • reversed(a) visits 0, 8, 1, then 3, and enumerate() numbers those 0, 1, 2, and 3.

  • The products accumulated are 0 * b[0] = 0, 8 * b[1] = 8, 1 * b[2] = 5, and 3 * b[3] = 12.

  • The total is 8 + 5 + 12 = 25.