Über die Zeit hinweg entstehen in einem Projekt Codeabschnitte, die sich überleben und obsolet werden. Mit Werkzeugen wie PyLint, Vulture, PyFlakes, Uncalled oder Dead finden Sie nutzlosen Code.
|
Teil 1 |
Code prüfen |
LU 02/2024, S. 84 |
|
Teil**2 |
Code optimieren |
LU**03/2024, S.**78 |
|
Teil 3 |
Dokumentation prüfen |
LU 04/2024 |
Im ersten Teil [1] der Serie haben wir Programmcode auf Korrektheit überprüft und Fehler beseitigt. Nun rückt die Optimierung in den Mittelpunkt. Mithilfe einiger Werkzeuge und Methoden identifizieren Sie ungenutzten Code wie Variablen, Funktionen und Module. Die Tabelle “Werkzeugübersicht” listet die für diese Aufgabe benötigten Tools auf: Unimport [2], PyAnalyze [3], PyChecker [4], Dead [5], Uncalled [6], Coverage [7], PyFlakes [8], Vulture [9] und PyLint [10]. Die Tabelle lässt PyCallgraph3 [11] bewusst aus, da es als einziges der Werkzeuge einen Aufrufgraphen erstellt. Wir besprechen das gesondert am Ende des Artikels.
|
Werkzeug/Funktion |
Unimport |
PyAnalyze |
PyChecker |
Dead |
Uncalled |
Coverage |
PyFlakes |
Vulture |
PyLint |
|---|---|---|---|---|---|---|---|---|---|
|
Unbenutzte Module |
x |
x |
– |
– |
– |
– |
x |
x |
x |
|
Unbenutzte Variablen |
– |
x |
x |
x |
– |
– |
x |
x |
x |
|
Unbenutzte Funktionen/Methoden |
– |
x |
– |
x |
x |
– |
– |
x |
– |
|
Unerreichbarer Code |
– |
x |
– |
– |
– |
x |
– |
x |
– |
|
Funktionsaufruf ohne Deklaration |
– |
x |
x |
– |
– |
– |
– |
– |
x |
|
Modulbenutzung ohne Deklaration |
– |
x |
x |
– |
– |
– |
– |
– |
x |
|
Mehrfachimport eines Moduls |
– |
x |
x |
– |
– |
– |
– |
– |
x |
|
Code direkt bereinigen |
– |
– |
x |
– |
– |
– |
– |
– |
– |
|
Statischer Programmcheck |
x |
– |
– |
x |
x |
– |
x |
x |
x |
|
Dynamischer Programmcheck |
– |
x |
x |
– |
– |
x |
– |
– |
– |
Programmieren verläuft selten vollständig linear, sondern meist evolutionär, sprich: Funktionen kommen hinzu, werden aber nicht unbedingt aufgeräumt, wenn sie niemand mehr braucht. Das bläht nicht nur den Code unnötig auf, sondern hat auch einen potenziell schmerzhaften Haken: Je größer der Codeumfang, desto größer das Risiko für Sicherheitslücken.
Wann das kontrollierte Aufräumen stattfinden sollte, daran scheiden sich zudem die Geister: Die Meinungen reichen von “vor Veröffentlichung” über “vor jedem Commit” bis hin zu “regelmäßig automatisiert”. Auf alle Fälle haben sich Code-Sprints und Bug-Squashing-Parties vor größeren Freigaben in der Praxis bewährt [12].
Beispielskript
Als Grundlage für die Analysen dient der präparierte Python-Code aus Listing 1. Darin finden sich importierte, aber nicht eingesetzte Module sowie ungenutzte Variablen und Funktionen.
Der Code berechnet die gesamte Fläche zwischen Messpunkten aus X- und Y-Koordinaten und der Y-Achse. Der Standardabstand beträgt eine Einheit. Die Gesamtfläche setzt sich aus einzelnen Streifen zusammen, wobei jeder Streifen ein Trapez aus den Punkten (xn,**0), (xn,**yn), (xn+1,**yn+1) und (xn+1,**0) darstellt (Abbildung 1). Im Beispiel läuft x von 0 bis 3, die Gesamtfläche besteht somit aus drei Streifen.

Abbildung 1: Je genauer die Datenpunkte liegen, desto präziser lässt sich der Materialbedarf berechnen.
Das Beispiel mag zunächst akademisch klingen, erweist sich aber als alltäglich: Repräsentiert das Polygon eine Wand, entspricht die Gesamtfläche der für deren Streichen nötigen Farbmenge. Je genauer Sie die Messpunkte setzen, umso schmaler fallen die Streifen aus und dementsprechend präziser bestimmen Sie den Materialbedarf.
Listing 1
Zu testender Python-Code (test.py)
import numpy as np
import fnmatch
def extractDataByAxis(coordinates, axis='x'):
"""return the data that belong to the specified axis
default axis: x
"""
if axis in (0, 'x'):
return coordinates[0]
if axis in (1, 'y'):
return coordinates[1]
def verifyData(coordinates):
"""check for equivalent x,y values"""
return len(coordinates[0]) == len(coordinates[1])
return True
def calculateAreaBetweenGraphs(coordinates, distance=1):
"""calculate area between graph (data points) and x axis
default distance: 1
"""
# initialize total size of the area; set default value to 0.0
totalSize = 0.0
# define distance for x axis
distanceX = 0.5
# extract y coordinates
data = extractDataByAxis(coordinates, 'y')
y = coordinates[1]
# calculate size of area using trapz function from NumPy
totalSize = np.trapz(data, dx=distance)
return totalSize
if __name__ == "__main__":
# define x and y coordinates as tuples
x = (0.0, 1.0, 2.0, 3.0)
y = (2.0, 2.0, 2.5, 3.0)
coordinates = (x,y)
print(calculateAreaBetweenGraphs(coordinates))
Unbenutzte Module finden
Um Programmcode besser zu strukturieren, lagern Sie üblicherweise Funktionsgruppen in Module aus. Später binden Sie sie mithilfe des Kommandos import Modul in das Python-Skript ein. Sobald Sie das erledigt haben, erweitert Python den Namensraum um die darin enthaltenen Klassen, Methoden, Funktionen und Variablen.
Bei umfangreichen Skripten oder Projekten geht manchmal Klarheit darüber verloren, welche Module noch einen tatsächlichen Nutzen besitzen. Um den Überblick zurückzugewinnen, bieten sich PyLint, Unimport, Vulture und PyFlakes an. Die Tools durchkämmen den Python-Code und identifizieren ungenutzte Module.
PyLint prüft den Code zusätzlich stilistisch und erzeugt mitunter etwas längliche Ausgaben. Hier hilft etwas Filtern beispielsweise mittels Grep weiter. Das erste Kommando in Listing 2 reduziert die Ausgabe von PyLint für Listing 1 auf sämtliche das Schlüsselwort unused enthaltende Zeilen. Dabei tauchen zwei Variablen und das Modul fnmatch auf.
Listing 2
Unbenutzte Module aufspüren
$ pylint test.py | grep -i Unused test.py:28:4: W0612: Unused variable 'distanceX' (unused-variable) test.py:32:4: W0612: Unused variable 'y' (unused-variable) test.py:4:0: W0611: Unused import fnmatch (unused-import) $ unimport --check test.py fnmatch at test.py:4
Der zweite Aufruf in Listing 2 zeigt den Aufruf des Spezialisten Unimport für das Skript aus Listing 1. Als Ergebnis bekommen Sie das überflüssige Modul fnmatch zurückgeliefert.
Unimport versteht eine ganze Reihe nützlicher Parameter. Voreingestellt entfernt es automatisch unbenutzte Module. Das entspricht dem Parameter -r (--remove) und erweist sich meist als praktisch, durchaus aber nicht immer. Um vorher zu erfahren, welche Änderungen Unimport im Programmcode vornehmen würde, helfen die drei Parameter -d (--diff), -p (--permission) und das bereits im Listing genutzte --check.
Während -d die Unterschiede zwischen den Änderungen anzeigt, kombiniert -p seinerseits -d und -r. Dadurch sehen Sie zunächst die Unterschiede und müssen zustimmen, bevor die Anwendung die Codezeilen korrigiert. Der Parameter --check liefert eine kompaktere Ausgabe. Sie umfasst nur den Namen des Moduls plus die betreffende Zeile im Python-Skript. Häufig genügt das als Information bereits vollkommen.
Werkzeuge
Während sich Unimport auf Module konzentriert, kümmert sich Uncalled um das Auffinden unbenutzter Funktionen im Programmcode. Dafür greift es auf zwei Techniken zurück: reguläre Ausdrücke (Standardfall, Option --how regex) oder einen abstrakten Syntaxbaum (AST, Option --how ast). Möchten Sie beides nutzen, aktivieren Sie das über die Option --how both. Als Ergebnis kommt entweder eine leere Ausgabe heraus, wenn alles in Ordnung ist, oder eine Liste mit den Namen der nicht aufgerufenen Funktionen. Im ersten Aufruf aus Listing 3 entdeckt Uncalled die Funktion verifyData(), die in Listing 1 nicht mehr zum Einsatz kommt.
Das schlanke Werkzeug Dead steht derzeit nur über den Python Package Index (PyPI) und nicht als Debian-Paket bereit. Es durchforstet den Programmcode auf der Basis des AST nach Definitionen und Referenzen von Variablen und Funktionen. Weder prüft es Python-Module, noch führt es den Programmcode aus. Der zweite Aufruf in Listing 3 liefert eine Ausgabe, die sich aus den Namen gefundener Objekte sowie deren Position im Python-Code zusammensetzt (Zeilennummer).
Listing 3
Uncalled und Dead
$ uncalled test.py test.py: Unused function verifyData $ dead --files test.py distanceX is never read, defined in test.py:28 y is never read, defined in test.py:32 verifyData is never read, defined in test.py:14
Vulture zielt auf größere Codemengen ab und analysiert statisch. Mithilfe des Ast-Moduls [13] erstellt es einen AST, in dem es sich alle im Code definierten Objekte und deren Verwendung merkt. Am Ende gibt Vulture sämtliche ungenutzten Objekte aus. Daneben sucht es nach nicht zu erreichenden Programmzeilen, die niemals ausgeführt werden, besonders nach den Anweisungen return, break, continue und raise. Zusätzlich analysiert Vulture If- und While-Anweisungen daraufhin, ob die Bedingungen darin überhaupt zu erfüllen sind.
Listing 4 illustriert die Auswertung von Vulture zu Listing 1. Das Tool findet das zwar importierte, später aber nicht mehr genutzte Modul fnmatch sowie die nie aufgerufene Funktion verifyData(). Daneben erkennt die Software den unnützen Programmcode nach dem return-Statement. Es überrascht etwas, dass Vulture zwar einerseits feststellt, dass die Variable distanceX nie zum Einsatz kam, aber andererseits die lokale Variable y übersieht. Jede Zeile in der Auswertung enthält zudem eine Angabe, wie sicher sich Vulture bei einem Treffer ist. Für das Beispiel liegen die Werte zwischen 60 und 100 Prozent.
Listing 4
Aufruf von Vulture
$ vulture test.py
test.py:2: unused import 'fnmatch' (90% confidence)
test.py:14: unused function 'verifyData' (60% confidence)
test.py:17: unreachable code after 'return' (100% confidence)
test.py:28: unused variable 'distanceX' (60% confidence)
Listing 4 benennt ausschließlich die ungenutzten Komponenten und gibt keinerlei Auskunft über deren Umfang. Über die Option --sort-by-size weisen Sie Vulture an, die ungenutzten Klassen und Funktionen anhand der Anzahl der Codezeilen zu sortieren und in aufsteigender Reihenfolge auszugeben (Listing 5). Damit lassen sich die zu überarbeitenden Codestellen priorisieren.
Listing 5
Vulture – Sortierung nach LOC
$ vulture --sort-by-size test.py
test.py:2: unused import 'fnmatch' (90% confidence, 1 line)
test.py:17: unreachable code after 'return' (100% confidence, 1 line)
test.py:28: unused variable 'distanceX' (60% confidence, 1 line)
test.py:14: unused function 'verifyData' (60% confidence, 4 lines)
PyFlakes führt ebenfalls eine statische Codeanalyse durch, ohne auf den im Beispiel verwendeten Stil Rücksicht zu nehmen. Dabei geht es dateiweise vor und untersucht den ermittelten AST. Das Testergebnis ähnelt dem von Vulture, fällt allerdings kürzer aus. Dahinter steckt eine etwas zurückhaltende Implementierung mit dem Ziel, keine False Positives zu produzieren. Listing 6 bestätigt, dass PyFlakes das ungenutzte Modul fnmatch und die beiden Variablen distanceX und y identifiziert. Wie bei Vulture beginnt jede Zeile in der Ausgabe mit dem Dateinamen und der Zeilennummer zur genauen Lokalisierung im Code.
Listing 6
Aufruf von PyFlakes
$ pyflakes3 test.py
test.py:2:1: 'fnmatch' imported but unused
test.py:28:5: local variable 'distanceX' is assigned to but never used
test.py:32:5: local variable 'y' is assigned to but never used
Perspektivwechsel
Während Vulture und PyFlakes den Code nur statisch analysieren, nimmt Coverage einen anderen Blickwinkel ein. Es führt das Skript aus und fördert so unausgeführte Codezeilen zutage. Die Entwickler haben sich dabei zum Ziel gesetzt, herauszubekommen, welche Programmzeilen durch passende Tests abgedeckt sind und wo noch Tests fehlen.
Das Tool bringt eine Reihe von Unterkommandos zur Analyse mit. Beispielsweise stößt run das Ausführen des Skripts und die dabei stattfindende Messung an. Coverage legt dazu die Datei .coverage in demselben Verzeichnis an. Sie hält die ermittelten Daten in Form einer SQLite-Datenbank vor und wird bei mehreren Aufrufen ergänzt. Für das Skript aus Listing 1 sieht der Aufruf so aus wie in der ersten Zeile von Listing 7.
Das Unterkommando report berichtet über die Abdeckung bezüglich der Module. Als vorteilhaft erweist sich hier der zusätzliche Parameter -m (--show-missing) für die Angabe zu den nicht ausgeführten Codezeilen. Der zweite Aufruf in Listing 7 knöpft sich das Skript aus Listing 1 vor. Wie sich der Spalte Missing entnehmen lässt, wurden die Zeilen 11 und 18 aus Listing 1 nicht ausgeführt.
Listing 7
Coverage
$ python3-coverage run test.py 7.0 $ python3-coverage report -m test.py Name Stmts Miss Cover Missing --------------------------------- test.py 21 2 90% 11, 18 --------------------------------- TOTAL 21 2 90%
Der Befehl html liefert das Ergebnis als HTML und erzeugt eine entsprechende Datei im Verzeichnis htmlcov/. Über weitere Optionen im Aufruf steuern Sie den Namen der Ausgabedatei sowie deren Inhalt. Die Datengrundlage für die Datei bildet die zuvor mittels run erstellte Datei .coverage. Ihr Inhalt ist identisch zur Ausgabe des Unterkommandos report, nur hübscher mit HTML und CSS verpackt. Mithilfe des Unterbefehls xml erzeugen Sie analog eine coverage.xml als Ausgabedatei. Löschen lassen sich die ermittelten Daten durch erase.
Listing 8 umfasst die beiden aufeinanderfolgenden Aufrufe zum Erstellen des Reports im HTML-Format. Er gibt an, dass Coverage eine Abdeckung von 90 Prozent errechnet hat, dass also 2 der 21 Zeilen nicht ausgeführt werden. Welche konkret, das behält Coverage in dieser Variante jedoch für sich. Hier müssen Sie gegebenenfalls auf die Textdarstellung zurückgreifen.
Listing 8
HTML-Report
$ python3-coverage run test.py 7.0 $ python3-coverage html test.py Wrote HTML report to htmlcov/index.html
Ferner liefen …
PyChecker blickt auf eine mehr als 20-jährige Historie zurück. Allerdings liegt das Projekt derzeit offensichtlich brach, weswegen wir es hier nur der Vollständigkeit halber kurz anreißen. Das Tool konzentriert sich darauf, Fehler zu identifizieren, die sonst ein Compiler entdeckt. Es liefert Warnungen zu falschen Aufrufen von Modulen, Funktionen und Methoden, zu nicht benutzten Identifiern und zu Codekomplexität und Stil. Letzteres betrifft beispielsweise den Mehrfachimport desselben Moduls oder fehlende Docstrings. Vereinfacht ausgedrückt importiert die Anwendung alle im Code benannten Module und prüft danach, ob die Funktionen vorhanden sind und korrekt aufgerufen werden.
PyAnalyze gehört zur selben Kategorie wie Vulture, geht jedoch vor allem in Bezug auf die Parameter zum Prüfen des Python-Codes noch darüber hinaus. Das betrifft zum Beispiel unbekannte Klassenattribute, mehrfach identische Schlüssel in Wörterbüchern, fehlerhafte Angaben bei Format-Strings, fehlende Rückgabewerte und zulässige Datentypen bei Funktionsparametern. Aus Platzgründen verzichten wir hier auf einen Beispielaufruf.
Aufrufgraphen ermitteln
Eine Methode, um Programmcode zu verstehen, liegt im Lesen von Quellcode. Je komplexer die Programme ausfallen, umso mehr Aufwand zieht das nach sich. Hier bieten sich als Ergänzung Aufrufgraphen zur Visualisierung an, die verdeutlichen, welche Bestandteile miteinander in Verbindung stehen [14]. PyCallgraph und dessen Nachfolger Callgraph4Py [15] benutzen Graphviz [16] zum Abbilden der Abhängigkeiten. Abbildung 2 entstand durch das Anwenden von PyCallgraph auf Listing 1, jedoch mit der maximalen Aufruftiefe 5 (Listing 9).
Listing 9
PyCallgraph
$ pycallgraph --max-depth 5 graphviz test.py
Fazit und Ausblick
Mit den vorgestellten Werkzeugen entdecken Sie zuverlässig ungenutzte Module, Variablen, Funktionen, Klassen und Objekte. Wie sich anhand der Ergebnisse zeigt, komplettiert erst der Aufruf mehrerer Tools das Gesamtbild. Mitunter fallen einige Objekte durch das Erkennungsraster – ein zwar offensichtliches, aber bislang ungelöstes Manko.
Nachdem wir den Code validiert, fehlerbereinigt (Teil 1 der Serie) und optimiert haben, folgt in der nächsten Ausgabe im dritten Teil der Artikelserie ein Blick auf den Stil und die Formulierung der Dokumentation. (csi/jlu)
Danksagung
Der Autor bedankt sich bei Veit Schiele für seine Hilfe und Kritik bei der Vorbereitung des Artikels.
Infos
-
Guten Python-Code schreiben (Teil 1): Frank Hofmann, “Schnüffelnase”, LU 02/2024, S. 84, https://www.linux-community.de/50379
-
Unimport: https://pypi.org/project/unimport/
-
PyAnalyze: https://pypi.org/project/pyanalyze/
-
PyChecker: http://pychecker.sourceforge.net/
-
Uncalled: https://pypi.org/project/uncalled/
-
Coverage: https://pypi.org/project/coverage/
-
PyFlakes: https://pypi.org/project/pyflakes/
-
PyLint: https://pylint.org/
-
PyCallgraph3: https://pypi.org/project/pycallgraph3/
-
Debian Bug Squashing Parties: https://wiki.debian.org/BSP
-
Python-Ast-Modul: https://docs.python.org/3/library/ast.html
-
“Generating and using a Callgraph, in Python”: https://cerfacs.fr/coop/pycallgraph
-
Callgraph4Py: https://pypi.org/project/callgraph4py/
-
Graphviz: https://graphviz.org






