Encadenar iteradors¶
Objectius¶
Exposar una nova estratègia per resoldre problemes basada en encadenar tractaments sobre iteradors.
Discutir els avantatges i inconvenients respecte de l’estratègia clàssica.
Veure com l’ús d’iteradors minimitza els inconvenients i fa viable la nova estratègia.
Metodologia¶
Triem un problema senzill de procés de llistes.
Resolem el problema seguint l’estratègia clàssica.
Resolem el problema usant funcions auxiliars que implementen esquemes habituals de tractament de llistes: sintetitzar, filtrar, aplicar…
Generalitzem les funcions auxiliars.
Convertim les funcions generalitzades en funcions generadores.
Substituïm les funcions generadores per funcions predefinides, o dels mòduls
itertools
ofunctools
Veiem com les expressions lambda permeten estalviar-nos de definir petites funcions auxiliars.
Veiem com el mòdul
operator
també permet estalviar-nos de definir petites funcions auxiliars o expressions lambda en alguns casos.
Exemple¶
Resolem el problema Cinc al dia.
Solució clàssica¶
Recorrem la llista de parelles aliment-ració sumant només les racions que corresponen a fruita o verdura.
def aliments_1(aliments_racions):
sr = 0
for ar in aliments_racions:
if ar[0] in ('fruita', 'verdura'):
sr = sr + ar[1]
return sr
Aquesta funció calcula el nombre total de racions de fruita i verdura que hi ha a la llista. Per exemple:
>>> from aliments import aliments_1
>>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]]
>>> aliments_1(lar)
10
Solució basada en llistes per comprensió¶
def aliments_lpc(aliments_racions):
racions = [ar[1] for ar in aliments_racions if ar[0] in ('fruita', 'verdura')]
sr = sum(racions)
return sr
La variable racions conté la llista de racions que corresponen a fruita o verdura. Per exemple:
>>> aliments_racions = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> racions = [ar[1] for ar in aliments_racions if ar[0] in ('fruita', 'verdura')] >>> racions [5, 2, 3]
La funció
aliments_lpc()
resol el problema encadenant els tractaments de filtrar i aplicar (la llista per comprensió), i sintetitzar (sumar en aquest cas).>>> from aliments import aliments_lpc >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_lpc(lar) 10
Observem que en aquesta solució la variable
racions
és una llista i que per calcular-la ha calgut un recorregut. Per tant, aquesta solució consumeix més memòria i més temps que la inicial.Ara bé, la llista per comprensió es pot convertir en una expressió generadora. Així, la solució següent és tan eficient com la inicial.
def aliments_eg(aliments_racions): racions = (ar[1] for ar in aliments_racions if ar[0] in ('fruita', 'verdura')) sr = sum(racions) return sr
>>> from aliments import aliments_eg >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_eg(lar) 10
Solució basada en encadenar tractaments¶
def aliments_2(aliments_racions):
fruites_verdures = filtra_fruita_verdura(aliments_racions)
racions = aplica_racio(fruites_verdures)
sr = sum(racions)
return sr
def filtra_fruita_verdura(aliments_racions):
fv = []
for ar in aliments_racions:
if ar[0] in ('fruita', 'verdura'):
fv.append(ar)
return fv
def aplica_racio(fruites_verdures):
rs = []
for ar in fruites_verdures:
r = ar[1]
rs.append(r)
return rs
La funció
filtra_fruita_verdura()
retorna un llista que conté només les parelles que corresponen a racions de fruita i verdura. Per exemple:>>> from aliments import filtra_fruita_verdura >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> lfv = filtra_fruita_verdura(lar) >>> lfv [['verdura', 5], ['fruita', 2], ['verdura', 3]]
La funció
aplica_racio()
retorna una llista on només hi ha el nombre de racions i no el nom de l’aliment. Per exemple:>>> from aliments import aplica_racio >>> lr = aplica_racio(lfv) >>> lr [5, 2, 3]
La funció
aliments_2()
resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar (sumar en aquest cas).>>> from aliments import aliments_2 >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_2(lar) 10
Observem que en aquesta solució les variables
fruita_verdura
iracions
són llistes i que per calcular cada una d’elles ha calgut un recorregut. Per tant, aquesta solució consumeix més memòria i més temps que la inicial. Més endavant veurem que l’ús d’iteradors minimitza aquests inconvenients.
Generalitzem filtra i aplica¶
def aliments_3(aliments_racions):
fruites_verdures = filtra(fruita_verdura, aliments_racions)
racions = aplica(racio, fruites_verdures)
sr = sum(racions)
return sr
def fruita_verdura(ar):
return ar[0] in ('fruita', 'verdura')
def racio(ar):
return ar[1]
def filtra(condicio, llista):
r = []
for elem in llista:
if condicio(elem):
r.append(elem)
return r
def aplica(funcio, llista):
r = []
for elem in llista:
elem_r = funcio(elem)
r.append(elem_r)
return r
La funció
filtra()
generalitza la funciófiltra_fruita_verdura()
. Té un paràmetre més. Aquest nou paràmetre ha de ser una funció booleana que donat un element de la llista retorniTrue
si ha de formar part dels elements de la llista resultat oFalse
altrament. Cal definir la petita funció booleanafruita_verdura()
que retornaTrue
si la parella aliment-ració correspon a fruita o verdura iFalse
altrament. Per exemple:>>> from aliments import filtra, fruita_verdura >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> lar[2] ['verdura', 5] >>> fruita_verdura(lar[2]) True >>> lar[0] ['pa', 2] >>> fruita_verdura(lar[0]) False >>> lfv = filtra(fruita_verdura, lar) >>> lfv [['verdura', 5], ['fruita', 2], ['verdura', 3]]
La funció
aplica()
generalitza la funcióaplica_racio()
. Té un paràmetre més. Aquest nou paràmetre ha de ser una funció que donat un element de la llista retorni l’element que li correspon en la llista resultat. Cal definir la petita funcióracio()
que donada una parella aliment-ració retorna només la ració. Per exemple:>>> from aliments import aplica, racio >>> lfv[0] ['verdura', 5] >>> racio(lfv[0]) 5 >>> lr = aplica(racio, lfv) >>> lr [5, 2, 3]
La funció
aliments_3()
resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usant les funcions generalitzades.>>> from aliments import aliments_3 >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_3(lar) 10
Observem que les funcions
filtra()
iaplica()
són prou generals com per ser usades en qualsevol problema que requereixi filtrar els elements d’un iterable o aplicar una funció a tots ells.Observem que totes les solucions que hem dissenyat fins ara són també vàlides si l’argument és un iterador en comptes d’una llista:
>>> it = iter(lar) >>> aliments_1(it) 10 >>> it = iter(lar) >>> aliments_2(it) 10 >>> it = iter(lar) >>> aliments_3(it) 10
Convertim les funcions generalitzades en funcions generadores¶
def aliments_4(aliments_racions):
fruites_verdures = filtra_iter(fruita_verdura, aliments_racions)
racions = aplica_iter(racio, fruites_verdures)
sr = sum(racions)
return sr
def filtra_iter(condicio, iterable):
for elem in iterable:
if condicio(elem):
yield elem
def aplica_iter(funcio, iterable):
for elem in iterable:
elem_r = funcio(elem)
yield elem_r
Recordem que només cal eliminar la inicialització de la llista i el
return
, i canviarappend()
peryield
.La funció
aliments_4()
resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usant les funcions generadores.>>> from aliments import aliments_4 >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_4(lar) 10
Observem que en aquesta solució les variables
fruita_verdura
iracions
són iteradors perquè les funcionsfitra_iter()
iaplica_iter()
són funcions generadores. Com que els iteradors només calculen el següent element quan és necessari, aquesta solució no consumeix més memòria que la inicial. Pel que fa al temps, podem comprovar usant Python Tutor que els elements de la llista original es recorren només un cop i per tant, que el temps que consumeix aquesta solució és del mateix ordre que en la solució inicial.Concloem que les solucions basades en encadenar tractaments sobre iteradors són tan eficients com les solucions clàssiques, en general.
Substituïm les funcions generadores per funcions predefinides¶
def aliments_5(aliments_racions):
fruites_verdures = filter(fruita_verdura, aliments_racions)
racions = map(racio, fruites_verdures)
sr = sum(racions)
return sr
La funció
aliments_5()
resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usa les funcions predefinidesfilter()
,map()
isum()
.>>> from aliments import aliments_5 >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_5(lar) 10
Trobarem els esquemes habituals de tractament d’iterables en funcions predefinides i els mòduls
itertools
ifunctools
.
Usem expressions lambda
¶
def aliments_6(aliments_racions):
fruites_verdures = filter(lambda ar: ar[0] in ('fruita', 'verdura'), aliments_racions)
racions = map(lambda ar: ar[1], fruites_verdures)
sr = sum(racions)
return sr
La funció
aliments_6()
resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usa les funcions predefinidesfilter()
,map()
isum()
. També utilitza expressions lambda per evitar definir funcions molt senzilles i que difícilment s’usaran en altres llocs.>>> from aliments import aliments_6 >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_6(lar) 10
Usem el mòdul operator
¶
import operator
def aliments_7(aliments_racions):
fruites_verdures = filter(lambda ar: ar[0] in ('fruita', 'verdura'), aliments_racions)
racions = map(operator.itemgetter(1), fruites_verdures)
sr = sum(racions)
return sr
La funció
aliments_7()
resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usa les funcions predefinidesfilter()
,map()
isum()
. També utilitza una expressió lambda per evitar definir la funciófruita_verdura()
i la funcióoperator.itemgetter()
per obtenir el primer element d’una llista.>>> from aliments import aliments_7 >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_7(lar) 10
Nota
Podeu descarregar el fitxer aliments.py
complet
que inclou totes les versions anteriors de la funció
aliments()
.