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 - forstatement 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_ascii_iterator at 0x7f14f8719360>
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: 73.51 MiB, increment: 0.38 MiB
%memit doubles = (2 * n for n in range(10000))
peak memory: 73.76 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 - Chebyshevthat 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', '♦')
 
    
  
  
