\( \newcommand{\NOT}{\neg} \newcommand{\AND}{\wedge} \newcommand{\OR}{\vee} \newcommand{\XOR}{\oplus} \newcommand{\IMP}{\Rightarrow} \newcommand{\IFF}{\Leftrightarrow} \newcommand{\TRUE}{\text{True}\xspace} \newcommand{\FALSE}{\text{False}\xspace} \newcommand{\IN}{\,{\in}\,} \newcommand{\NOTIN}{\,{\notin}\,} \newcommand{\TO}{\rightarrow} \newcommand{\DIV}{\mid} \newcommand{\NDIV}{\nmid} \newcommand{\MOD}[1]{\pmod{#1}} \newcommand{\MODS}[1]{\ (\text{mod}\ #1)} \newcommand{\N}{\mathbb N} \newcommand{\Z}{\mathbb Z} \newcommand{\Q}{\mathbb Q} \newcommand{\R}{\mathbb R} \newcommand{\C}{\mathbb C} \newcommand{\cA}{\mathcal A} \newcommand{\cB}{\mathcal B} \newcommand{\cC}{\mathcal C} \newcommand{\cD}{\mathcal D} \newcommand{\cE}{\mathcal E} \newcommand{\cF}{\mathcal F} \newcommand{\cG}{\mathcal G} \newcommand{\cH}{\mathcal H} \newcommand{\cI}{\mathcal I} \newcommand{\cJ}{\mathcal J} \newcommand{\cL}{\mathcal L} \newcommand{\cK}{\mathcal K} \newcommand{\cN}{\mathcal N} \newcommand{\cO}{\mathcal O} \newcommand{\cP}{\mathcal P} \newcommand{\cQ}{\mathcal Q} \newcommand{\cS}{\mathcal S} \newcommand{\cT}{\mathcal T} \newcommand{\cV}{\mathcal V} \newcommand{\cW}{\mathcal W} \newcommand{\cZ}{\mathcal Z} \newcommand{\emp}{\emptyset} \newcommand{\bs}{\backslash} \newcommand{\floor}[1]{\left \lfloor #1 \right \rfloor} \newcommand{\ceil}[1]{\left \lceil #1 \right \rceil} \newcommand{\abs}[1]{\left | #1 \right |} \newcommand{\xspace}{} \newcommand{\proofheader}[1]{\underline{\textbf{#1}}} \)

5.8 PythonTA and Accumulation Tables

Accumulation tables are a powerful tool for understanding the behaviour of for loops, but they also can be a bit time-consuming to create when given large quantities of data, or for more complex loops. So to make it easier to use accumulation tables as part of your code-writing process, PythonTA provides a way of generating and displaying loop accumulation tables!

Let’s illustrate using PythonTA in this way by modifying our product function from 5.4 Repeated Execution: For Loops.

from python_ta.debug import AccumulationTable  # New import!


def product(numbers: list[int]) -> int:
    """Return the product of the given numbers.

    >>> product([10, 20])
    200
    >>> product([-5, 4])
    -20
    """
    # ACCUMULATOR product_so_far: keep track of the product
    # of the elements in numbers seen so far in the loop.
    product_so_far = 1

    with AccumulationTable(['product_so_far']):  # New line!
        for number in numbers:
            product_so_far = product_so_far * number

    return product_so_far

Now, here’s what gets printed when we call print(product([10, 20, 30, 40])) in the main block:

iteration    number    product_so_far
-----------  --------  ----------------
0            N/A       1
1            10        10
2            20        200
3            30        6000
4            40        240000
240000

The very last line, containing just 240000, is the function’s return value, as we would normally expect to see when printing the result of a function call. The part above it is what’s interesting: a text-based representation of an accumulation table, displaying the loop iteration number (0 to 4), the value of the loop variable number at each iteration, and the value of the accumulator product_so_far.

Pretty cool! Now let’s take a few moments to discuss how this new code is structured.

Breaking down the code

This code imports AccumulationTable, a custom data type defined by PythonTA. What does this data type do, exactly? When we write AccumulationTable(['product_so_far']), we are creating a new Python value that will expect to “wrap around” a for loop and keep track of the accumulator variable product_so_far at each loop iteration.

We achieve this “wrap around” behaviour through a new form of Python syntax called a with statement: Yet another form of compound statement, like if statements and for loops!

with <value>:
    <statement1>
    <statement2>
    ...

For our purposes, with statements are similar to decorators:

In general, with statements offer programmers the ability to modify the behaviour of a block of code. Using with AccumulationTable(['product_so_far']): to wrap the for loop, the Python interpreter does the following:

  1. Evaluate AccumulationTable(['product_so_far']), producing a value of the AccumulationTable data type that is intended to track the accumulator variable product_so_far.
  2. Then execute the for loop written inside the with statement.
    • Immediately before the for loop executes, and at the end of each iteration of the for loop, the AccumulationTable value records the iteration number, value of the loop variable, and value of the product_so_far accumulator.
  3. At the end of the with statement, after the for loop ends, the AccumulationTable prints a text-based table of the values it recorded in Step 2.

A subtlety: printing vs. return value

One subtlety with AccumulationTable is that even though it records data in a tabular form, it prints the table to the terminal, rather than making that data available in code. To see what we mean by this, let’s change our code in the main block, now using an assignment statement to store product’s return value: result = product([10, 20, 30, 40]). Running the program now produces the following in the terminal:

iteration    number    product_so_far
-----------  --------  ----------------
0            N/A       1
1            10        10
2            20        200
3            30        6000
4            40        240000

Now, even though we are storing the result of the function call in the result variable rather than printing it, the accumulation table is still displayed immediately. The return value of the function is the value 240000, but the accumulation table isn’t part of that return value. Using AccumulationTable, we can see the accumulation table for a given for loop, but we cannot access or compute with the underlying data, just look at it. Okay, technically there is a way to access the data recorded by the AccumulationTable, but that’s beyond the scope of this course. Please feel free to ask about this during office hours! This illustrates one of the key limitations of “printing” data: we see that data immediately in the terminal, but can’t access it directly. In CSC110/111, we’ll use printing techniques like AccumulationTable as a way to gain information about our code, which is especially helpful when debugging. However, printing should never replace the usual data flow of storing the results of computations in variables and returning values from functions! And one final warning:

Warning: python_ta.debug.AccumulationTable is a useful tool for debugging purposes, and we hope you make use of it when completing your work for CSC110/111. However, because the accumulation table prints debugging text to the terminal, it violates the principle of function correctness: a function’s body should do exactly what the function specification says, no more and no less.

That means that before submitting any graded work for this course, you should always remove the with statement containing AccumulationTable (and unindent the for loop contained within) from your final submission!