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

Funktionale Programmierung mit Python

Funktionale Programmierung mit Python

Das funktionale Programmierparadigma bietet interessante Möglichkeiten, die helfen können robustere, besser testbare und modularere Programme zu erhalten.
Der Vortrag gibt einen Überblick über die funktionalen Programmiermerkmale in Python und wie diese zu besseren Programmen beitragen können.

Mike Müller

October 31, 2012
Tweet

More Decks by Mike Müller

Other Decks in Programming

Transcript

  1. Funktionale Programmierung mit Python Ein Vortrag auf der PyCon DE

    2012 30. Oktober 2012 Autor: Dr.-Ing. Mike Müller E-Mail: [email protected]
  2. Einleitung • Funktionales Paradigma mit langer Geschichte • Lisp 1958

    • Renaissance: F#, Haskell, Erlang … • Einsatz in der Industrie (Trading, Hardware-nah, algorithmisch)
  3. Merkmale der funktionalen Programmierung • die Funktion im Mittelpunkt •

    "reine" Funktionen ohne Seiteneffekte • unveränderliche Datenstrukturen • Zustandsspeicherung in Funktionen • Rekursive Funktionsaufrufe statt Schleifen
  4. Vorteile der funktionalen Programmierung • Fehlen von Seiteneffekten kann zu

    robusteren Programmen führen • häufig "kleinteiliger" • Bessere Testbarkeit • Fokus auf die Algorithmen • Grundsätzliche Eignung für parallele Verarbeitung
  5. Nachteile der funktionalen Programmierung • Teilweise sehr andere Lösungen im

    Vergleich zu prozeduralen / objekt-orientierten • Nicht für alle Probleme gleichermaßen gut geeignet • Input/Output passt nicht ins Konzept • Rekursion ist "eine Größenordnung" komplexer als eine Schleife • Unveränderliche Datenstrukturen können die Laufzeit verlängern
  6. Funktionale Merkmale in Python - Überblick • Reine Funktionen •

    Closures - Funktionen als Zustandsspeicher • Unveränderliche Datentypen • Verzögerte Auswertung - Generatoren und Co. • Rekursion besser nicht - "Schleifen sind eine Größenordnung einfacher"
  7. Reine Funktionen • kein Seiteneffekt, nur Rückgabewert • "shallow copy"-Problematik

    def do_pure(data): """Return copy times two. """ return data * 2 • nur Seiteneffekt def do_side_effect(my_list): """Modify list appending 100. """ my_list.append(100)
  8. Funktionen als Objekte def func1(): return 1 def func2(): return

    2 >>> my_funcs = {'a': func1, 'b': func2} >>> my_funcs['a']() 1 >>> my_funcs['b']() 2 • alles ist ein Objekt
  9. Closures und "Currying" >>> def outer(outer_arg): >>> def inner(inner_arg): >>>

    return inner_arg + outer_arg >>> return inner >>> func = outer(10) >>> func(5) 15 >>> func.__closure__ (<cell at 0x10286f558: int object at 0x100313170>,) >>> func.__closure__[0] <cell at 0x10286f558: int object at 0x100313170> >>> func.__closure__[0].cell_contents 10
  10. Partielle Funktionen • Module functools enthält einige Werkzeuge für funktionale

    Ansätze >>> import functools >>> def func(a, b, c): ... return a, b, c ... >>> p_func = functools.partial(func, 10) >>> p_func(3, 4) 10 3 4 >>> p_func = functools.partial(func, 10, 12) >>> p_func(3) 10 12 3
  11. Dekoratoren • Anwendung von Closures >>> import functools >>> def

    decorator(func): ... @functools.wraps(func) ... def new_func(*args, **kwargs): ... print 'decorator was here' ... return func(*args, **kwargs) ... return new_func ... >>> @decorator ... def add(a, b): ... return a + b ... >>> add(2, 3) decorator was here 5 >>>
  12. Unveränderliche Datentypen - Tupel statt Listen >>> my_list = range(10)

    >>> my_list [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> my_tuple = tuple(my_list) >>> my_tuple (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) • widerspricht etwas der Nutzungsempfehlung: • Listen == gleiche Elemente • Tuple == "benannte" Elemente
  13. Unveränderliche Datentypen - Sets einfrieren >>> my_set = set(range(5)) >>>

    my_set set([0, 1, 2, 3, 4]) >>> my_frozenset = frozenset(my_set) >>> my_frozenset frozenset([0, 1, 2, 3, 4]) • Verwendung als Dictionary-Schlüssel
  14. Nicht nur funktional • Rein funktionale Programme sind häufig schwierig

    umzusetzen • Kombination mit prozeduralen und objekt-orientierten Programmteilen • Richtiges Werkzeug für den jeweiligen Zweck auswählen • Gespür dafür entwickeln wo funktionale Ansätze passen
  15. Seiteneffekte vermeiden class MyClass(object): """Example for init-only definitions. """ def

    __init__(self): self.attr1 = self.make_attr1() self.attr2 = self.make_attr2() @staticmethod def make_attr1(): """Do many things to create attr1. """ attr1 = [] # skipping many lines return attr1
  16. Seiteneffekte vermeiden • Alle Attribute in __init__ setzen • Statische

    Methoden passen hier gut • Weniger Seiteneffekte als bei Setzen von Attributen außerhalb der __init__ • Klassen und Instanzen gibt es immer noch
  17. Klassen einfrieren class Reader(object): def __init__(self): self.data = self.read() @staticmethod

    def read(): """Return tuple of read data. """ data = [] with open('data.txt') as fobj: for line in fobj: data.append(tuple(line.split())) return tuple(data) • Veränderliche Datenstrukturen sind sehr nützlich zum Einlesen • "Einfrieren" und zu Nur-Lese-Versionen umwandeln • Weitere, unerwünschte Änderungen unmöglich
  18. Klassen einfrieren - Der Einzeiler class Reader(object): def __init__(self): self.data

    = self.read() @staticmethod def read(): """Return tuple of read data. """ return tuple(tuple(line.split()) for line in open('data.txt'))
  19. Stufenweise Einfrieren und wieder Auftauen class FrozenUnFrozen(object): def __init__(self): self.__repr

    = {} self.__frozen = False def __getitem__(self, key): return self.__repr[key] def __setitem__(self, key, value): if self.__frozen: raise KeyError('Cannot change key %r' % key) self.__repr[key] = value def freeze(self): self.__frozen = True def unfreeze(self): self.__frozen = False
  20. Stufenweises Einfrieren und wieder Auftauen II >>> fuf = FrozenUnFrozen()

    >>> fuf['a'] = 100 >>> fuf['a'] 100 >>> fuf.freeze() >>> fuf['a'] = 100 Traceback (most recent call last): File "<interactive input>", line 1, in <module> File "C:\boerse\freeze.py", line 9, in __setitem__ raise KeyError('Cannot change key %r' % key) KeyError: "Cannot change key 'a'" >>> fuf['a'] 100 >>> fuf.unfreeze() >>> fuf['a'] = 100 >>>
  21. Anwendungsbeispiele - Einfrieren • Altsysteme: Werden die Daten irgendwo verändert?

    • Komplexe Verarbeitung: Unabsichtliche Änderung wahrscheinlich • Weitere Nutzung von funktionalen Merkmalen wie Generatoren
  22. Verzögerte Auswertung • Iteratoren und Generatoren >>> [x * 2

    for x in xrange(5)] [0, 2, 4, 6, 8] >>> (x * 2 for x in xrange(5)) <generator object <genexpr> at 0x00F1E878> >>> sum(x *x for x in xrange(10)) 285 • spart Speicher und ggf. CPU-Zeit
  23. Itertools - "Lazy Programmers are Good Programmers" • Module itertools

    bietet Werkzeuge für Arbeit mit Iteratoren >>> it.izip('abc', 'xyz') <itertools.izip object at 0x00FA9F80> >>> list(it.izip('abc', 'xyz')) [('a', 'x'), ('b', 'y'), ('c', 'z')] >>> list(it.islice(iter(range(10)), None, 8, 2)) [0, 2, 4, 6] >>> range(10)[:8:2] [0, 2, 4, 6]
  24. Pipelining - Befehlsverknüpfungen • Generatoren eignen sich für "Pipelines" •

    Gut für Workflow-Probleme • Beispiel Parsen einer Log-Datei
  25. Pipelining - Beispiel lines = read_forever(open(file_name)) filtered_lines = filter_comments(lines) numbers

    = get_number(filtered_lines) sum_ = 0 for number in numbers: sum_ += number print('sum: %d' % sum_)
  26. Koroutinen - "Schieben" • Generatoren "ziehen" die Daten • Kouroutinen

    == Generatoren genutzt mit send() • Koroutinen - "schieben" die Daten
  27. Koroutinen - Beispiel # read_forever > filter_comments > get_number >

    TARGETS read_forever(open(file_name), filter_comments(get_number(TARGETS)))
  28. Schlussfolgerungen • Python hat einige nützliche funktionale Eigenschaften • ist

    aber keine "reine" funktionale Sprache • für manche Aufgaben ist der funktionaler Ansatz gut geeignet • für andere weniger • Kombination mit OO und prozeduraler Programmierung • "Stay pythonic, be pragmatic"
  29. Generatoren - "Ziehen" - Importieren """Use generators to sum log

    file data on the fly. """ import sys import time
  30. Generatoren - "Ziehen" - Datei lesen def read_forever(fobj): """Read from

    a file as long as there are lines. Wait for the other process to write more lines. """ counter = 0 while True: line = fobj.readline() if not line: time.sleep(0.1) continue yield line
  31. Generatoren - "Ziehen" - Kommentare filtern def filter_comments(lines): """Filter out

    all lines starting with #. """ for line in lines: if not line.strip().startswith('#'): yield line
  32. Generatoren - "Ziehen" - Zahlen konvertieren def get_number(lines): """Read the

    number in the line and convert it to an integer. """ for line in lines: yield int(line.split()[-1])
  33. Generatoren - "Ziehen" - alles anstoßen def show_sum(file_name='out.txt'): """Start all

    the generators and calculate the sum continuously. """ lines = read_forever(open(file_name)) filtered_lines = filter_comments(lines) numbers = get_number(filtered_lines) sum_ = 0 try: for number in numbers: sum_ += number sys.stdout.write('sum: %d\r' % sum_) sys.stdout.flush() except KeyboardInterrupt: print 'sum:', sum_
  34. Generatoren - "Ziehen" - alles anstoßen II if __name__ ==

    '__main__': import sys show_sum(sys.argv[1])
  35. Koroutinen - "Schieben" Log-Datei: ERROR: 78 DEBUG: 72 WARN: 99

    CRITICAL: 97 FATAL: 40 FATAL: 33 CRITICAL: 34 ERROR: 18 ERROR: 89 ERROR: 46
  36. Koroutinen - "Schieben" - Imports """Use coroutines to sum log

    file data with different log levels. """ import functools import sys import time
  37. Koroutinen - "Schieben" - Initialisieren mit Dekorator def init_coroutine(func): functools.wraps(func)

    def init(*args, **kwargs): gen = func(*args, **kwargs) next(gen) return gen return init
  38. Koroutinen - "Schieben" - Datei lesen def read_forever(fobj, target): """Read

    from a file as long as there are lines. Wait for the other process to write more lines. Send the lines to `target`. """ counter = 0 while True: line = fobj.readline() if not line: time.sleep(0.1) continue target.send(line)
  39. Koroutinen - "Schieben" - Kommentare filtern @init_coroutine def filter_comments(target): """Filter

    out all lines starting with #. """ while True: line = yield if not line.strip().startswith('#'): target.send(line)
  40. Koroutinen - "Schieben" - Zahl umwandeln @init_coroutine def get_number(targets): """Read

    the number in the line and convert it to an integer. Use the level read from the line to choose the to target. """ while True: line = yield level, number = line.split(':') number = int(number) targets[level].send(number)
  41. Koroutinen - "Schieben" - Konsument I # Consumers for different

    cases. @init_coroutine def fatal(): """Handle fatal errors.""" sum_ = 0 while True: value = yield sum_ += value sys.stdout.write('FATAL sum: %7d\n' % sum_) sys.stdout.flush()
  42. Koroutinen - "Schieben" - Konsument II @init_coroutine def critical(): """Handle

    critical errors.""" sum_ = 0 while True: value = yield sum_ += value sys.stdout.write('CRITICAL sum: %7d\n' % sum_)
  43. Koroutinen - "Schieben" - Konsument III @init_coroutine def error(): """Handle

    normal errors.""" sum_ = 0 while True: value = yield sum_ += value sys.stdout.write('ERROR sum: %7d\n' % sum_)
  44. Koroutinen - "Schieben" - Konsument IV @init_coroutine def warn(): """Handle

    warnings.""" sum_ = 0 while True: value = yield sum_ += value sys.stdout.write('WARN sum: %7d\n' % sum_)
  45. Koroutinen - "Schieben" - Konsument V @init_coroutine def debug(): """Handle

    debug messages.""" sum_ = 0 while True: value = (yield) sum_ += value sys.stdout.write('DEBUG sum: %7d\n' % sum_)
  46. Koroutinen - "Schieben" - alle Konsumenten TARGETS = {'CRITICAL': critical(),

    'DEBUG': debug(), 'ERROR': error(), 'FATAL': fatal(), 'WARN': warn()}
  47. Koroutinen - "Schieben" - Anstoßen def show_sum(file_name='out.txt'): """Start start the

    pipline. """ # read_forever > filter_comments > get_number > TARGETS read_forever(open(file_name), filter_comments(get_number(TARGETS))) if __name__ == '__main__': show_sum(sys.argv[1])