Save 37% off PRO during our Black Friday Sale! »

Des boucles aux transducers - PyConFr 2015

Des boucles aux transducers - PyConFr 2015

En supprimant les boucles que nous utilisons traditionnellement dans les transformations de données. Et, à travers une série de refactoring successifs, nous verrons comment améliorer le traitement d’importante collection de données sur les questions de :
- la performance,
- la consommation mémoire
- la réusabilité
- et le découplage

Nous démarrerons avec les architectures traditionnelles pour passer aux architectures à Pipeline. Enfin nous terminerons par l'utilisation de certaines techniques de la programmation fonctionnelle afin d’optimiser les traitements jusqu’à pouvoir gérer des collections de tailles infinies.

Le code sera écrit en Python, mais les techniques décrites sont applicables avec n’importe quels autres langages, tous les développeurs sont donc bienvenus.

Une vidéo de cette conférence est disponible sur https://medium.com/software-craftsman/des-boucles-aux-transducers-pyconfr-2015-d9cd1849b429

Beb422437c1dfb5366f197919e41ac50?s=128

Arnaud LEMAIRE
PRO

October 18, 2015
Tweet

Transcript

  1. DES BOUCLES AUX TRANSDUCTEURS @LILOBASE

  2. IL ÉTAIT UNE COLLECTION DE DONNÉE invoices = (
 dict(


    due_date=datetime(2015, 10, 22),
 amount_excl_vat=36,
 quantity=1,
 vat_rate=20,
 paid=False
 ),
 dict(
 due_date=datetime(2015, 9, 18),
 amount_excl_vat=28,
 quantity=1,
 vat_rate=10,
 paid=True
 ),
 dict(
 due_date=datetime(2015, 10, 23),
 amount_excl_vat=17,
 quantity=2,
 vat_rate=5,
 paid=False
 )
 )
  3. EXEMPLE DE TRAITEMENT IMPÉRATIF def get_amount_for_month(invoices, month):
 total_amount = 0


    for invoice in invoices:
 if (invoice['due_date'].month != month):
 continue
 
 total_amount += invoice['amount_excl_vat']\
 + (invoice['amount_excl_vat']
 / 100.0
 * invoice['vat_rate']) \
 * invoice['quantity']
 
 return total_amount
  4. PIPELINES MAP, FILTER, REDUCE…

  5. def get_amount_for_month(invoices, month):
 total_amount = 0
 for invoice in invoices:


    if (invoice['due_date'].month != month):
 continue
 
 total_amount += invoice['amount_excl_vat']\
 + (invoice['amount_excl_vat']
 / 100.0
 * invoice['vat_rate']) \
 * invoice['quantity']
 
 return total_amount IDENTIFICATION DES ÉTAPES DU PIPELINE filter = tri 1 1 reduce = accumulateur 2 2 map = traitements sur chaque élément de la collection 3 3
  6. REFACTORONS POUR UTILISER DES PIPELINES def get_amount_for_month(invoices, month):
 invoices_of_the_month =

    filter(
 lambda invoice: invoice['due_date'].month == month,
 invoices
 )
 
 incl_vat_amount = map(
 lambda invoice: invoice['amount_excl_vat']\
 + (invoice['amount_excl_vat']
 / 100.0
 * invoice['vat_rate']) \
 * invoice['quantity'],
 invoices_of_the_month
 )
 
 return reduce(
 lambda carry, x: carry + x,
 incl_vat_amount
 )
  7. COLLECTION PIPELINE Les étapes sont isolées Suppression des effets de

    bords Le code est plus résilient
  8. EXTRACTION DES FONCTIONS def invoice_is_due_for(month):
 return lambda invoice: \ invoice['due_date'].month

    == month def including_vat(invoice):
 return invoice[‘amount_excl_vat'] \
 + (invoice['amount_excl_vat']
 / 100.0
 * invoice['vat_rate']) \
 * invoice['quantity']
 
 
 def sum_invoice_amount(carry, x):
 return carry + x Clojure = pré-contextualisation d’une fonction 1 1
  9. VERSION REFACTORÉE def get_amount_for_month(invoices, month):
 invoice_of_the_month = \
 filter(invoice_is_due_for(month), invoices)


    
 including_vat_amount = \
 map(including_vat, invoice_of_the_month)
 
 return reduce(sum_invoice_amount, including_vat_amount)
  10. COLLECTION PIPELINE Les comportements sont isolées Les comportements peuvent être

    réutilisés Le code est plus modulaire
  11. REDUCE PEUT ACCUMULER DU NON SCALAIRES def letter_count(sentence):
 
 def

    acc_letter(carry, word):
 if(not carry.has_key(word)):
 carry[word] = 1
 else:
 carry[word] += 1
 
 return carry
 
 return reduce(acc_letter, sentence, {})
  12. MAIS… L’ensemble des éléments de la collection est parcouru à

    chaque étape de transformation Chaque étape de transformation crée une collection intermédiaire Chaque étape supplémentaire augmente le temps de calcul et la mémoire consommée
  13. TRANSDUCERS MAPPING, FILTERING, REDUCING…

  14. MAP OU FILTER PEUVENT ÊTRE EXPRIMÉS VIA REDUCE def reduced_map(callable,

    collection):
 
 def map_reducer(carry, item):
 carry.append(callable(item))
 return carry
 
 return reduce(map_reducer, collection, [])
 
 def reduced_filter(callable, collection):
 
 def filter_reducer(carry, item):
 if callable(item):
 carry.append(item)
 return carry
 
 return reduce(filter_reducer, collection, [])
  15. SORTONS L’INSTRUCTION REDUCE def mapper(callable):
 
 def map_reducer(carry, item):
 carry.append(callable(item))


    return carry
 
 return map_reducer
 
 def filterer(callable):
 
 def filter_reducer(carry, item):
 if callable(item):
 carry.append(item)
 return carry
 
 return filter_reducer
  16. None
  17. UTILISATION def paid_invoice(invoice):
 return invoice['paid']
 
 
 
 reduce(filterer(paid_invoice), invoices,

    [])
  18. DÉPLAÇONS APPEND def appender(carry, item):
 carry.append(item)
 return carry

  19. ET NOS RÉDUCERS DEVIENNENT TRANSDUCERS def mapping(callable):
 def transducer(next_reducer):
 


    def map_reducer(carry, item):
 return next_reducer(carry, callable(item))
 
 return map_reducer
 
 return transducer
 
 
 def filtering(callable):
 def transducer(next_reducer):
 
 def filter_reducer(carry, item):
 if callable(item):
 return next_reducer(carry, item)
 else:
 return carry
 
 return filter_reducer
 
 return transducer
  20. None
  21. ET VOICI LES TRANSDUCTEURS À L’ACTION def adder(carry, item):
 return

    carry + item
 
 
 
 reduce(
 filtering(invoice_is_due_for(10))(
 mapping(including_vat)(
 adder
 )
 ), 
 invoices, 
 0
 )
  22. TRANSDUCERS L’ensemble des traitements est composé 
 en une fonction

    Chaque élément de la collection parcourt l’ensemble des transformations en une fois Le nombre d’itération ne dépend plus du nombre de transformation
  23. TRANSDUCERS Gains en consommation mémoire et temps de calcul Peut

    traiter des ensembles infinis Plus besoin de créer des représentations intermédiaires de la collection
  24. Les traitements se font successivement sur la collection L’ensemble des

    transformations est composé en une fonction pipelines « classiques » transducers
  25. SUCRE SYNTAXIQUE COMPOSE, MAPITY, FILTERITY

  26. COMPOSE def compose2(f, g):
 return lambda *a, **kw: f(g(*a, **kw))


    
 
 def compose(*fs):
 return reduce(compose2, fs)
 
 
 def get_amount_for_month(invoices, month):
 return reduce(
 compose(
 filtering(invoice_is_due_for(month)),
 mapping(including_vat),
 )(adder),
 invoices,
 0
 )
  27. None
  28. AMÉLIORONS L’API DES TRANSDUCERS def mapity(callable, collection = None):
 if

    collection is None:
 return mapping(callable)
 else:
 return map(callable, collection)
 
 
 def filterity(callable, collection = None):
 if collection is None:
 return filtering(callable)
 else:
 return filter(callable, collection)
  29. AMÉLIORONS L’API DES TRANSDUCERS, SUITE… filterity(
 paid_invoice, 
 mapity(
 including_vat,

    
 invoices
 )
 )
 
 compose(
 filterity(paid_invoice),
 mapity(including_vat)
 )(appender) Génération d’un transducer Utilisation avec les pipelines « classiques » 1 1 2 2
  30. MERCI ! @LILOBASE