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

  1. Triem un problema senzill de procés de llistes.

  2. Resolem el problema seguint l’estratègia clàssica.

  3. Resolem el problema usant funcions auxiliars que implementen esquemes habituals de tractament de llistes: sintetitzar, filtrar, aplicar…

  4. Generalitzem les funcions auxiliars.

  5. Convertim les funcions generalitzades en funcions generadores.

  6. Substituïm les funcions generadores per funcions predefinides, o dels mòduls itertools o functools

  7. Veiem com les expressions lambda permeten estalviar-nos de definir petites funcions auxiliars.

  8. 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 i racions 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 retorni True si ha de formar part dels elements de la llista resultat o False altrament. Cal definir la petita funció booleana fruita_verdura() que retorna True si la parella aliment-ració correspon a fruita o verdura i False 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() i aplica() 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 canviar append() per yield.

  • 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 i racions són iteradors perquè les funcions fitra_iter() i aplica_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 predefinides filter(), map() i sum().

    >>> 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 i functools.

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 predefinides filter(), map() i sum(). 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 predefinides filter(), map() i sum(). 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().