Slide 1

Slide 1 text

Funktionale Programmierung mit Python Ein Vortrag auf der PyCon DE 2012 30. Oktober 2012 Autor: Dr.-Ing. Mike Müller E-Mail: [email protected]

Slide 2

Slide 2 text

Einleitung • Funktionales Paradigma mit langer Geschichte • Lisp 1958 • Renaissance: F#, Haskell, Erlang … • Einsatz in der Industrie (Trading, Hardware-nah, algorithmisch)

Slide 3

Slide 3 text

Merkmale der funktionalen Programmierung • die Funktion im Mittelpunkt • "reine" Funktionen ohne Seiteneffekte • unveränderliche Datenstrukturen • Zustandsspeicherung in Funktionen • Rekursive Funktionsaufrufe statt Schleifen

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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"

Slide 7

Slide 7 text

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)

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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__ (,) >>> func.__closure__[0] >>> func.__closure__[0].cell_contents 10

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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 >>>

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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'))

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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 "", line 1, in 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 >>>

Slide 21

Slide 21 text

Anwendungsbeispiele - Einfrieren • Altsysteme: Werden die Daten irgendwo verändert? • Komplexe Verarbeitung: Unabsichtliche Änderung wahrscheinlich • Weitere Nutzung von funktionalen Merkmalen wie Generatoren

Slide 22

Slide 22 text

Unveränderliche Datenstrukturen - Gegenargumente • Für manche Algorithmen ungünstig • Kann ineffizient werden

Slide 23

Slide 23 text

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)) at 0x00F1E878> >>> sum(x *x for x in xrange(10)) 285 • spart Speicher und ggf. CPU-Zeit

Slide 24

Slide 24 text

Itertools - "Lazy Programmers are Good Programmers" • Module itertools bietet Werkzeuge für Arbeit mit Iteratoren >>> it.izip('abc', 'xyz') >>> 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]

Slide 25

Slide 25 text

Pipelining - Befehlsverknüpfungen • Generatoren eignen sich für "Pipelines" • Gut für Workflow-Probleme • Beispiel Parsen einer Log-Datei

Slide 26

Slide 26 text

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_)

Slide 27

Slide 27 text

Koroutinen - "Schieben" • Generatoren "ziehen" die Daten • Kouroutinen == Generatoren genutzt mit send() • Koroutinen - "schieben" die Daten

Slide 28

Slide 28 text

Koroutinen - Beispiel # read_forever > filter_comments > get_number > TARGETS read_forever(open(file_name), filter_comments(get_number(TARGETS)))

Slide 29

Slide 29 text

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"

Slide 30

Slide 30 text

Generatoren - "Ziehen" • Log-Datei: 35 29 75 36 28 54 # comment 54 56

Slide 31

Slide 31 text

Generatoren - "Ziehen" - Importieren """Use generators to sum log file data on the fly. """ import sys import time

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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])

Slide 35

Slide 35 text

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_

Slide 36

Slide 36 text

Generatoren - "Ziehen" - alles anstoßen II if __name__ == '__main__': import sys show_sum(sys.argv[1])

Slide 37

Slide 37 text

Koroutinen - "Schieben" Log-Datei: ERROR: 78 DEBUG: 72 WARN: 99 CRITICAL: 97 FATAL: 40 FATAL: 33 CRITICAL: 34 ERROR: 18 ERROR: 89 ERROR: 46

Slide 38

Slide 38 text

Koroutinen - "Schieben" - Imports """Use coroutines to sum log file data with different log levels. """ import functools import sys import time

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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)

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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)

Slide 43

Slide 43 text

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()

Slide 44

Slide 44 text

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_)

Slide 45

Slide 45 text

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_)

Slide 46

Slide 46 text

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_)

Slide 47

Slide 47 text

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_)

Slide 48

Slide 48 text

Koroutinen - "Schieben" - alle Konsumenten TARGETS = {'CRITICAL': critical(), 'DEBUG': debug(), 'ERROR': error(), 'FATAL': fatal(), 'WARN': warn()}

Slide 49

Slide 49 text

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])