<jupyter_start><jupyter_text>Itertools Prima di approfondire il modulo `itertools`, che implementa una serie di funzioni / "building blocks" per gestire in modo succinto ed efficace gli iteratori, ovvero tipi di dati che possono essere usati in un ciclo for, conviene approfondire ancora meglio il concetto di **iterator**.L'iterator più comune (ma ce ne sono molti altri) in Python è la lista `list`, sulla quale è possibile appunto iterare.<jupyter_code>colors = ['red', 'orange', 'yellow', 'green']
for each in colors:
    print(each)<jupyter_output>red
orange
yellow
green<jupyter_text>IteratoriE' possibile creare i propri oggetti `iterable`, da usarsi nei *loops* e per le *list comprehensions*.Basta implementare i metodi *magici* necessari:- `__iter__` che ritorna l'oggetto iterable, di solito self - `__next__` che definisce i valori dell'iteratorePer esempio, vogliamo un iteratore che ritorni i numeri in ordine ad eccezione dei multipli dei numeri passati al costrutture.Per essere sicuri di farlo terminare comunque, rilanceremo l'eccezione `StopIteration` dopo aver raggiunto i 200.*Questo è un ottimo esempio di un'eccezione piuttosto comune in Python: gestire un evento che non sia inaspettato ma che richieda una fine; for loops e list comprehensions attendono `StopIteration` exception come segnale di stop.*<jupyter_code>class NonFattoreIterable(object):
    def __init__(self, *args):
        # List of number for whic avoid multiples
        self.evita_multipli_di = args 
        self.x = 0
        
    def __next__(self):
        self.x += 1 
        while True: 
            if self.x > 200: 
                raise StopIteration 
            for y in self.evita_multipli_di: 
                if self.x % y == 0: 
                    self.x += 1 
                    break 
            else: 
                return self.x 
    
    def __iter__(self): 
        return self
es = NonFattoreIterable(3, 5)
[x for x in es]
quasi_primi = NonFattoreIterable(2, 3, 5, 7, 11, 13, 17, 19) 
[x for x in quasi_primi]
somma_parziale = 0
for x in quasi_primi: 
    print(x)
    somma_parziale += x
somma_parziale # E' zero perchè l'iterabile è già stato "consumato" (da verificare!)
quasi_primi = NonFattoreIterable(2, 3, 5, 7, 11, 13, 17, 19) 
print(sum(quasi_primi))
quasi_primi = NonFattoreIterable(2, 3, 5, 7, 11, 13, 17, 19)
print(max(quasi_primi))<jupyter_output>199<jupyter_text>Sembra inutile la presenza del metodo `__iter__`, ma ci sono casi in cui l'iterator di un oggetto non è lo stesso oggetto (occuparsi di questi casi ci farebbe però andare fuori tema). getItemC'è un altro metodo che si può implementare un iterator personalizzato: il metodo `__getitem__`.Questo permette la notazione parentesi quadra [] per ottenere il dato fuori dall'oggetto.Comunque dobbiamo ricordarci di rilanciare `StopIteration` per farlo funzionare bene nei for e nelle list comprehensions.<jupyter_code>class Quadrati(object): 
    def __init__(self, limite=200): 
        self.limite = limite 
        self.x = 0 

    def __next__(self): 
        self.x += 1 
        if self.x > self.limite: 
            raise StopIteration 
        return (self.x-1)**2 

    def __getitem__(self, idx): 
        # initialize counter to 0 
        self.x = 0 
        if not isinstance(idx, int): 
            raise Exception("Solo indici integer sono accettati!") 
        while self.x < idx: 
            self.__next__() 
        return self.x**2
    
    def __iter__(self): 
        return self
miei_quadrati = Quadrati(limite=20) 
[x for x in miei_quadrati]
miei_quadrati[5]
miei_quadrati[25]<jupyter_output><empty_output><jupyter_text>Nell'esempio precedente abbiamo creato un iterator che ritorna quadrati di numeri. Nel metodo `__next__` si itera il nostro contatore ( `self.x` ) ritornando il suo quadrato finchè il contatore non supera il limte predefinito ( `self.limit`). Il while dell'esempio precedente (NonFattoreIterable) era specifico per quel caso d'uso.Nel metodo `__next__` non è solitamente necessario implementare loop perchè normalmente viene richiamato all'interno di un loop sull'iterator.Nell'esempio che segue implementiamo il metodo `__getitem__`, che ci permette di reperire un valore dell'iteratore ad una certa posizione di indice. Questo semplicemente chiama l'iteratore usando `self.__next__` finchè non arriva all'indice desiderato e restituisce il valore.  Vantaggi degli iterator personalizzatiPerchè usare *iterator* personalizzati?- Codice pulito- Possibilità di lavorare con sequenze infinite- Possibilità di usare funzioni built-in (come sum) che lavorano con iterable- Possibilità di salvare memoria (es. range )  Built-ins "itertools" Uno dei tools sugli iterabili, fornito di base (built-in) è  zip()che prende un numero qualsiasi di iterabili e ritorna un'iteratore di tuple con gli elementi corrispondenti:<jupyter_code>list(zip([1, 2, 3], ['a', 'b', 'c']))<jupyter_output><empty_output><jupyter_text>Un'altro operatore built-in sugli iteratori è map()  che, nella sua forma più semplice, applica una funzione per ciascun elemento di un iterabile.<jupyter_code>list(map(len, ['abc', 'de', 'fghi']))<jupyter_output><empty_output><jupyter_text>Il modulo itertoolsUna collezione di *funzioni*<jupyter_code>import itertools
import operator #Utile per alcuni degli esempi<jupyter_output><empty_output><jupyter_text>accumulate()`itertools.accumulate(iterable[, func])`Questa funziona crea un iteratore che ritorna il risultato di una funzione (passata come argomento) applicata su un iterabile e restituisce il risultato "accumulato".I risultati sono contenuti essi stessi in un iterable.Gli esempi possono aiutare la comprensione:<jupyter_code>data = [1, 2, 3, 4, 5]
result = itertools.accumulate(data, operator.mul)
for each in result:
    print(each)
operator.mul(1, 2) # Prende due numeri e li moltiplica
data = [5, 2, 6, 4, 5, 9, 1]
result = itertools.accumulate(data, max)
for each in result:
    print(each)
max(5, 2) # Restituisce il massimo tra i due<jupyter_output><empty_output><jupyter_text>Il passaggio di una funzione è opzionale, per default viene usata la somma.<jupyter_code>data = [5, 2, 6, 4, 5, 9, 1]
result = itertools.accumulate(data)
for each in result:
    print(each)<jupyter_output>5
7
13
17
22
31
32<jupyter_text>combinations()`itertools.combinations(iterable, r)`Dato un iterable e un intero, crea tutte le combinazioni con r membri.<jupyter_code>shapes = ['circle', 'triangle', 'square',]
result = itertools.combinations(shapes, 2)
for each in result:
    print(each)<jupyter_output>('circle', 'triangle')
('circle', 'square')
('triangle', 'square')<jupyter_text>combinations_with_replacement()`itertools.combinations_with_replacement(iterable, r)`Come **combinations()**, ma ammette ripetizioni.<jupyter_code>shapes = ['circle', 'triangle', 'square',]
result = itertools.combinations_with_replacement(shapes, 2)
for each in result:
    print(each)<jupyter_output>('circle', 'circle')
('circle', 'triangle')
('circle', 'square')
('triangle', 'triangle')
('triangle', 'square')
('square', 'square')<jupyter_text>count()`itertools.count(start=0, step=1)`Crea iterator che ritorna valori partendo dal valore start, applicando lo step desiderato.<jupyter_code>for i in itertools.count(10,3):
    print(i)
    if i > 20:
        break<jupyter_output>10
13
16
19
22<jupyter_text>cycle()`itertools.cycle(iterable)`Cicla sull'iteratore all'infinito.<jupyter_code>colors = ['red', 'orange', 'yellow', 'green', 'blue', 'violet']
for color in itertools.cycle(colors):
    print(color)<jupyter_output>red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue
violet
red
orange
yellow
green
blue[...]<jupyter_text>chain()`itertools.chain(*iterables)`Prende una serie di iterable e ritorna come unico.<jupyter_code>colors = ['red', 'orange', 'yellow', 'green', 'blue']
shapes = ['circle', 'triangle', 'square', 'pentagon']
result = itertools.chain(colors, shapes)
for each in result:
    print(each)<jupyter_output>red
orange
yellow
green
blue
circle
triangle
square
pentagon<jupyter_text>compress()`itertools.compress(data, selectors)`Filtra un iterable con un altro<jupyter_code>shapes = ['circle', 'triangle', 'square', 'pentagon']
selections = [True, False, True, False]
result = itertools.compress(shapes, selections)
for each in result:
    print(each)<jupyter_output>circle
square<jupyter_text>dropwhile()`itertools.dropwhile(predicate, iterable)`Crea un iterator che elimina elementi da iterable finchè il predicato è True, dopodichè ritorna tutto il restante<jupyter_code>data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1]
result = itertools.dropwhile(lambda x: x<5, data)
for each in result:
    print(each)<jupyter_output>5
6
7
8
9
10
1<jupyter_text>filterfalse()`itertools.filterfalse(predicate, iterable)`Crea un iterator che filtra elementi da iterable restituendo solo quelli per i quali il predicato è False.<jupyter_code>data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = itertools.filterfalse(lambda x: x<5, data)
for each in result:
    print(each)<jupyter_output>5
6
7
8
9
10<jupyter_text>groupby()`itertools.groupby(iterable, key=None)`Semplicemente raggruppa, ma non è così semplice da spiegare...<jupyter_code>robots = [{
    'name': 'blaster',
    'faction': 'autobot'
}, {
    'name': 'galvatron',
    'faction': 'decepticon'
}, {
    'name': 'jazz',
    'faction': 'autobot'
}, {
    'name': 'metroplex',
    'faction': 'autobot'
}, {
    'name': 'megatron',
    'faction': 'decepticon'
}, {
    'name': 'starcream',
    'faction': 'decepticon'
}]
for key, group in itertools.groupby(robots, key=lambda x: x['faction']):
    print(key)
    print(list(group))<jupyter_output>autobot
[{'name': 'blaster', 'faction': 'autobot'}]
decepticon
[{'name': 'galvatron', 'faction': 'decepticon'}]
autobot
[{'name': 'jazz', 'faction': 'autobot'}, {'name': 'metroplex', 'faction': 'autobot'}]
decepticon
[{'name': 'megatron', 'faction': 'decepticon'}, {'name': 'starcream', 'faction': 'decepticon'}]<jupyter_text>islice()`itertools.islice(iterable, start, stop[, step])`Molto simile a **slices**, permette di tagliare una fetta di un iterable.<jupyter_code>colors = ['red', 'orange', 'yellow', 'green', 'blue',]
few_colors = itertools.islice(colors, 2)
for each in few_colors:
    print(each)<jupyter_output>red
orange<jupyter_text>permutations()`itertools.permutations(iterable, r=None)`Restituisce un iterabile con tutte le tuple combinazione<jupyter_code>alpha_data = ['a', 'b', 'c']
result = itertools.permutations(alpha_data)
for each in result:
    print(each)<jupyter_output>('a', 'b', 'c')
('a', 'c', 'b')
('b', 'a', 'c')
('b', 'c', 'a')
('c', 'a', 'b')
('c', 'b', 'a')<jupyter_text>product()`itertools.product(iterable1, iterable2, ...)`Crea un iterable di tuple prodotto cartesiano da una serie di iterabili<jupyter_code>num_data = [1, 2, 3]
alpha_data = ['a', 'b', 'c']
result = itertools.product(num_data, alpha_data)
for each in result:
    print(each)<jupyter_output>(1, 'a')
(1, 'b')
(1, 'c')
(2, 'a')
(2, 'b')
(2, 'c')
(3, 'a')
(3, 'b')
(3, 'c')<jupyter_text>repeat()`itertools.repeat(object[, times])`Ripete un oggetto all'infinito a meno che venga specificato quante (times) volte<jupyter_code>for i in itertools.repeat("spam",10):
    print(i)<jupyter_output>spam
spam
spam
spam
spam
spam
spam
spam
spam
spam<jupyter_text>starmap()`itertools.starmap(function, iterable)`Crea un iterator che esegue la funzione usando gli argomenti ottenuti da iterable<jupyter_code>data = [(2, 6), (8, 4), (7, 3)]
result = itertools.starmap(operator.mul, data)
for each in result:
    print(each)<jupyter_output>12
32
21<jupyter_text>takewhile()`itertools.takewhile(predicate, iterable)`Opposto di `dropwhile()`, crea un iterator e restutisce elementi da iterable finchè il predicato è True.<jupyter_code>data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1]
result = itertools.takewhile(lambda x: x<5, data)
for each in result:
    print(each)<jupyter_output>1
2
3
4<jupyter_text>tee()`itertools.tee(iterable, n=2)`Restituisce n iterators indipendenti da un singolo iterable; default è 2.<jupyter_code>colors = ['red', 'orange', 'yellow', 'green', 'blue']
alpha_colors, beta_colors = itertools.tee(colors)
for each in alpha_colors:
    print(each)
print('..')
for each in beta_colors:
     print(each)<jupyter_output>red
orange
yellow
green
blue
..
red
orange
yellow
green
blue<jupyter_text>zip_longest()`itertools.zip_longest(*iterables, fillvalue=None)`Crea un iterator che aggrega elementi per ciascun iterable. Se hanno lunghezza differente, i valori mancanti sono riempiti con fillvalue fino all'iterabile più lungo.<jupyter_code>colors = ['red', 'orange', 'yellow', 'green', 'blue',]
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,]
for each in itertools.zip_longest(colors, data, fillvalue=None):
    print(each)<jupyter_output>('red', 1)
('orange', 2)
('yellow', 3)
('green', 4)
('blue', 5)
(None, 6)
(None, 7)
(None, 8)
(None, 9)
(None, 10)<jupyter_text>Esempi Es. 1Avete tre biglietti da 20 EUR, cinque da 10 EUR, due da 5 EUR e cinque monete da 1 EUR.In quanti modi potete comporre 100 EUR ?<jupyter_code>soldi = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1]

# Usiamo le combinazioni
fai_100 = []
for n in range(1, len(soldi) + 1):
    for comb in itertools.combinations(soldi, n):
        if sum(comb) == 100:
            fai_100.append(comb)
fai_100<jupyter_output><empty_output><jupyter_text>Meglio rimuovere i duplicati<jupyter_code>set(fai_100)<jupyter_output><empty_output><jupyter_text>Es. 2 - Variazione di 1In quanti modi si possono comporre 100 EUR usando un numero qualsiasi di pezzi da 50, 20, 10, 5, e 1 EUR?<jupyter_code># Ci metterà un po' di tempo! 96560645 combinazioni
pezzi = [50, 20, 10, 5, 1]
fa_100 = []
for n in range(1, 101):
     for comb in itertools.combinations_with_replacement(pezzi, n):
        if sum(comb) == 100:
             fa_100.append(comb)
len(fa_100)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11481732070365358, 'fraction_numerical': 0.022259810554803788, 'mean_word_length': 5.052825552825553, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 96, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '28', 'dir': 'jupyter-structured-clean-dedup', 'id': '474474', 'license': 'MIT', 'n_tokens_mistral': 5777, 'n_tokens_neox': 5410, 'n_words': 1626, 'path': 'Italiano/1_introduction/5-Itertools.ipynb', 'repo_name': 'Burlesco70/DataScienceCourse', 'size': '14780'}