Upgrade to Pro — share decks privately, control downloads, hide ads and more …

IL RASOIO DEI GENERATORI DI PYTHON

IL RASOIO DEI GENERATORI DI PYTHON

Come usare i generatori di Python per semplicare il codice e ridurre l'uso di memoria.

Davide Brunato

June 05, 2022
Tweet

More Decks by Davide Brunato

Other Decks in Programming

Transcript

  1. IL RASOIO DEI GENERATORI DI PYTHON (per sempli care il

    codice e ridurre l'uso di memoria) PyCon Nove - Firenze, Aprile 2018 Davide Brunato
  2. Scuola Internazionale Superiore di Studi Avanzati di Trieste Fondata nel

    1978 3 Aree di Ricerca: Fisica, Matematica e Neuroscienze ~260 studenti di Dottorato/PhD
  3. Agenda 1. Iteratori 2. Generatori 3. Test di performance 4.

    Esempi d'uso dei generatori 5. Un parser XPath con generatori
  4. Iterare una sequenza Per iterare una sequenza (lista, tupla, insieme,

    chiavi di un dizionario) la si usa direttamente come argomento di un ciclo for: In alternativa al for possiamo usare due metodi built-in, next e iter, con e un ciclo while: elements = ['square', 'circle', 'triangle'] for item in elements: print(item) elements = ['square', 'circle', 'triangle'] elements_iterator = iter(elements) while True: try: print(next(elements_iterator)) except StopIteration: # iteratore esaurito ... break
  5. Ma ... cos'è un iteratore?? E' un oggetto speci co

    per il contenitore che deve iterare Implementa i metodi speciali iter e next (iterator protocol) Solitamente tende ad esaurirsi (ma si possono de nire iteratori che non terminano mai) my_iterator = iter(['a', 'b', 'c']) my_iterator my_iterator.__iter__, my_iterator.__next__ try: next(my_iterator); next(my_iterator); next(my_iterator); next(my_iterator) except StopIteration: print("Esaurito!!") <list_iterator at 0x7f5c0807ab70> (<method-wrapper '__iter__' of list_iterator object at 0x7f5c0807ab70>, <method-wrapper '__next__' of list_iterator object at 0x7f5c0807ab70>) Esaurito!!
  6. Creare nuovi iteratori class MyListIterator: def __init__(self, target_list): self.target =

    target_list self.index = 0 def __iter__(self): self.index = 0 return self def __next__(self): try: value = self.target[self.index] except (IndexError, TypeError): self.index = None raise StopIteration() from None else: self.index += 1 return value next = __next__ # Per rendere il codice Python 2.X compatibile my_iterator = MyListIterator([10, True, 'hello']) print(next(my_iterator), next(my_iterator), next(my_iterator)) 10 True hello
  7. Iterare all'in nito Non necessariamente un iteratore deve terminare ...

    class CycleIterator: def __init__(self, target): self.target = target self._iterator = iter(target) def __iter__(self): return self def __next__(self): try: return next(self._iterator) except StopIteration: self._iterator = iter(self.target) return next(self._iterator) values = CycleIterator(['a', 'b', 'c']) print([next(values) for _ in range(10)]) ['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c', 'a']
  8. Iterare senza un iterabile Non necessariamente un iteratore dev'essere basato

    su un oggetto iterabile ... class FibIterator: def __init__(self): self.fib, self.next_fib = 0, 1 def __iter__(self): return self def __next__(self): fib = self.fib self.fib, self.next_fib = self.next_fib, fib + self.next_fib return fib fib = FibIterator() values = [next(fib) for k in range(13)] print(values) print(next(fib)) print(next(fib)) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144] 233 377
  9. Iteratori built-in Nella libreria itertools sono de niti iteratori e

    funzioni di base utili per costruire iteratori Le funzioni built-in map, lter, range, che in Python 2 restituivano una sequenza, in Python 3 restituiscono un iteratore o un oggetto iterabile: import itertools my_counter = itertools.count() print([next(my_counter) for _ in 'abcdefg']) >>> range(10) # Python 2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> filter(lambda x: x<'m', 'atcfjxwzbgjk') 'acfjbgjk' >>> range(10) # Python 3 range(0, 10) >>> filter(lambda x: x<'m', 'atcfjxwzbgjk') <filter object at 0x7f70dde4ad30> [0, 1, 2, 3, 4, 5, 6]
  10. Iteratori e generatori: una visione d'insieme Cortesia di Vincent Dressien,

    l'originale si trova nel post . http://nvie.com/posts/iterators-vs-generators/
  11. Generator expressions Le espressioni generatrici sono delle espressioni, delimitate da

    parentesi tonde, che ritornano un generatore: Come una list comprehension posso annidare le espressioni e mettere delle condizioni: square_values = (i*i for i in range(10)) # genera 0, 1, 4, ... 81 print(repr(square_values)) print(type(square_values)) print(square_values.__iter__) print(square_values.__next__) >>> (i*i for k in range(10) for i in range(k) if i != k) <generator object <genexpr> at 0x7f5c080ab468> <class 'generator'> <method-wrapper '__iter__' of generator object at 0x7f5c080ab468> <method-wrapper '__next__' of generator object at 0x7f5c080ab468> <generator object <genexpr> at 0x7f5c0808e150>
  12. Espressioni generatrici come argomento Posso passare espressioni generatrici come argomento

    a funzioni che richiedono un oggetto iterabile: Quando solo l'unico argomento passato alla funzione le parentesi tonde possono essere omesse: Le parentesi tonde servono a delimitare l'espressione generatrice: >>> sum((i*i for i in range(10))) >>> sum(i*i for i in range(10)) list((i*i for i in range(k)) for k in range(3)) 285 285 [<generator object <genexpr>.<genexpr> at 0x7f5c0808e1a8>, <generator object <genexpr>.<genexpr> at 0x7f5c0808e200>, <generator object <genexpr>.<genexpr> at 0x7f5c0808e048>]
  13. Generator functions Le funzioni generatrici o generator functions sono funzioni

    che ritornato un generatore, caratterizzate dall'uso dell'istruzione yield (ne serve almeno una esplicita) al posto del classico return. def fibonacci(): fib, next_fib = 0, 1 while True: yield fib fib, next_fib = next_fib, fib + next_fib fib = fibonacci() print(fib) values = [next(fib) for k in range(13)] print(values) print(next(fib)) print(next(fib)) <generator object fibonacci at 0x7f5c0808e780> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144] 233 377
  14. Struttura di un generatore Una funzione generatrice è a tutti

    gli effetti una funzione: Quando chiamata ritorna un generatore: repr(fibonacci) fib = fibonacci() print(repr(fib)) print(fib.__iter__) print(fib.__next__) iter(fib) is fib # iter(fib) ritorna il generatore stesso '<function fibonacci at 0x7f5c08087400>' <generator object fibonacci at 0x7f5c080abe60> <method-wrapper '__iter__' of generator object at 0x7f5c080abe60> <method-wrapper '__next__' of generator object at 0x7f5c080abe60> True
  15. Funzioni generatrici e return Si possono usare dei return privi

    di valore di ritorno: Il return genera uno StopIteration: def safe_fibonacci(max_value): fib, next_fib = 0, 1 while True: if max_value < fib: return # genera solo uno StopIteration... else: yield fib fib, next_fib = next_fib, fib + next_fib fib = safe_fibonacci(300) max_fib = 0 while True: try: max_fib = next(fib) except StopIteration: print("Massimo numero di Fibonacci: {}".format(max_fib)) break Massimo numero di Fibonacci: 233
  16. Usare un generatore In genere si usa il costrutto for

    che gestisce automaticamente l'esaurimento del generatore: Oppure si può materializzare su strutture dati come un normale iteratore: for value in safe_fibonacci(300): print(value, '', end='') list(safe_fibonacci(300)) [v for v in safe_fibonacci(1000000) if not v % 2] # Numeri di Fibonacci pari < 1000000 0 1 1 2 3 5 8 13 21 34 55 89 144 233 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233] [0, 2, 8, 34, 144, 610, 2584, 10946, 46368, 196418, 832040]
  17. Concatenare generatori Posso concatenare i generatori con altri generatori: def

    odd_filter(values): for v in values: if v % 2: yield v list(odd_filter(safe_fibonacci(1000))) list(odd_filter(x*x for x in range(10))) [1, 1, 3, 5, 13, 21, 55, 89, 233, 377, 987] [1, 9, 25, 49, 81]
  18. Python 2.5+: Generatori potenziati A partire da Python 2.5 yield

    è un'espressione e i generatori includono 3 metodi addizionali: De niti nel documento "PEP 342 - Coroutines via Enhanced Generators" g = odd_filter([0, 21, 2, 80, 3]) print(g.send) print(g.throw) print(g.close) <built-in method send of generator object at 0x7f5c080abe60> <built-in method throw of generator object at 0x7f5c080abe60> <built-in method close of generator object at 0x7f5c080abe60>
  19. Python 3 e i generatori Python 3.3+ include anche il

    costrutto yield from per la delega puntuale a sottogeneratore: Python 3.6+ implementa anche generatori asincroni def chain(g1, g2): for v in g1: yield v for v in g2: yield v list(chain(range(5), 'abcde')) def chain(g1, g2): yield from g1 yield from g2 list(chain(range(5), 'abcde')) [0, 1, 2, 3, 4, 'a', 'b', 'c', 'd', 'e'] [0, 1, 2, 3, 4, 'a', 'b', 'c', 'd', 'e']
  20. Setup per i test from timeit import timeit from sys

    import maxunicode NUM = 30 def run_timeit_test(stmt, setup): print("%s --> %ss" % (stmt, timeit(stmt, setup, number=30)))
  21. Test di velocità VS range built-in def gen1(n): yield from

    range(n) def gen2(n): for i in range(n): yield i setup = 'from __main__ import maxunicode, gen1, gen2' run_timeit_test('list(range(maxunicode))', setup) run_timeit_test('[e for e in range(maxunicode)]', setup) run_timeit_test('list(gen1(maxunicode))', setup) run_timeit_test('list(gen2(maxunicode))', setup) list(range(maxunicode)) --> 1.0693229699973017s [e for e in range(maxunicode)] --> 1.5412012599990703s list(gen1(maxunicode)) --> 2.370624939998379s list(gen2(maxunicode)) --> 2.6127205209995736s
  22. Test di velocità rispetto a lista def func1(s): return [cp

    for cp in s] def func2(s): res = [] for cp in s: res.append(cp) return res def gen1(s): yield from s def gen2(s): for cp in s: yield cp setup = 'from __main__ import maxunicode, func1, func2, gen1, gen2' run_timeit_test('for _ in func1(range(maxunicode)): pass', setup) run_timeit_test('for _ in func2(range(maxunicode)): pass', setup) run_timeit_test('for _ in gen1(range(maxunicode)): pass', setup) run_timeit_test('for _ in gen2(range(maxunicode)): pass', setup) for _ in func1(range(maxunicode)): pass --> 1.8159301619962207s for _ in func2(range(maxunicode)): pass --> 3.484359250003763s for _ in gen1(range(maxunicode)): pass --> 1.8978255050024018s for _ in gen2(range(maxunicode)): pass --> 2.2618252960019163s
  23. Risparmio di memoria %memit sum([x*x for x in range(maxunicode)]) %memit

    sum(x*x for x in range(maxunicode)) peak memory: 95.37 MiB, increment: 34.33 MiB peak memory: 65.86 MiB, increment: 0.00 MiB
  24. Risparmio di memoria indiretto Il problema di consumo di memoria

    è stato risolto modi cando il MutableSet dell'implementazione: https://github.com/brunato/xmlschema/issues/32 Line # Mem usage Increment Line Contents 2 10.762 MiB 10.762 MiB @profile 3 def my_func(): 4 233.723 MiB 222.961 MiB import xmlschema 5 233.723 MiB 0.000 MiB return 2 class UnicodeSubset(MutableSet): def __init__(self): self._code_points = list() def __iter__(self): for item in self._code_points: if isinstance(item, tuple): for cp in range(item[0], item[1] + 1): yield cp else: yield item
  25. Risparmio di codice «A parità di fattori la spiegazione più

    semplice è da preferire», Guglielmo di Occam, 1285 - 1347 Sorgente: Wikimedia Commons - CC BY-SA 3.0
  26. Iter VS Get import xmlschema names = dir(xmlschema.XMLSchema) generators =

    [x for x in names if x.startswith('iter')] getters = [x for x in names if x.startswith('get')] print("%d generators: %r" % (len(generators), generators)) print("%d getters: %r" % (len(getters), getters)) 8 generators: ['iter', 'iter_components', 'iter_decode', 'iter_encode', 'iter_ errors', 'iter_globals', 'iterchildren', 'iterfind'] 2 getters: ['get_converter', 'get_locations']
  27. Esempio 1: sequenze annidate def iter_resources(resource_map): for resources in resource_map.values():

    for v in resources: yield v projects = { 'alpha': ['requirements.txt', 'data.json'], 'beta': ['old_data.xml', 'report.doc'], } print([res for res in iter_resources(projects)]) def flatten_nested_lists(obj): if isinstance(obj, list): for item in obj: for value in flatten_nested_lists(item): yield value else: yield obj values = ['a', ['b'], ['c', ['d', 'e', ['f', 'g'], 'h', [['i'], 'j'], [], 'k']], ['l']] print([e for e in flatten_nested_lists(values)]) ['requirements.txt', 'data.json', 'old_data.xml', 'report.doc'] ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l']
  28. Esempio 2: costruire delle funzioni di validazione class ValidationError(ValueError): pass

    def byte_validator(*values): for x in values: if not (-2**7 <= x < 2**7): yield ValidationError("value x must be -128 <= x < 128: {}".format(x)) print("Numero di errori: %s" % len(list(byte_validator(10, 1000, -300, 2, 5)))) Numero di errori: 2
  29. Un XPath per gli schemi XML Pacchetto XML Schema (2016):

    Quantum ESPRESSO simulation suite MaX - Materials design at the eXascale (Horizon 2020) XPath necessario per: XPath 1.0 per completare XML Schema 1.0 XPath 2.0 per implementare anche XML Schema 1.1 Alternative non applicabili agli schemi: la libreria standard (ElementPath) supporta solo una parte di XPath lxml supporta XPath 1.0 Da modulo a pacchetto: https://github.com/brunato/elementpath
  30. Parser di Pratt Articolo Top Down Operator Precedence di Vaughan

    Pratt (1973) Copia dell'articolo originale: Variante di parser ricorsivo discendente: Ef ciente Modularizzabile e essibile Utilizza dei coef cienti (bind power) per de nire le precedenze tra gli operatori Per una descrizione completa: https://tdop.github.io/ https://www.slideshare.net/percolate/pratt-parser-in-python
  31. Elementi per un parser XPath con metodo di Pratt Classe

    per i Token con 2 attributi base (symbol, value) Classe per il parser: Per costruire facilmente parser diversi (XPath 1.0, XPath 2.0 e varianti limitate ...) Avere un'implementazione base che può essere riutilizzata per altro (es. Fortran) Classe per il contesto dinamico di XPath Il contesto statico implementato direttamente nel parser
  32. Metodi dei token per parser XPath def nud(self): """Pratt's null

    denotation.""" return self def led(self, left): """Pratt's left denotation.""" self[:] = left, expression(self.rbp) return self def evaluate(self, context=None): """Evaluation method for XPath tokens.""" return list(self.select(context)) def select(self, context=None): """Select method for XPath tokens.""" item = self.evaluate(context) if item is not None: if isinstance(item, list): for _item in item: yield _item else: if context is not None: context.item = item yield item
  33. Esempi dal parser XPath 1.0 Moltiplicazione Union expressions @method(infix('*', bp=45))

    def evaluate(self, context=None): if self: return self[0].evaluate(context) * self[1].evaluate(context) @method(infix('|', bp=50)) def select(self, context=None): if context is not None: results = {item for k in range(2) for item in self[k].select(context.copy())} for item in context.iter(): if item in results: yield item
  34. Espressione for (XPath 2.0) @method('for', bp=20) def nud(self): del self[:]

    while True: self.parser.next_token.expected('$') self.append(self.parser.expression(5)) self.parser.advance('in') self.append(self.parser.expression(5)) if self.parser.next_token.symbol == ',': self.parser.advance() else: break self.parser.advance('return') self.append(self.parser.expression(5)) return self @method('for') def select(self, context=None): if context is not None: selectors = tuple(self[k].select( context.copy()) for k in range(1, len(self) - 1, 2)) for results in itertools.product(*selectors): for i in range(len(results)): context.variables[self[i * 2][0].value] = results[i] for result in self[-1].select(context.copy()): yield result
  35. Espressione if (XPath 2.0) @method('if', bp=20) def nud(self): self.parser.advance('(') self[:]

    = self.parser.expression(), self.parser.advance(')') self.parser.advance('then') self[1:] = self.parser.expression(), self.parser.advance('else') self[2:] = self.parser.expression(), return self @method('if') def evaluate(self, context=None): if boolean_value(self[0].evaluate(context)): return self[1].evaluate(context) else: return self[2].evaluate(context) @method('if') def select(self, context=None): if boolean_value(list(self[0].select(context))): for result in self[1].select(context): yield result else: for result in self[2].select(context): yield result