Iterators#

Most container objects can be looped over using a for statement:

for element in [1, 2, 3]:
    print(element, end=' ')
1 2 3 
for element in (1, 2, 3):
    print(element, end=' ')
1 2 3 
for key in {'one': 1, 'two': 2}:
    print(key, end=' ')
one two 
for char in "123":
    print(char, end=' ')
1 2 3 
for line in open("../environment.yml"):
    print(line, end= ' ')
channels:
   - sympy
   - conda-forge
   - defaults
 dependencies:
   - cython
   - fortran-magic
   - h5py
   - imageio
   - ipykernel
   - ipywidgets
   - joblib
   - jupytext
   - line_profiler
   - lorem
   - matplotlib
   - memory_profiler
   - numba
   - numexpr
   - numpy
   - pillow
   - progressbar2
   - pythran
   - scipy
   - seaborn
   - setuptools
   - sympy
   - tqdm
   - ujson
   - pip
   - pip:
     - py-heat-magic
     - jupyter-book
 
  • The for statement calls iter() on the container object.

  • The function returns an iterator object that defines the method __next__()

  • To add iterator behavior to your classes:

    • Define an __iter__() method which returns an object with a __next__().

    • If the class defines __next__(), then __iter__() can just return self.

    • The StopIteration exception indicates the end of the loop.

s = 'abc'
it = iter(s)
it
<str_iterator at 0x7f3c74526830>
next(it), next(it), next(it)
('a', 'b', 'c')
class Reverse:
    """Iterator for looping over a sequence backwards."""

    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
rev = Reverse('spam')
for char in rev:
    print(char, end='')
maps
def reverse(data): # Python 3.6
    yield from data[::-1]
    
for char in reverse('bulgroz'):
     print(char, end='')
zorglub

Generators#

  • Generators are a simple and powerful tool for creating iterators.

  • Write regular functions but use the yield statement when you want to return data.

  • the __iter__() and __next__() methods are created automatically.

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
for char in reverse('bulgroz'):
     print(char, end='')
zorglub

Exercise#

Generates a list of IP addresses based on IP range.

ip_range = 
for ip in ip_range("192.168.1.0", "192.168.1.10"):
   print(ip)

192.168.1.0
192.168.1.1
192.168.1.2
...

Generator Expressions#

  • Use a syntax similar to list comprehensions but with parentheses instead of brackets.

  • Tend to be more memory friendly than equivalent list comprehensions.

sum(i*i for i in range(10))                 # sum of squares
285
%load_ext memory_profiler
%memit doubles = [2 * n for n in range(10000)]
peak memory: 74.95 MiB, increment: 0.59 MiB
%memit doubles = (2 * n for n in range(10000))
peak memory: 75.09 MiB, increment: 0.00 MiB
# list comprehension
doubles = [2 * n for n in range(10)]
for x in doubles:
    print(x, end=' ')
0 2 4 6 8 10 12 14 16 18 
# generator expression
doubles = (2 * n for n in range(10))
for x in doubles:
    print(x, end=' ')
0 2 4 6 8 10 12 14 16 18 

Exercise#

The Chebyshev polynomials of the first kind are defined by the recurrence relation

\begin{align} T_o(x) &= 1 \ T_1(x) &= x \ T_{n+1} &= 2xT_n(x)-T_{n-1}(x) \end{align}

  • Create a class Chebyshev that generates the sequence of Chebyshev polynomials

itertools#

zip_longest#

itertools.zip_longest() accepts any number of iterables as arguments and a fillvalue keyword argument that defaults to None.

x = [1, 1, 1, 1, 1]
y = [1, 2, 3, 4, 5, 6, 7]
list(zip(x, y))
from itertools import zip_longest
list(map(sum,zip_longest(x, y, fillvalue=1)))
[2, 3, 4, 5, 6, 7, 8]

combinations#

loto_numbers = list(range(1,50))

A choice of 6 numbers from the sequence 1 to 49 is called a combination. The itertools.combinations() function takes two arguments—an iterable inputs and a positive integer n—and produces an iterator over tuples of all combinations of n elements in inputs.

from itertools import combinations
len(list(combinations(loto_numbers, 6)))
13983816
from math import factorial
factorial(49)/ factorial(6) / factorial(49-6)
13983816.0

permutations#

from itertools import permutations
for s in permutations('dsi'):
    print( "".join(s), end=", ")
dsi, dis, sdi, sid, ids, isd, 

count#

from itertools import count
n = 2024
for k in count(): # replace  k = 0; while(True) : k += 1
    if n == 1:
        print(f"k = {k}")
        break
    elif n & 1:
        n = 3*n +1
    else:
        n = n // 2
k = 112

cycle, islice, dropwhile, takewhile#

from itertools import cycle, islice, dropwhile, takewhile
L = list(range(10))
cycled = cycle(L)  # cycle through the list 'L'
skipped = dropwhile(lambda x: x < 6 , cycled)  # drop the values until x==4
sliced = islice(skipped, None, 20)  # take the first 20 values
print(*sliced)
6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
result = takewhile(lambda x: x > 0, cycled) # cycled begins to 4
print(*result)
6 7 8 9

product#

ranks = ['A', 'K', 'Q', 'J', '10', '9', '8', '7']
suits = [ '\u2660', '\u2665', '\u2663', '\u2666']
cards = [(rank, suit) for rank in ranks for suit in suits]
len(cards)
from itertools import product
cards = product(ranks, suits)
print(*cards)
('A', '♠') ('A', '♥') ('A', '♣') ('A', '♦') ('K', '♠') ('K', '♥') ('K', '♣') ('K', '♦') ('Q', '♠') ('Q', '♥') ('Q', '♣') ('Q', '♦') ('J', '♠') ('J', '♥') ('J', '♣') ('J', '♦') ('10', '♠') ('10', '♥') ('10', '♣') ('10', '♦') ('9', '♠') ('9', '♥') ('9', '♣') ('9', '♦') ('8', '♠') ('8', '♥') ('8', '♣') ('8', '♦') ('7', '♠') ('7', '♥') ('7', '♣') ('7', '♦')