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ènciayield
. Els valors de les variables de la funció es conserven.next
retorna l’objecte al que s’avalua l’expressió de la sentènciayield
.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:
Esborrem la inicialització de la llista i la sentència
return
.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