Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Lassen Sie den Computer die Arbeit machen

By Joshua FoxNov 30, 20218 min read

Diese Seite ist auch in English, Español, Français, Italiano, 日本語 und Português verfügbar.

Programmier-Einsteigern wird oft geraten, ihren Code so ausführlich wie möglich zu kommentieren. Doch kaum hat man das verinnerlicht, liest man Artikel, die genau das Gegenteil empfehlen. Verwirrend? Dieser Artikel zeigt klar, wann Kommentare sinnvoll sind – und wann nicht.

Das Prinzip ist einfach: Lassen Sie Programmiersprache und Laufzeitumgebung so früh wie möglich dafür sorgen, dass Ihre Dokumentation der Wahrheit entspricht.

Unsere Roboter-Wächter

Ich beginne mit einer undokumentierten Funktion und gehe in der Reihenfolge vor, in der ich solche Dinge üblicherweise umsetze: zuerst die einfachen Grundlagen, dann automatisierte Mechanismen, die sicherstellen, dass die Dokumentation hält, was sie verspricht – und schließlich die schwächeren Formen der Dokumentation für Fälle, in denen sich das nicht automatisiert prüfen lässt.

1\. Keine Dokumentation

Beginnen wir mit dieser einfachen Funktion: p!, die Fakultät – ein Klassiker aus den ersten Programmierübungen. Das Beispiel ist in Python geschrieben, lässt sich aber im Wesentlichen auf andere Programmiersprachen übertragen.

def fact(p):
    ret = 1
    if x == 0:
        return 1
    for i in range(1, x + 1):
        ret *= i
    return ret

Sie könnten in Versuchung geraten, einen Kommentar zu ergänzen wie:

""" This is the factorial """

Doch Kommentare werden oft gar nicht gelesen – und dieser hier ist überflüssig, sobald die Benennung halbwegs sauber ist.

2\. Naming

Ändern Sie die Funktion in:

def factorial(p):...

Die Dokumentation steckt jetzt direkt im ausführbaren Code und liegt nicht mehr nur daneben. Wer Ihre Funktion nutzen möchte, muss diesen Namen lesen. Genau deshalb sollten Funktionen kurz sein und genau einen Zweck erfüllen: So kann der Funktionsname nah am Code stehen und ihn treffend beschreiben.

Auch andere Maßnahmen, die das Verständnis erleichtern – etwa der gezielte Einsatz privater Member –, sind eine solide Form der Dokumentation.

Benennen Sie als Nächstes den Parameter in n um, um zu signalisieren, dass es sich um eine Ganzzahl handelt.

def factorial(n):...

Denn obwohl die Fakultät nur für ganze Zahlen definiert ist, lässt sie sich auf Nicht-Ganzzahlen erweitern. Die Gamma-Funktion ist gewissermaßen eine erweiterte Fakultät.

Ein erfahrener Nutzer könnte sich fragen, ob unsere Implementierung darauf zurückgreift – und das mit gutem Grund: scipy.special.factorial etwa liefert tatsächlich Werte für nicht-ganzzahlige float-Eingaben und nutzt dafür die Gamma-Interpolation der Fakultät. Wir müssen also klarstellen, dass wir hier etwas anderes anbieten.

Die Gamma-Funktion erweitert die Fakultät auf nahezu alle reellen (und komplexen) Zahlen.

3\. Type Hints

Der Parametername ist eine Art Kommentar, und Sie könnten zusätzlich kommentieren, dass es sich um einen int handelt (wie in scipy.special.factorial). Es gibt aber einen besseren Weg, die Ganzzahligkeit kenntlich zu machen: Type Hints.

def factorial3(n: int) -> int:...

Das Python-Tooling warnt Sie vor den meisten Fehlern. Das Prinzip: Lassen Sie den Computer dokumentieren und verlassen Sie sich so wenig wie möglich auf das menschliche Gedächtnis. Wenn Ihre Implementierung beispielsweise ein float 3.0 nicht als int akzeptiert, hält der Type Hint das sofort fest – etwas, das Ihnen sonst leicht durchrutschen könnte.

Kommentare können veraltet oder schlicht falsch sein. scipy.special.factorial (kein Seitenhieb gegen SciPy – eine großartige Bibliothek!) gibt im Kommentar etwa "n : int" an, akzeptiert aber tatsächlich floats und liefert sogar Werte gemäß der Gamma-Funktion zurück.

Typisierung zur Compile-Zeit

Unsere Beispiele sind in Python, doch in einer Sprache mit Compile-Zeit-Typisierung wie Java oder C++ blockiert der Compiler bei Typdeklarationen viele Fehler bereits zur Entwicklungszeit – und garantiert so, dass diese "Dokumentation" so früh wie möglich korrekt ist.

Dennoch eignen sich Assertions besser, um feinkörnige Bedingungen zu prüfen – etwa das Verbot negativer Eingabewerte –, sofern Sie nicht eine Sprache mit besonders fortgeschrittenem Typsystem nutzen.

4\. Assertions, Exceptions oder Pre- und Post-Conditions

Type Hints lassen sich noch toppen. Stellen Sie per Assertion sicher, dass der Wert eine Ganzzahl ist und nicht negativ. So zwinkert Ihnen nicht nur das Entwicklungstool zu – die Laufzeitumgebung stoppt Fehler genau dann, wenn sie auftreten.

def factorial4(n: int) -> int:
    assert isinstance(n, int), f"{n} is not an integer and so unsupported."
    assert n >= 0, f"{n} is negative and so unsupported."
    ...

Es gibt verschiedene Wege, den Fehler auszulösen.

Da es keinen Sinn ergibt, solche Fehler durchzulassen, sollten Assertions aktiviert bleiben. Alternativ können Sie auch einen ValueError werfen. Wenn Sie eine Sprache oder Bibliothek nutzen, die Programming by Contract mit Pre- und Post-Conditions (eine Art strukturierter Assertion) unterstützt, erfüllt das denselben Zweck.

@icontract.require(lambda n:  isinstance(n, int) and n>=0, "n must be a nonnegative integer")
def factorial_precondition(n: int) -> int:
    return  __iterative_factorial(n)

An diesem Punkt prüft der Computer unsere Arbeit tatsächlich. Doch Assertions oder andere Exceptions haben einen zweiten Nutzen: Sie sind eine Form der Dokumentation, die nicht falsch sein kann. Der Leser weiß: Was eine Assertion sagt, stimmt.

Zurück zu unserer Piñata scipy.special.factorial: Im Kommentar heißt es "If n < 0, the return value is 0."

Technisch trifft das auf diese Implementierung zu, ist aber für Nutzer verwirrend, die wissen, dass die Fakultät für negative Zahlen nicht definiert ist. Selbst die Fakultäts-Erweiterung Gamma ist – obwohl auf den reellen und sogar komplexen Zahlen definiert – für negative ganze Zahlen nicht definiert. Viel besser ist es, einen Fehler zu werfen und unmissverständlich klarzumachen, was erlaubt ist und was nicht.

5\. Unit-Tests

Unit-Tests ähneln Assertions: Sie prüfen während der Entwicklung, ob das System tut, was Sie erwarten. Als Dokumentation sind sie schwächer als Assertions, weil sie weiter vom Code entfernt sind – sowohl räumlich (sie liegen in anderen Dateien) als auch zeitlich (sie laufen zu anderen Zeitpunkten). Andere Entwickler werden sie seltener lesen.

Wenn die Entwickler die Tests aber kontinuierlich ausführen – wie es sich gehört –, werden Fehler schnell sichtbar, ohne dass jemand erst Kommentare durchforsten muss.

Unsere einfache iterative Fakultät ist zum Beispiel fehlerhaft! Sie gibt für negative Werte 1 zurück, was sowohl für die Fakultät als auch für die Gamma-Erweiterung definitiv falsch ist. Dieser Test zeigt, dass wir einen Fehler erwarten, leider aber einen Rückgabewert erhalten.

def test_factorial():
    with pytest.raises(Exception):
        factorial(-2)

Sie können sogar Mini-Unit-Tests in Kommentare einbauen – mit doctest, einem eingebauten Feature von Python 3. So liegt der Test so nah am Code wie möglich, wo er als Dokumentation am besten wirkt (auch wenn er die Übersicht beeinträchtigen kann).

def factorial_doctest(n) -> int:
    """
    Run this with python -m doctest -v factorial.py

    >>> factorial_doctest(3)
    >>> factorial_doctest(0)
    >>> factorial_doctest(1.5)
    Traceback (most recent call last):
      ...
    TypeError: 'float' object cannot be interpreted as an integer
    """
    return __iterative_factorial(n)

Integrationstests sind dementsprechend noch einen Schritt weiter vom Code entfernt und eignen sich weniger zur Code-Dokumentation. Sie dokumentieren stattdessen das Verhalten des Gesamtsystems.

6\. Logging

Wenn etwas schiefgehen wird und Sie es im Vorfeld nicht verhindern können, sollten Sie es zumindest protokollieren. Zurück zu SciPy.

Übergeben Sie einen float, erscheint auf Standard Error:

DeprecationWarning: Using factorial() with floats is deprecated

Offenbar wurde ursprünglich eine Gamma-basierte Implementierung verwendet. Als später klar wurde, dass eigentlich nur Ganzzahlen unterstützt werden sollten, wollte man die Eingabe auf int beschränken. Da das wegen abhängiger Anwendungen nicht möglich war, schreibt man die Warnung wenigstens an einen Ort, an dem sie gelesen werden könnte. Entwickler lesen vielleicht keine Dokumentation – aber wenn Probleme auftreten, lesen sie die Logs.

7\. Kommentare

Wir sind nun bei der vorletzten Option angelangt: dem Kommentar. Es gibt einen Platz für ihn – meiner Erfahrung nach aber nur in diesen drei Fällen:

a. Öffentliche APIs sollten mit Inline-Kommentaren dokumentiert werden, aus denen anschließend HTML generiert wird – mit Doxygen für Python, Javadocs für Java usw. Strukturieren Sie die Kommentare mit Tags, nach dem Prinzip: Wo ein Computer die Struktur prüfen kann, soll er es auch tun.

Ein Beispiel für strukturierte Kommentare in Python:

:param n: A nonnegative integer
:return: The product of all integers from 1 up to and including n; or for 0, return 1.

Wo die Funktion jedoch keine öffentliche API ist, sollten Sie diese strukturierten Felder sparsam einsetzen: Solche Kommentare sind dazu da, eine konkrete Botschaft zu etwas Ungewöhnlichem zu vermitteln – nicht dazu, jeden Aspekt der Nutzung minutiös zu dokumentieren.

b. Überraschende Fakten sollten dokumentiert werden. Nutzer wissen vielleicht nicht, dass die Fakultät von null als 1 definiert ist, da sich das nicht recht in die Definition "das Produkt aller Zahlen von 1 bis n" einfügt. Ebenso sollten merkwürdige Workarounds und unschöne Hacks im Code kommentiert werden.

c. Algorithmen sollten benannt werden, da der Computer weder zeigen noch garantieren kann, welcher Algorithmus zum Einsatz kommt – und auch der Leser ihn nicht zwangsläufig auf den ersten Blick erkennt. Die Fakultät kann etwa per Multiplikation berechnet oder durch verschiedene Algorithmen für Gamma approximiert werden. Wer numerisch anspruchsvolle Anwendungen baut, möchte das vielleicht wissen.

def factorial(n: int) -> int:
    """
    This is a simple iterative implementation of factorial.
    The input must be a non-negative integer.
    Note that the factorial of zero is defined as 1.
    """

8\. Externe Dokumentation

Kommentare waren die vorletzte Option; externe Dokumentation ist die allerletzte. RTFM ist ein schöner Slogan, aber kaum jemand liest das Handbuch wirklich – es ist so weit vom Code entfernt, dass es schnell veraltet. Wenn Sie dennoch einen Funktionsüberblick oder Tutorials brauchen, können Sie diese in einem Dokument unterbringen, das getrennt vom Code gepflegt wird.

Aber Vorsicht: Sie müssen diese Dokumente kontinuierlich überprüfen und überarbeiten, wenn sie den ständig wechselnden Code abbilden sollen.

Je näher am Code und je automatisierter, desto besser – aber alles hat seinen Nutzen.

Dokumentation soll die Wahrheit sagen. Ihre Sprach-Tools und die Laufzeitumgebung bieten viele Wege, Verlässlichkeit zu garantieren – nutzen Sie sie. Platzieren Sie die Dokumentation so nah wie möglich am Code und sorgen Sie dafür, dass deren Korrektheit so früh wie möglich im Entwicklungsprozess automatisiert geprüft wird.

Den lauffähigen Code für die verschiedenen Implementierungen finden Sie hier, samt Unit-Tests: https://github.com/doitintl/commenting

Bleiben Sie in Kontakt – folgen Sie uns auf dem DoiT Engineering Blog, dem DoiT LinkedIn-Kanal und dem DoiT Twitter-Kanal. Karrieremöglichkeiten finden Sie unter https://careers.doit-intl.com.