Funcions generadores

Què són?

  • Són funcions tals que en el cos de la funció apareix una o més sentències yield en comptes de sentències return.

  • La crida a una funció generadora retorna un iterador (anomenat també generador).

  • L’execució de la funció comença en cridar next() sobre l’iterador i se suspèn en executar la primera sentència yield. Els valors de les variables de la funció es conserven.

  • next retorna l’objecte al que s’avalua l’expressió de la sentència yield.

  • L’execució de la funció es reprèn en cridar altre cop next sobre l’iterador. Les variables de la funció conserven el valor que tenien quan es va suspendre.

Com transformar funcions que calculen llistes?

Partim d’una funció que calcula una llista començant per la llista buida i afegint al final de la llista cada un dels elements que va calculant. Per exemple, la funció digits_llista() que donat un nombre natural, calcula la llista amb els dígits del nombre:

1def digits_llista(n):
2    ld = []
3    while n != 0:
4        d = n % 10
5        ld.append(d)
6        n = n // 10
7    return ld

>>> from digits import digits_llista

>>> ld = digits_llista(326)
>>> ld is iter(ld)
False
>>> ld
[6, 2, 3]
>>> ld = digits_llista(952748)
>>> ld is iter(ld)
False
>>> ld
[8, 4, 7, 2, 5, 9]

Per tal de transformar-la en una funció generadora cal fer el següent:

  1. Esborrem la inicialització de la llista i la sentència return.

  2. Substituïm la crida al mètode append() per la sentència yield.

Seguint l’exemple, la funció digits() l’hem obtingut de digits_llista() aplicant els canvis anteriors, és a dir, esborrant les línies 2 i 7, i substituint l'append de a línia 5 per yield.

def digits(n):
    while n != 0:
        d = n % 10
        yield d
        n = n // 10

La crida a la funció digits() retorna un iterador. El recorregut d’aquest iterador obtindrà els mateixos elements i en el mateix ordre en què estaven a la llista que retornava la funció digits_llista()


>>> from digits import digits

>>> it = digits(326)
>>> it is iter(it)
True
>>> list(it)
[6, 2, 3]
>>> it = digits(952748)
>>> it is iter(it)
True
>>> list(it)
[8, 4, 7, 2, 5, 9]

Vegeu l’execució de la funció generadora digits al Python Tutor.

Per què usar funcions generadores?

  • Una funció generadora calcula els elements d’una seqüència, successió o sèrie. Diguem-ne \(\mathcal{S}\).

  • El problema de calcular \(\mathcal{S}\) està resolt a la funció generadora.

  • Sempre que calguin els elements de la seqüencia \(\mathcal{S}\) per resoldre un problema, podrem cridar la funció generadora en comptes de tornar a resoldre el problema de calcular els elements d'\(\mathcal{S}\).

Per exemple, a l’exercici Dígits d’un nombre enter cal implementar una funció que calculi la suma dels dígits d’un enter i una altra que digui si un nobre enter té el dígit donat. Totes dues fan un tractament sobre la seqüència de dígits d’un nombre enter i poden aprofitar el càlcul que fa la funció generadora digits():

def suma_digits(n):
    s = 0
    for d in digits(n):
        s = s + d
    return s
def conte_digit(n, dg):
    trobat = False
    for d in digits(n):
        trobat = d == dg
        if trobat:
            break
    return trobat

Exemples

  • Successió de Fibonacci: fib.py, fib.txt.

    def fibonacci():
        a, b = 0, 1
        while True:
            yield a
            a, b = b, a+b
    
    >>> from fib import fibonacci
    
    >>> it = fibonacci()
    >>> for i in range(10):
    ...     print(next(it), end=', ')
    0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 
    
  • Progressions aritmètica i geomètrica: prog.py, prog.txt.

    def aritmetica(a, d):
        an = a
        while True:
            yield an
            an = an + d
    
    def geometrica(a, r):
        an = a
        while True:
            yield an
            an = an * r
    
    

    Important

    Observeu que les progressions aritmètica i geomètrica són successions infinites. Una funció generadora que calcula un iterador infinit normalment s’implementa amb una sentència while amb l’expressió True.

    Perill

    Una sentència while amb l’expressió True només té sentit en una funció generadora, mai en una funció.

    >>> from prog import *
    
    >>> it = aritmetica(3, 2)
    >>> for i in range(5):
    ...     print(next(it), end=', ')
    3, 5, 7, 9, 11, 
    
    >>> it = geometrica(2, 3)
    >>> for i in range(5):
    ...     print(next(it), end=', ')
    2, 6, 18, 54, 162, 
    

    Perill

    Cal anar en compte al recòrrer un iterador infinit. Cal recòrrer només alguns elements com en els exemples anteriors. Si els intentem recòrrer tots, el programa no acabarà. Per exemple, el fragment següent no acabarà mai:

    it = aritmetica(3, 2)
    for e in it:
        print(e)
    
  • Nombres triangulars: triangulars.py.

    def triangulars():
        n = 1
        tn = 1
        while True:
            yield tn
            n = n + 1
            tn = tn + n
    
    def quants_cal_sumar(v):
        q = 0
        s = 0
        for t in triangulars():
            if s >= v:
                break
            s = s + t
            q = q + 1
        return q
    
    def llista_mespetits(v):
        r = []
        for t in triangulars():
            if t > v:
                break
            r.append(t)
        return r
    
    def mitjana_parells(v):
        np = 0
        sp = 0
        for t in triangulars():
            if t > v:
                break
            if t%2 == 0:
                sp = sp + t
                np = np + 1
        return sp/np
    
    def es_triangular(n):
        for b, t in enumerate(triangulars(), 1):
            if t >= n:
                break
        if t == n:
            r = b
        else:
            r = 0
        return r
    
  • Iterador ordenat? ord.py, ord.txt.

    def ordenat_creixent(itb):
        it = iter(itb)
        trobat = False
        a = next(it)
        for b in it:
            trobat = a >= b
            if trobat:
                break
            a = b
        return not trobat
    
    >>> from ord import *
    
    >>> from digits import digits
    
    >>> it = digits(1234)
    >>> ordenat_creixent(it)
    False
    >>> it = digits(1234)
    >>> list(it)
    [4, 3, 2, 1]
    
    >>> it = digits(9642)
    >>> ordenat_creixent(it)
    True
    
    >>> ordenat_creixent([1, 3, 4, 8])
    True
    >>> ordenat_creixent([1, 3, 7, 4, 2])
    False