Refactoring bezeichnet das Aufräumen und Neustrukturieren bestehenden Programmcodes. Ein Blick auf die passenden Werkzeuge für Python lohnt sich.
Als Ausgangspunkt unserer Betrachtungen dient bestehender Programmcode, beispielsweise in Form eines Prototyps. Die Entwicklung hin zum ausgereiften Endprodukt spielt sich gewöhnlich in mehreren Stufen ab, wobei der Übergang von einer Projektstufe zur nächsten einen oder mehrere Aufräumvorgänge umfasst. Refactoring bedeutet, die Struktur von Quelltexten manuell oder automatisiert zu verbessern und dabei das beobachtbare Programmverhalten beizubehalten. Nach außen hin leistet das Programm dasselbe, lediglich die interne Struktur ändert sich.
In diesem Schritt nehmen Entwickler den bestehenden Programmcode und die innere Struktur unter die Lupe. Dabei klären sie, was davon tatsächlich (bereits) zu gebrauchen ist, also bleiben soll, was verworfen wird, und was sie neu schreiben. Immerhin stecken im Programmcode schon Arbeit und Ideen, die keineswegs umsonst gewesen sein sollen. Mitunter fällt es schwer, loszulassen und Umsetzungen noch einmal neu zu gestalten. Findet dieser Aufräumprozess allerdings nicht statt, entstehen technische Schulden [1], die sich mit der Zeit zu einem großen Berg anhäufen [2]. Sobald der Ist-Zustand in den gewünschten Soll-Zustand übergeht, sind die technischen Schulden abgetragen. Je länger man als Entwickler das Refactoring aufschiebt, umso aufwendiger und langwieriger gestaltet sich die Arbeit.
Der Zeitpunkt
Refactoring zielt darauf ab, den Programmcode effizienter, einfacher und sauberer zu formulieren. Als erfolgreich gilt es, wenn der Programmcode entweder verständlicher und schlanker ausfällt oder sich die Ausführungszeit verringert. Die höhere Wartbarkeit trägt außerdem zum Verständnis für alle beteiligten Entwickler bei. Später neue Funktionen anzuflanschen, fällt deutlich leichter.
Doch zwischen Theorie und Praxis liegen manchmal Welten – das gilt auch für das Refactoring. In welchen Situationen es Sinn ergibt, verrät die Tabelle “Passende Zeitpunkte für das Refactoring”. Das Ersetzen fußgesteuerter Schleifen durch kopfgesteuerte Varianten gelingt nur, wenn die verwendete Sprache das als Konstrukt mitbringt. Python verfügt zwar nicht über fußgesteuerte Schleifen, lässt sich aber dahingehend programmieren [3].
|
Situation |
Lösungsmöglichkeit |
|---|---|
|
Mehrfach vorkommender identischer oder recht ähnlicher Programmcode |
Ersetzen durch eine oder mehrere Schleifen |
|
Fußgesteuerte Schleifen (Test am Schleifenende) |
Ersetzen durch kopfgesteuerte Schleifen (Test am Schleifenanfang – |
|
Zu große oder unübersichtliche Funktionen oder Methoden |
Aufteilen in mehrere kleinere Funktionen oder Methoden |
|
Funktionen oder Methoden mit zu vielen Parametern |
Aufteilen in mehrere kleinere Funktionen oder Methoden mit weniger Parametern; Festlegen von Default-Werten |
|
Nicht intuitive Namen von Bezeichnern für Variablen, Funktionen, Klassen, Methoden, Attributen oder Modulen |
Vergeben präziserer, anschaulicherer Namen |
|
Unterschiedlicher Codestil (etwa durch mehrere Entwickler) |
Vereinheitlichen des verwendeten Codestils, Einführen und konsequentes Verwenden eines Styleguides |
Wann das Refactoring dann üblicherweise tatsächlich erfolgt, steht auf einem ganz anderen Blatt. Oft erfolgt es nie, besonders, wenn der bereits bestehende Programmcode wie erwartet funktioniert. Möglicherweise sieht auch der Projektplan überhaupt keine Zeit für eine möglicherweise notwendige Umstrukturierung vor. Manchmal versteht schlicht niemand (mehr) den bestehenden Programmcode, gerade, wenn er schlicht undokumentiert ist und sich deshalb keiner an Änderungen herantraut. Häufig kommt das bei Software für Banken vor, die mitunter Jahrzehnte auf dem Buckel hat.
Ein häufiger Anlass für Refactoring sind im Programm auftauchende Fehler oder neue Anforderungen. Manchmal verschlingt die Ausführung des bestehenden Programms zu viele Ressourcen (CPU, Speicher, Zeit) und das Umfeld verändert sich. Gelegentlich gilt es, Sicherheitslücken zu schließen. Oft erlaubt eine neue Version der Programmiersprache zusätzliche, effizientere Konstrukte und Techniken. Hin und wieder ändern sich verwendete Bibliotheken und Frameworks, beispielsweise deren API, Funktionalität oder Verfügbarkeit im Allgemeinen, etwa bei einer Änderung der Nutzungslizenz.
Listing 1 und Listing 2 zeigen das Lesen und Ausgeben des Inhalts der Datei demofile.txt. Listing 1 öffnet und schließt die Datei explizit zum Lesen, Listing 2 öffnet die Datei explizit zum Lesen, schließt sie aber implizit, sobald der With-Block verlassen wird. So kann man das Schließen der Datei nie vergessen. Das Schlüsselwort with für Context Manager existiert ab Python 2.5 und entschärft Programmcode, der vor dem Jahr 2006 entstand [4].
Listing 1
Dateiinhalt ausgeben (Python, Original)
try:
fileHandle = open("demofile.txt", "r")
print(fileHandle.read())
fileHandle.close()
except:
print("Fehler beim Lesen der Datei demofile.txt")
Listing 2
Dateiinhalt ausgeben (Python, redigiert)
try:
with open("demofile.txt","r") as fileHandle:
print(fileHandle.read())
except:
print("Fehler beim Lesen der Datei demofile.txt")
Die Integration
In die Projektentwicklung und den Projektplan lässt sich Refactoring an zwei Stellen integrieren: nach einem Peer Review und während eines Code Reviews. Beide gehören zu den Qualitätssicherungsmaßnahmen. Bevor einer oder gar beide Schritte stattfinden, helfen Function- oder Unit-Tests [5] um die betreffenden Passagen dabei zu prüfen, ob der Code nach den Änderungen noch dasselbe leistet.
Peer Review, auch Kreuzgutachten genannt, meint das Begutachten des Programmcodes durch Peers (“gleichwertige Gutachter”). Häufig übernehmen das mit dem Thema oder der Programmiersprache gleichermaßen oder besser vertraute Kollegen. Üblich sind Peer Reviews beispielsweise vor der Publikation von Artikeln in Fachzeitschriften (das gilt auch für diesen Beitrag, daher die Danksagung an die Reviewer am Ende des Artikels). Während ein Peer Review eher ein singuläres Ereignis ist, kommt ein Code Review mehrmals vor. Diese Codeüberprüfung bezeichnet das systematische Untersuchen von Quellcode auf Schwachstellen hin.
Über die Jahre hinweg hat sich gezeigt, dass dabei die “Politik der kleinen Schritte” am meisten Sinn ergibt. Kleine Verbesserungen gehen zumeist leichter von der Hand als großflächige Änderungen. Sie lassen sich später besser nachvollziehen und gegebenenfalls wieder revidieren. Bei der Codeverwaltung mithilfe eines Versionskontrollsystems wie Git ist diese Vorgehensweise unter dem Spruch “commit early, commit often” bekannt.
Insgesamt halten Peer Reviews und Code Reviews den Programmcode stets ausführbar, also in einem produktiven Zustand (“productive state”). Dazu gehört auch, nach jeder Veränderung alle Tests für den Code zu durchlaufen, um zu sehen, ob alles noch wie erwartet funktioniert. Und zur Erinnerung: Bitte integrieren Sie während des Refactorings keine neuen Features, sondern erst, wenn der Prozess wirklich abgeschlossen ist.
Werkzeuge
Um festzustellen, wie komplex ein Programmcode überhaupt ist, haben sich in den vergangenen Jahrzehnten eine ganze Reihe unterschiedlicher Methoden entwickelt (siehe Tabelle “Softwaremetriken”). Mit der Zeit wurden sie zunehmend anspruchsvoller und decken inzwischen ganz unterschiedliche Aspekte ab.
|
Metrik |
Bedeutung |
|---|---|
|
Lines of Code (LOC) |
Anzahl der Programmzeilen |
|
Cyclomatic Complexity (McCabe) |
Anzahl der verschiedenen Pfade durch den Kontrollflussgraphen des Programmcodes |
|
Abhängigkeitstiefe |
Wie stark hängt eine Funktion oder eine Klasse vom Rest des Programmcodes ab |
|
Kognitive Komplexität |
Verständlichkeit und Verschachtelung |
|
Halstead-Volumen |
Informationsgehalt im Programmcode, Anzahl der Variablen und die Häufigkeit, mit der sie auftauchen |
|
Rework Ratio |
Verhältnis zwischen Zeit für Fehlerkorrekturen und Gesamtentwicklungszeit |
Für die Analyse von Python-Code stehen unter anderem Radon [6], Wily [7], Rope [8] und PyMetrics [9] bereit. Anleitungen zu Wily [10] und Rope [11] finden sich im Python4DataScience-Tutorial. PyMetrics liegt derzeit in einer aktuellen und einer bereits seit 2015 veralteten Version [12] vor, ebenso als gepflegtes Debian-Paket [13]. Außerdem gibt es Alternativen für andere Programmiersprachen, beispielsweise Sloccount [14] für Ada, Awk, die Bash, XML und andere, Cccc [15] und Pmccabe [16] für C/C++ sowie R-cran-cyclocomp [17] für die Programmiersprache R.
Radon
Zunächst werfen wir anhand des Beispielcodes aus Listing 1 bis Listing 3 einen Blick auf Radon. Den Inhalt der ersten beiden Listings haben wir bereits besprochen. Listing 3 enthält in den Zeilen 1 bis 6 die Funktion fakultaet() mit dem Aufrufparameter zahl. Für den Wert von zahl berechnet es dessen Fakultät mithilfe eines rekursiven Aufrufs.
Anschließend definiert Zeile 7 eine Zahlenliste aus vier Werten. In der For-Schleife ab Zeile 8 wird für jeden Wert aus der Zahlenliste die oben definierte Funktion fakultaet() aufgerufen und ein Feedback zum errechneten Ergebnis ausgegeben.
Listing 3
Beispielcode (basis3.py)
def fakultaet(zahl):
if zahl < 0:
return -1
if zahl == 0:
return 1
return zahl * fakultaet(zahl - 1)
zahlenliste = [15, 4, -8, 0]
for wert in zahlenliste:
ergebnis = fakultaet(wert)
if ergebnis == -1:
print("Kann keine Fakultät für Werte < 0 berechnen")
else:
print(f"Fakultät für {wert} ist {ergebnis}")
Nun analysieren wir mithilfe von Radon den Python-Code. Das Werkzeug versteht mehrere Parameter für die Metrik, anhand der Python-Code zu analysieren ist: cc (zyklomatische Komplexität, Cyclomatic Complexity), raw (rohe Metrik), mi (Wartbarkeitsindex, Maintainability Index) und hal (Halstead-Metrik).
Im Aufruf in Listing 4 nutzen wir den Wartbarkeitsindex als Bewertungsmaßstab und rufen Radon daher mit mi als ersten Parameter auf. Anschließend übergeben wir den Namen der zu prüfenden Datei. Der undokumentierte Parameter -s gibt den exakten Wert aus, der der Einstufung in eine Kategorie zugrunde liegt.
Listing 4
Analyse von Python-Code mittels Radon
$ radon mi basis3.py -s
basis3.py - A (60.44)
Der Wartbarkeitsindex kombiniert McCabes zyklomatische Komplexität, Halsteads Volumenmethode und die Menge der Programmzeilen [18]. Daraus errechnet sich ein Wert zwischen 0 und 100, wobei alles unter 25 als schwer zu pflegen und alles über 75 als leicht zu pflegen gilt. Die Tabelle “Stufen des Wartbarkeitsindex nach McCabe” stellt die einzelnen Stufen gegenüber.
|
Wert |
Wartbarkeit |
|---|---|
|
0 bis 25 |
unwartbar |
|
25 bis 50 |
bedenklich |
|
50 bis 75 |
Code bedarf der Verbesserung |
|
75 bis 100 |
erstklassiger Programmcode |
Die Ausgabe von Radon umfasst eine Zeile mit der Bewertungskategorie (A bis F) und dem errechneten Wert. Kategorie A entspricht der höchsten Stufe, F der niedrigsten, schlechtesten. Dementsprechend liegen wir für Listing 2 mit einem Wert von 100.00 am oberen Ende und für Listing 3 mit 60.44 in einem akzeptablen Bereich.
Führen Sie ein Refactoring durch, indem Sie Ihren Code ändern, und prüfen anschließend erneut mit Radon, verrät Ihnen der errechnete Wert, wie es um Codequalität steht. Für Listing 2 bleibt er unverändert bei 100.00 – es lässt sich also nicht weiter verbessern. In Listing 5 haben wir die Funktion durch eine While-Schleife ersetzt. Listing 6 zeigt das Ergebnis der Analyse und erfreut mit einer leichten Verbesserung auf einen Wert von 61.19.
Listing 5
Beispielcode (basis4.py)
zahlenliste = [15, 4, -8, 0]
for wert in zahlenliste:
ergebnis = 1
if wert < 0:
ergebnis = -1
else:
while wert > 1:
ergebnis = ergebnis * wert
wert = wert - 1
if ergebnis == -1:
print("Kann keine Fakultät für Werte < 0 berechnen")
else:
print(f"Fakultät für {wert} ist {ergebnis}")
Listing 6
Analyse von Python-Code mittels Radon
$ radon mi basis[34].py -s
basis3.py - A (60.44)
basis4.py - A (61.19)
Wily
Die Python-Bibliothek Wily geht ein Stück weiter als Radon, indem sie den Verlauf eines Projekts beobachtet und auswertet. Dazu analysiert sie die Veränderungen zwischen den Projektzuständen unter Zuhilfenahme eines Versionskontrollsystems wie Git. Spannend ist, dass die Wily dabei über die Zweige eines VCS hinweg arbeitet. Es erstellt einen Report zu den Veränderungen, die sich als Tabelle sowie in Form einer interaktiven Grafik ausgeben lassen. Bislang ist die Bibliothek nicht für Debian paketiert. Ein gleichnamiges Debian-Paket liegt vor, gehört jedoch zur Textanwendung Wily [19].
Rope
Das Ziel der Python Refactoring Library Rope besteht darin, den Programmcode zu harmonisieren. Sie vereinheitlicht Bezeichner von Variablen, Modulen, Klassen, Methoden und Objekten im gesamten Projekt. Das erweist sich beispielsweise bei Umbenennungen als sehr nützlich und hilft dabei, keine Bezeichner zu übersehen [20]. Rope ändert den Code an den betreffenden Stellen direkt. Mittels Git ermitteln Sie anschließend, was sich in welchen Dateien geändert hat. Die Bibliothek lässt sich sowohl als Kommandozeilenwerkzeug als auch als importiertes Modul in einem Python-Programm einsetzen.
Dasselbe Ergebnis?
Um sicherzustellen, dass der geänderte Programmcode dasselbe Ergebnis wie vor dem Refactoring liefert, stehen mehrere Wege offen, die Sie nach Bedarf miteinander kombinieren können. Hier kommen Funktions- und Softwaretests ins Spiel, ebenso Unit-Tests für größere Codefragmente oder gar komplette Module.
Variante 1 ist die Bereitstellung von Testdaten für die Ein- und Ausgabe mit einem Vorher-Nachher-Vergleich. Bitte beachten Sie: Die Daten müssen alle Testfälle abdecken und die Resultate beider Vergleiche müssen identisch ausfallen. Tauchen Abweichungen auf, steht Detektivarbeit zur Fehlersuche an. Variante 2 besteht darin, die Anweisung assert [21] zu verwenden. Sie überprüft, ob ein bestimmter Zustand vorliegt. Listing 7 veranschaulicht ihren Einsatz in Kombination mit einem Try-Except-Block in einer Python-Shell.
Listing 7
assert benutzen
>>> a = 5 >>> try: ... assert (a > 10) ... except AssertionError: ... print("Wert von a nicht größer 10") ... Wert von a nicht größer 10
In Variante 3 rücken spezielle Python-Module für Tests in den Fokus, zum Beispiel Unittest [22] oder PyTest [23]. In Listing 8 sehen Sie die Funktion fakultaet() aus Listing 3, die wir um drei Testfunktionen ergänzt haben: test_fakultaet_0() (erste bis dritte Zeile), test_fakultaet_negativ() (Zeile 4 bis 6) und test_fakultaet_positiv() (Zeile 7 bis 9). Damit prüfen wir den Aufruf der Fakultät für den Wert 0, einen Wert kleiner als 0 und einen Wert größer als 0. PyTest interpretiert alle Funktionen mit dem Präfix test als Funktionen für Tests, daher die Benennung. Den Aufruf samt Testergebnis zeigt Abbildung 1. Erfreulicherweise bescheinigt die Prüfung eine positive Abdeckung von 100 Prozent.
Listing 8
Validierung über PyTest
def test_fakultaet_0():
ergebnis = fakultaet(0)
assert ergebnis == 1, f"ungültiger Wert für Fakultät von 0: {ergebnis}"
def test_fakultaet_negativ():
ergebnis = fakultaet(-8)
assert ergebnis == -1, f"ungültiger Wert für Fakultät von -8: {ergebnis}"
def test_fakultaet_positiv():
ergebnis = fakultaet(2)
assert ergebnis == 2, f"ungültiger Wert für Fakultät von 2: {ergebnis}"
def fakultaet(zahl):
if zahl < 0:
return -1
if zahl == 0:
return 1
return zahl * fakultaet(zahl - 1)
Automatisierung
Alle bisher genutzten Werkzeuge fallen in die Kategorie Handarbeit und hängen von einem scharfen Blick ab. Besser wäre es, entsprechende Tests automatisch auszulösen, um zu wissen, wann es wieder Zeit für ein Refactoring ist. Dabei greift Ihnen Git unter die Arme: Es bringt Pre-commit-Hooks ins Spiel [24].
Diese Erweiterungen klinken sich in Git ein und prüfen vor Akzeptieren eines Commits, ob vorher festgelegte Bedingungen erfüllt sind. Dabei lassen sich Shell-Kommandos aufrufen – in unserem Fall das Radon samt Parametern, um die Komplexität des Programmcodes zwischen dem Bestand und dem neuen Code zu bestimmen.
In Listing 9 sehen Sie die YAML-Datei mit der Konfiguration für den Pre-commit-Hook. Speichern Sie sie unter dem Namen .pre-commit-config.yaml in Ihrem Projektverzeichnis und rufen Sie daraufhin pre-commit install auf. Damit wird die Datei dem Git-Repository hinzugefügt, ist aktiv und prüft von nun an bei jedem Aufruf von git commit die Komplexität der geänderten Dateien.
Listing 9
Pre-commit-Hook für Radon
repos:
- repo: local
hooks:
- id: radon
name: radon
entry: radon mi -s
verbose: true
language: python
additional_dependencies: [radon]
Fazit
Um das Aufräumen von Programmcode kommen Sie nicht herum, denn selten gelingen Programmierwunder auf Anhieb. Das Refactoring hilft dabei, den eigenen Programmcode mit etwas zeitlichem Abstand zu betrachten und dabei eine Adlerperspektive einzunehmen, um das Werk in der Gesamtheit zu sehen. Das erleichtert, nicht ideale Komponenten im Programmcode zu identifizieren.
Weitere Anregungen zum Thema liefern beispielsweise Martin Fowlers Buch zu Refactoring [25] und das Material inklusive Kurs der (kommerziellen) Webseite Refactoring.guru [26]. Um in Zukunft die Notwendigkeit für ein Refactoring geringer zu halten, lohnt sich auch die Beschäftigung mit den Themen Python Design Patterns [27] und den Python-Internas [28]. Beide schärfen das Verständnis, wie der Python-Interpreter die einzelnen Sprachkonstrukte letztendlich in Prozessoranweisungen übersetzt. Das wirkt sich direkt darauf aus, wie schnell das System Ihren Programmcode ausführt.
Außerdem unterstützt Python das Prinzip der funktionalen Programmierung mit Generatoren, Iteratoren, Lambda-Funktionen und Funktionsausdrücken [29]. Das eröffnet eine neue Ebene im Python-Universum. Funktionaler Programmierung eilt der Ruf voraus, ein Türöffner zu sicherer, zuverlässiger Parallelverarbeitung in Industriequalität zu sein, mit der sich der Durchsatz jeder modernen CPU maximieren lässt. Mit diesen Möglichkeiten befassen wir uns in einem separaten Artikel. (csi)
Danksagung
Der Autor bedankt sich bei Veit Schiele, Kristian Rother, Axel Beckert und Gerold Rupprecht für deren Unterstützung und Anmerkungen bei der Erstellung des Artikels.
Der Autor
Frank Hofmann arbeitet zumeist von unterwegs aus als Entwickler, Trainer und Autor. Bevorzugte Arbeitsorte sind Berlin, Genf und Kapstadt. Er gehört zu den Verfassern des Debian-Paketmanagement-Buchs [30].
Infos
-
Technische Schulden: https://cusy.io/de/blog/technische-schulden
-
“Complexity as Debt”: http://wiki.c2.com/?ComplexityAsDebt
-
“Python Do While – Loop Example”: https://www.freecodecamp.org/news/python-do-while-loop-example/
-
“The Python ‘with’ Statement by Example”: https://preshing.com/20110920/the-python-with-statement-by-example/
-
“A Gentle Introduction to Unit Testing in Python”: https://machinelearningmastery.com/a-gentle-introduction-to-unit-testing-in-python/
-
PyMetrics: https://pypi.org/project/pymetrics/
-
Wily im Python4DataScience-Tutorial: https://www.python4data.science/de/latest/productive/qa/wily.html
-
Rope im Python4DataScience-Tutorial: https://www.python4data.science/de/latest/productive/qa/rope.html
-
PyMetrics (veraltete Projektseite, 2015): https://sourceforge.net/projects/pymetrics/
-
PyMetrics (Debian-Paket): https://packages.debian.org/buster/pymetrics
-
Sloccount (Debian-Paket): https://packages.debian.org/bookworm/sloccount
-
Cccc (Debian-Paket): https://packages.debian.org/bookworm/cccc
-
Pmccabe (Debian-Paket): https://packages.debian.org/bookworm/pmccabe
-
R-cran-cyclocomp (Debian-Paket): https://packages.debian.org/bookworm/r-cran-cyclocomp
-
“Refactoring Python Applications for Simplicity”: https://realpython.com/python-refactoring/
-
Textanwendung Wily (Debian-Paket): https://packages.debian.org/bookworm/wily
-
Refactoring bei Rope: https://rope.readthedocs.io/en/latest/overview.html#refactorings
-
“Pythons assert: Debug and Test Your Code Like a Pro”: https://realpython.com/python-assert-statement/
-
PyTest: https://docs.pytest.org/
-
Pre-commit: https://pre-commit.com/
-
Martin Fowler und Kent Beck, “Refactoring”, Addison-Wesley, ISBN 978-0134757599: https://martinfowler.com/books/refactoring.html
-
Refactoring Guru: https://refactoring.guru/refactoring
-
Brandon Rhodes’ Python Design Patterns: https://python-patterns.guide
-
“Python Design Patterns”: https://www.geeksforgeeks.org/python-design-patterns/
-
“Python Functional Programming HOWTO”: https://docs.python.org/3/howto/functional.html
-
Debian-Paketmanagement-Buch: https://dpmb.org






