<jupyter_start><jupyter_text>Generadors Executa el codi a continuació i intenta entendre les diferències entre `crear_range` i `crear_range_generador`. Per què creus que és més ràpida la segona versió?  Primera versió<jupyter_code>def crear_range(inici, fi, pas):
    llista = []
    i = inici
    while i < fi:
        llista.append(i)
        i += pas
        
    return llista

def crea_i_itera(inici, fi, pas):
    llista = crear_range(inici, fi, pas)
    
    for i in llista:
        x = i
print(crear_range(1, 90, 10))
if __name__ == '__main__':
    %timeit crea_i_itera(1, 1e3, 1)<jupyter_output>135 µs ± 1.67 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)<jupyter_text>Segona Versió<jupyter_code>def crear_range_generador(inici, fi, pas):
    i = inici
    llista = []
    while i < fi:
        yield i
        i += pas
        
def crea_i_itera_generador(inici, fi, pas):
    llista = crear_range_generador(inici, fi, pas)
    
    for i in llista:
        x = i
print(crear_range_generador(1, 5, 1))
if __name__ == '__main__':
    %timeit crea_i_itera_generador(1, 1e3, 1)<jupyter_output>115 µs ± 2.62 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)<jupyter_text>Com funcionen? La paraula clau `yield` serveix per indicar a Python que la funció és generadora. Una funció generadora retorna un tipus especial que es pot iterar, el funcionament és el següent:**Obtenir el generador a través de la crida**```pythongen = crear_range_generador(1, 5, 1)```**Iterar els elements del generador**```pythonelement_1 = next(gen)element_2 = next(gen)...element_n = next(gen)```Cada cop que es fa `next`, el codi executa fins a trobar la paraula `yield`. Al trobar-la, retorna l'element del `yield` i es queda "pausat" en la següent instrucció, fins que es fa el següent `next`.**Alternativament, es pot iterar el generador com si fos una llista**```pythonfor element in gen:    print(element)```**Conversió a llista**També es pot convertir el generador a una llista de forma explícita. Compte però, normalment es fan servir generadors per evitar ocupar memòria, convertint a llista (o equivalent) ocuparem tota la memòria:```pythonllista = list(gen)```  Proves<jupyter_code>gen = crear_range_generador(1, 5, 1)
print(gen)

element_1 = next(gen)
element_2 = next(gen)
element_3 = next(gen)
element_4 = next(gen)

print(element_1)
print(element_2)
print(element_3)
print(element_4)<jupyter_output><generator object crear_range_generador at 0x000001B27F135CF0>
1
2
3
4<jupyter_text>Que passa si intentem obtenir un següent element?<jupyter_code>element_5 = next(gen)<jupyter_output><empty_output><jupyter_text>L'objecte generador `gen` no guarda els elements que va generant, cada cop que es fa un `next`, l'anterior "s'elimina" i es genera el següent.En vistes d'això, que passa si ara iterem el generador com si fos una llista?<jupyter_code>for element in gen:
    print(element)<jupyter_output><empty_output><jupyter_text>Efectivament, no produeix cap sortida perquè el generador ja està buit. Fa un moment hem vist l'error `StopIteration`. En tot cas, podríem crear-lo de nou i iterar:<jupyter_code>gen = crear_range_generador(1, 5, 1)
for element in gen:
    print(element)
import random
random.randint(1, 17)
gen = crear_range_generador(1, 5, 1)
llista = list(gen)
print(llista)<jupyter_output>[1, 2, 3, 4]
{'fraction_non_alphanumeric': 0.09873949579831932, 'fraction_numerical': 0.02611044417767107, 'mean_word_length': 4.716981132075472, 'pattern_counts': {'":': 0, '<': 24, '<?xml version=': 0, '>': 22, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '441874', 'license': 'MIT', 'n_tokens_mistral': 1316, 'n_tokens_neox': 1257, 'n_words': 388, 'path': 'assignment-0-2019-introduction-leRoderic/2-Generators.ipynb', 'repo_name': 'leRoderic/TNUI', 'size': '3332'}