Share
Embed
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
Transcript
49 slides
-
Funktionale Programmierung mit Python Ein Vortrag auf der PyCon DE
2012 30. Oktober 2012 Autor: Dr.-Ing. Mike Müller E-Mail: [email protected] -
Einleitung • Funktionales Paradigma mit langer Geschichte • Lisp 1958
• Renaissance: F#, Haskell, Erlang … • Einsatz in der Industrie (Trading, Hardware-nah, algorithmisch) -
Merkmale der funktionalen Programmierung • die Funktion im Mittelpunkt •
"reine" Funktionen ohne Seiteneffekte • unveränderliche Datenstrukturen • Zustandsspeicherung in Funktionen • Rekursive Funktionsaufrufe statt Schleifen -
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 -
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 -
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" -
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) -
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 -
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 -
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 -
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 >>> -
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 -
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 -
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 -
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 -
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 -
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 -
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')) -
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 -
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 >>> -
Anwendungsbeispiele - Einfrieren • Altsysteme: Werden die Daten irgendwo verändert?
• Komplexe Verarbeitung: Unabsichtliche Änderung wahrscheinlich • Weitere Nutzung von funktionalen Merkmalen wie Generatoren -
Unveränderliche Datenstrukturen - Gegenargumente • Für manche Algorithmen ungünstig •
Kann ineffizient werden -
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 -
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] -
Pipelining - Befehlsverknüpfungen • Generatoren eignen sich für "Pipelines" •
Gut für Workflow-Probleme • Beispiel Parsen einer Log-Datei -
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_) -
Koroutinen - "Schieben" • Generatoren "ziehen" die Daten • Kouroutinen
== Generatoren genutzt mit send() • Koroutinen - "schieben" die Daten -
Koroutinen - Beispiel # read_forever > filter_comments > get_number >
TARGETS read_forever(open(file_name), filter_comments(get_number(TARGETS))) -
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" -
Generatoren - "Ziehen" • Log-Datei: 35 29 75 36 28
54 # comment 54 56 -
Generatoren - "Ziehen" - Importieren """Use generators to sum log
file data on the fly. """ import sys import time -
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 -
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 -
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]) -
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_ -
Generatoren - "Ziehen" - alles anstoßen II if __name__ ==
'__main__': import sys show_sum(sys.argv[1]) -
Koroutinen - "Schieben" Log-Datei: ERROR: 78 DEBUG: 72 WARN: 99
CRITICAL: 97 FATAL: 40 FATAL: 33 CRITICAL: 34 ERROR: 18 ERROR: 89 ERROR: 46 -
Koroutinen - "Schieben" - Imports """Use coroutines to sum log
file data with different log levels. """ import functools import sys import time -
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 -
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) -
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) -
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) -
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() -
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_) -
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_) -
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_) -
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_) -
Koroutinen - "Schieben" - alle Konsumenten TARGETS = {'CRITICAL': critical(),
'DEBUG': debug(), 'ERROR': error(), 'FATAL': fatal(), 'WARN': warn()} -
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])