=================== 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 :ref:`funcions predefinides `, o dels mòduls :mod:`itertools` o :mod:`functools` #. Veiem com les expressions lambda permeten estalviar-nos de definir petites funcions auxiliars. #. Veiem com el mòdul :mod:`operator` també permet estalviar-nos de definir petites funcions auxiliars o expressions lambda en alguns casos. Exemple ======= Resolem el problema :doc:`fi:temes/llistes_i_tuples/Cinc_al_dia/index`. Solució clàssica ---------------- Recorrem la llista de parelles aliment-ració sumant només les racions que corresponen a fruita o verdura. .. literalinclude:: aliments.py :start-after: # aliments_1 :end-before: # aliments_2 Aquesta funció calcula el nombre total de racions de fruita i verdura que hi ha a la llista. Per exemple: .. code-block:: pycon >>> 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ó ---------------------------------------- .. literalinclude:: aliments.py :start-after: # aliments_lpc :end-before: # aliments_eg - La variable `racions` conté la llista de racions que corresponen a fruita o verdura. Per exemple: .. code-block:: pycon >>> 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ó :func:`aliments_lpc` resol el problema encadenant els tractaments de filtrar i aplicar (la llista per comprensió), i sintetitzar (sumar en aquest cas). .. code-block:: pycon >>> 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 :code:`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. .. literalinclude:: aliments.py :start-after: # aliments_eg .. code-block:: pycon >>> 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 --------------------------------------- .. literalinclude:: aliments.py :start-after: # aliments_2 :end-before: # aliments_3 - La funció :func:`filtra_fruita_verdura` retorna un llista que conté només les parelles que corresponen a racions de fruita i verdura. Per exemple: .. code-block:: pycon >>> 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ó :func:`aplica_racio` retorna una llista on només hi ha el nombre de racions i no el nom de l'aliment. Per exemple: .. code-block:: pycon >>> from aliments import aplica_racio >>> lr = aplica_racio(lfv) >>> lr [5, 2, 3] - La funció :func:`aliments_2` resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar (sumar en aquest cas). .. code-block:: pycon >>> 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 :code:`fruita_verdura` i :code:`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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: aliments.py :start-after: # aliments_3 :end-before: # aliments_4 - La funció :func:`filtra` generalitza la funció :func:`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 :code:`True` si ha de formar part dels elements de la llista resultat o :code:`False` altrament. Cal definir la petita funció booleana :func:`fruita_verdura` que retorna :code:`True` si la parella aliment-ració correspon a fruita o verdura i :code:`False` altrament. Per exemple: .. code-block:: pycon >>> 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ó :func:`aplica` generalitza la funció :func:`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ó :func:`racio` que donada una parella aliment-ració retorna només la ració. Per exemple: .. code-block:: pycon >>> from aliments import aplica, racio >>> lfv[0] ['verdura', 5] >>> racio(lfv[0]) 5 >>> lr = aplica(racio, lfv) >>> lr [5, 2, 3] - La funció :func:`aliments_3` resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usant les funcions generalitzades. .. code-block:: pycon >>> 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 :func:`filtra` i :func:`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: .. code-block:: pycon >>> 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: aliments.py :start-after: # aliments_4 :end-before: # aliments_5 - Recordem que només cal eliminar la inicialització de la llista i el :code:`return`, i canviar :meth:`~list.append` per :code:`yield`. - La funció :func:`aliments_4` resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usant les funcions generadores. .. code-block:: pycon >>> 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 :code:`fruita_verdura` i :code:`racions` són iteradors perquè les funcions :func:`fitra_iter` i :func:`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. .. __: http://pythontutor.com/visualize.html#code=def%20fruita_verdura%28ar%29%3A%0A%20%20%20%20return%20ar%5B0%5D%20in%20%28'fruita',%20'verdura'%29%0A%0Adef%20racio%28ar%29%3A%0A%20%20%20%20return%20ar%5B1%5D%0A%0Adef%20aliments_4%28aliments_racions%29%3A%0A%20%20%20%20fruites_verdures%20%3D%20filtra_iter%28fruita_verdura,%20aliments_racions%29%0A%20%20%20%20racions%20%3D%20aplica_iter%28racio,%20fruites_verdures%29%0A%20%20%20%20sr%20%3D%20sum%28racions%29%0A%20%20%20%20return%20sr%0A%0Adef%20filtra_iter%28condicio,%20iterable%29%3A%0A%20%20%20%20for%20elem%20in%20iterable%3A%0A%20%20%20%20%20%20%20%20if%20condicio%28elem%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20yield%20elem%0A%0Adef%20aplica_iter%28funcio,%20iterable%29%3A%0A%20%20%20%20for%20elem%20in%20iterable%3A%0A%20%20%20%20%20%20%20%20elem_r%20%3D%20funcio%28elem%29%0A%20%20%20%20%20%20%20%20yield%20elem_r%0A%0A%0Alar%20%3D%20%5B%5B'pa',%202%5D,%20%5B'llet',%203%5D,%20%5B'verdura',%205%5D,%20%5B'fruita',%202%5D,%20%5B'pa',%202%5D,%20%5B'verdura',%203%5D%5D%0Anr%20%3D%20aliments_4%28lar%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false Substituïm les funcions generadores per funcions predefinides ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: aliments.py :start-after: # aliments_5 :end-before: # aliments_6 - La funció :func:`aliments_5` resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usa les :ref:`funcions predefinides ` :func:`filter`, :func:`map` i :func:`sum`. .. code-block:: pycon >>> 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 :ref:`funcions predefinides ` i els mòduls :mod:`itertools` i :mod:`functools`. Usem expressions :code:`lambda` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: aliments.py :start-after: # aliments_6 :end-before: # aliments_7 - La funció :func:`aliments_6` resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usa les :ref:`funcions predefinides ` :func:`filter`, :func:`map` i :func:`sum`. També utilitza :ref:`expressions lambda ` per evitar definir funcions molt senzilles i que difícilment s'usaran en altres llocs. .. code-block:: pycon >>> 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 :mod:`operator` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: aliments.py :start-after: # aliments_7 :end-before: # aliments_lpc - La funció :func:`aliments_7` resol el problema encadenant els tractaments de filtrar, aplicar i sintetitzar, però ara usa les :ref:`funcions predefinides ` :func:`filter`, :func:`map` i :func:`sum`. També utilitza una :ref:`expressió lambda ` per evitar definir la funció :func:`fruita_verdura` i la funció :func:`operator.itemgetter` per obtenir el primer element d'una llista. .. code-block:: pycon >>> from aliments import aliments_7 >>> lar = [['pa', 2], ['llet', 3], ['verdura', 5], ['fruita', 2], ['pa', 2], ['verdura', 3]] >>> aliments_7(lar) 10 .. note:: Podeu descarregar el fitxer :download:`aliments.py` complet que inclou totes les versions anteriors de la funció :func:`aliments`.