Beautiful Code [236]
19.4.2. Iterator Termination
Another important aspect of the iterator (especially when it is used in a loop) is figuring out when the iterator is finished and how to signal that information. The most general approach to signaling is to attach a flag variable to the iterator that is checked each time around the loop, and set when there are no more elements to iterate through.
One possible way to set this flag would be to look for the transition in the first-dimension counter from n1-1 to 0. The problem with this approach is that it requires a temporary variable to store the last counter value, so it doesn't work for contiguous arrays, which do not keep track of the counter.
The easiest thing to do, however, is just remember that a particular number (n1 x…x nN) of iterations will take place given the size of the array. This number can be stored in the iterator structure. Then, during each iterator stage, this number can be decremented. When it reaches 0, the iterator should terminate.
NumPy uses a slight modification of this countdown. In order to preserve the total number of iterations as another piece of information, as well as to keep a running counter of the total number of iterations so far, NumPy uses an integer counter that counts up from zero. The iteration terminates when this number reaches the total number of elements.
19.4.3. Iterator Setup
When the iterator is created, the size of the underlying array must be computed and stored. In addition, the integer counter must be set to 0, and the coordinate counter must be initialized to (0,0,…,0). Finally, NumPy determines whether the iterator can be based on simple contiguous memory and sets a flag to remember the answer.
In order to speed up the "back-tracking step" that occurs whenever an index in the counter moves from ni-1 to 0, the product of (ni-1) xstrides[i] is precalculated and stored for each index. In addition to avoid repeatedly computing ni-1, this is also precomputed and stored in the structure.
While it is doubtful that there is any speed increase in storing this easily computed quantity, it is still very useful to have the dimensions of the array stored in the iterator structure. In the same manner, it is useful to have information about strides stored directly in the iterator, along with a variable tracking how many dimensions the underlying array has. With the dimensions and strides of the array stored in the iterator, modifications to how the array is interpreted later can be easily accomplished by modifying these values in the iterator and not in the underlying array itself. This is especially useful in implementing broadcasting, which makes arrays that are not shaped the same appear as if they were shaped the same (as will be explained later).
Finally, an array of precomputed factors is stored to simplify the calculations involved in the one-to-one mapping between the single integer counter into the array and its N-index counterpart. For example, every item in the array can be referenced by a single integer k between 0 and n1 x … x nN-1-1 or by a tuple of integers: (k1,…,kN). The relationship can be defined by l1=k and:
Going back the other way, the relationship is:
The terms within the parentheses of the previous equation are precomputed and stored in the iterator as an array of factors, to facilitate mapping back and forth between the two ways of thinking about the N-dimensional index.
19.4.4. Iterator Counter Tracking
Code for keeping track of the N-dimensional index counter is fairly straightforward. A distinction must be made between the case when the iterator will simply add 1 to the last index and when wrapping might occur. Whenever wrapping occurs, it has the potential to cause other indices to wrap as well. Therefore, there