#!/usr/bin/env python # coding: utf-8 # # Debugging and Profiling # ## Debugging # Um ein Programm von Fehlern zu bereinigen, muss man zuerst abklären um welchen Typ von Fehler es sich handelt: # ### Syntaxfehler # # Ein Syntaxfehler tritt unmittelbar bei der (versuchten) Auführung des Programmes auf. # Es gibt viele Gründe hierfür und es ist eine genaue Kontrolle der Stelle unmittelbar bevor der Fehler auftritt notwendig. # Beispiele sind falsch balanzierte Klammern, Zeilenumbrüche mit vergessenen oder falschen Einrückungen, vergessene Zeichen wie Doppelpunkte, Beistriche, usw. # Andererseits sind Syntaxfehler die am einfachsten zu korrigierenden Fehler! # ### Laufzeitfehler # # Bricht ein Programm während der Ausführung ab, so handelt es sich um einen Laufzeitfehler. # Dies sind entweder Ausnahmen ("[Exceptions](https://docs.python.org/2/library/exceptions.html)"), # die höchstwahrscheinlich durch vorschalten geeigneter Tests vermeidbar gewesen wären, oder "Errors". # Letzteres sind Fehler, die nie auftreten dürfen. # # Beispiele: # * Schreiben auf eine geöffnete Datei, die aber bereits geschlossen ist. # * Entfernen eines Elements aus einer Liste, obwohl die Liste schon leer ist (Test: `len(liste) == 0`). # * usw. # # Es gibt auch Exceptions, die im Programmablauf gewollt auftreten. # Zum Beispiel die [StopIteration-Exception](https://docs.python.org/2/library/exceptions.html#exceptions.StopIteration), # welche beim Ende des Iterierens über einen Iterator ausgeworfen wird. # ### Logikfehler # # Das sind Fehler im Ablauf des Programmes, # die keinen Fehler auswerfen aber trotzdem zu einem falschen Ergebnis führen. # Dies sind die kompliziertesten Fehler und können die meisten Kopfschmerzen erzeugen. # # Qualitätstests können helfen, sie zu beseitigen. # Es kann auch nützlich sein, mit dem `assert` Statement Invatianten in den Programmfluss einzubauen. # (Eine "Invariante" ist eine logische Aussage, die immer wahr sein muss.) # # Beispiel: Wir entfernen möglicherweise ohne vorherigen Test 2x ein Element aus einer Liste. # Damit dies immer funktioniert, # fügen wir ein `assert`-Statement ein, # um sofort einen Fehler zu bekommen falls die Liste nicht ausreichend gefüllt sein sollte. # Damit wird vermieden, nur manchaml mit geringer Wahrscheinlichkeit einen Fehler zu bekommen. # In[1]: from random import random ll = [1, 2, 3] assert len(ll) >= 2 if random() > .5: print(ll.pop()) if random() > .9: print(ll.pop()) # ## Profiling # Die einfachste Art des Profilings ist, mit %timeit die Ausführungsgeschwindigkeit zu kontrollieren. # In[2]: import math get_ipython().run_line_magic('timeit', 'math.sqrt(2)') # Es gibt diverse Pythonmodule für Profiling: Zeilenweise für ein Skript oder für den Speicherverbrauch. # # ``` # $ pip install [--user] line-profiler # $ pip install [--user] psutil # $ pip install [--user] memory_profiler # ``` # (In Canopy, ist `--user` nicht notwendig) # Anschließend entweder über die IPython Notebook Konfiguration permanent oder wie folgt manuell diese IPython Magic Funktionen aktivieren. # In[13]: get_ipython().run_line_magic('load_ext', 'memory_profiler') # ## Zeitmessung # Hierfür speichern wir ein kleines (nicht sinnvolles, aber anschauliches) Programm in als Modul `profile_me.py` ab. # In[14]: get_ipython().run_cell_magic('writefile', 'res/profile_me.py', 'def profile_me(e):\n x = 1\n z = [0]\n import math\n while z[-1] < e:\n v = math.sin(x)\n for i in range(x):\n v2 = v + math.sqrt(i)\n z.append(v2)\n x += 1\n return x\n') # In[15]: from res.profile_me import profile_me get_ipython().run_line_magic('lprun', '-f profile_me profile_me(40)') # Man erhält eine detaillierte Auflistung für jede Zeile, wie viel Zeit sie verbrauchte und wie oft sie ausgeführt wurde. # Diese Informationen gehören zu den aussagekräftigsten überhaupt. # Nur auf Basis solcher Analysen kann eine erfolgreiche (und sinnvolle!) Optimierung des Codes stattfinden. # # ``` # Timer unit: 1e-06 s # # Total time: 1.25865 s # File: res/profile_me.py # Function: profile_me at line 1 # # Line # Hits Time Per Hit % Time Line Contents # ============================================================== # 1 def profile_me(e): # 2 1 2 2.0 0.0 x = 1 # 3 1 1 1.0 0.0 z = [0] # 4 1 6 6.0 0.0 import math # 5 1529 1295 0.8 0.1 while z[-1] < e: # 6 1528 1451 0.9 0.1 v = math.sin(x) # 7 1169684 534961 0.5 42.5 for i in range(x): # 8 1168156 718623 0.6 57.1 v2 = v + math.sqrt(i) # 9 1528 1374 0.9 0.1 z.append(v2) # 10 1528 936 0.6 0.1 x += 1 # 11 1 1 1.0 0.0 return x # ``` # In[16]: get_ipython().run_line_magic('prun', 'profile_me(40)') # `%prun` sammelt alle Funktionsaufrufe. # # ``` # 1172743 function calls in 0.344 seconds # # Ordered by: internal time # # ncalls tottime percall cumtime percall filename:lineno(function) # 1 0.229 0.229 0.344 0.344 profile_me.py:1(profile_me) # 1168156 0.106 0.000 0.106 0.000 {math.sqrt} # 1528 0.007 0.000 0.007 0.000 {range} # 1528 0.000 0.000 0.000 0.000 {math.sin} # 1528 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} # 1 0.000 0.000 0.344 0.344 :1() # 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} # ``` # ## Speicherverbrauch # # Speicherverbrauch gehört zu den kompliziertesten Aspekten des Profilings. # Objekte werden dynamisch erzeugt und hierfür dynamisch Speicher angelegt. # Der [Garbage Collector](http://en.wikipedia.org/wiki/Garbage_collection_%28computer_science%29) räumt nicht mehr benötigte Objekte auf. # Spannend ist daher, wieviel Speicherverbrauch während des Ausführens verbraucht wird (Maximalwert) und ob wiederholende Aufrufe den Speicherverbrauch immer weiter erhöhen. # In[17]: get_ipython().run_line_magic('memit', '-r 3 profile_me(10)') # Für %mprun muss das Skript in einer physischen Datei liegen, # wir nehmen das `profile_me.py` von früher. # In[18]: from res.profile_me import profile_me get_ipython().run_line_magic('mprun', '-f profile_me profile_me(10)') # ``` # Filename: res/profile_me.py # # Line # Mem usage Increment Line Contents # ================================================ # 1 29.8 MiB 0.0 MiB def profile_me(e): # 2 29.8 MiB 0.0 MiB x = 1 # 3 29.8 MiB 0.0 MiB z = [0] # 4 29.8 MiB 0.0 MiB import math # 5 29.8 MiB 0.0 MiB while z[-1] < e: # 6 29.8 MiB 0.0 MiB v = math.sin(x) # 7 29.8 MiB 0.0 MiB for i in range(x): # 8 29.8 MiB 0.0 MiB v2 = v + math.sqrt(i) # 9 29.8 MiB 0.0 MiB z.append(v2) # 10 29.8 MiB 0.0 MiB x += 1 # 11 29.8 MiB 0.0 MiB return x # ```