Code- und Dokumentations-Styleguide - Die fehlenden Bits#

Dies ist eine Sammlung von Kodierungs- und Richtlinien für die Dokumentation für SciPy, die nicht explizit in den bestehenden Richtlinien und Standards aufgeführt sind, einschließlich

Einige davon sind trivial und scheinen vielleicht nicht diskussionswürdig zu sein, aber in vielen Fällen wurde das Problem in einer Pull-Request-Überprüfung in den Repositories von SciPy oder NumPy angesprochen. Wenn ein Stilproblem wichtig genug ist, dass ein Prüfer eine Änderung vor dem Mergen verlangt, dann ist es wichtig genug, dokumentiert zu werden – zumindest für Fälle, in denen das Problem mit einer einfachen Regel gelöst werden kann.

Kodierungsstil und Richtlinien#

Beachten Sie, dass Docstrings trotz der Tatsache, dass sie Unicode sind, im Allgemeinen aus ASCII-Zeichen bestehen sollten. Der folgende Codeblock aus der Datei tools/check_unicode.py teilt dem Linter mit, welche zusätzlichen Zeichen erlaubt sind.

18latin1_letters = set(chr(cp) for cp in range(192, 256))
19greek_letters = set('αβγδεζηθικλμνξoπρστυϕχψω' + 'ΓΔΘΛΞΠΣϒΦΨΩ')
20box_drawing_chars = set(chr(cp) for cp in range(0x2500, 0x2580))
21extra_symbols = set('®ő∫≠≥≤±∞²³·→√✅⛔⚠️')
22allowed = latin1_letters | greek_letters | box_drawing_chars | extra_symbols

Erforderliche Schlüsselwortnamen#

Für neue Funktionen oder Methoden mit mehr als ein paar Argumenten sollten alle Parameter nach den ersten paar "offensichtlichen" Parameter *zwingend* mit dem Schlüsselwort übergeben werden. Dies wird durch die Aufnahme von * an der entsprechenden Stelle in der Signatur implementiert.

Zum Beispiel würde eine Funktion foo, die auf einem einzelnen Array operiert, aber mehrere optionale Parameter hat (z. B. method, flag, rtol und atol) wie folgt definiert werden:

def foo(x, *, method='basic', flag=False, rtol=1.5e-8, atol=1-12):
    ...

Um foo aufzurufen, müssen alle Parameter außer x mit einem expliziten Schlüsselwort angegeben werden, z. B. foo(arr, rtol=1e-12, method='better').

Dies zwingt die Aufrufer, explizite Schlüsselwortparameter zu verwenden (was die meisten Benutzer wahrscheinlich sowieso tun würden, auch ohne die Verwendung von *), *und* es bedeutet, dass zusätzliche Parameter jederzeit nach dem * zur Funktion hinzugefügt werden können; neue Parameter müssen nicht nach den vorhandenen Parametern hinzugefügt werden.

Rückgabeobjekte#

Für neue Funktionen oder Methoden, die zwei oder mehr konzeptionell unterschiedliche Elemente zurückgeben, geben Sie die Elemente in einem Objekttyp zurück, der nicht iterierbar ist. Geben Sie insbesondere kein tuple, namedtuple oder einen "bunch", der von scipy._lib._bunch.make_tuple_bunch erzeugt wurde, zurück. Letzteres ist für das Hinzufügen neuer Attribute zu von bestehenden Funktionen zurückgegebenen Iterables reserviert. Verwenden Sie stattdessen eine vorhandene Rückgabeklasse (z. B. OptimizeResult), eine neue, benutzerdefinierte Rückgabeklasse.

Diese Praxis, nicht-iterierbare Objekte zurückzugeben, zwingt die Aufrufer, expliziter auf das Element des zurückgegebenen Objekts zuzugreifen, das sie verwenden möchten, und erleichtert die Erweiterung der Funktion oder Methode auf abwärtskompatible Weise.

Wenn die Rückgabeklasse einfach und nicht öffentlich ist (d. h. aus einem öffentlichen Modul importierbar), kann sie wie folgt dokumentiert werden:

Returns
-------
res : MyResultObject
    An object with attributes:

    attribute1 : ndarray
        Customized description of attribute 1.
    attribute2 : ndarray
        Customized description of attribute 2.

Hier verlinkt "MyResultObject" oben nicht auf externe Dokumentation, da es einfach genug ist, um alle Attribute unmittelbar unter seinem Namen vollständig zu dokumentieren.

Einige Rückgabeklassen sind ausreichend komplex, um eine eigene gerenderte Dokumentation zu verdienen. Dies ist ziemlich üblich, wenn die Rückgabeklasse öffentlich ist, aber Rückgabeklassen sollten nur öffentlich sein, wenn 1) sie von Endbenutzern importiert werden sollen und 2) wenn sie vom Forum genehmigt wurden. Für komplexe, private Rückgabeklassen siehe, wie binomtest BinomTestResult zusammenfasst und auf dessen Dokumentation verlinkt, und beachten Sie, dass BinomTestResult nicht aus stats importiert werden kann.

Abhängig von der Komplexität von "MyResultObject" kann eine normale Klasse oder eine Dataclass verwendet werden. Bei der Verwendung von Dataclasses verwenden Sie nicht dataclasses.make_dataclass, sondern eine ordnungsgemäße Deklaration. Dies ermöglicht die Autovervollständigung, um alle Attribute des Ergebnisobjekts aufzulisten, und verbessert die statische Analyse. Verstecken Sie schließlich private Attribute, falls vorhanden.

@dataclass
class MyResultObject:
    statistic: np.ndarray
    pvalue: np.ndarray
    confidence_interval: ConfidenceInterval
    _rho: np.ndarray = field(repr=False)

Testfunktionen von numpy.testing#

Verwenden Sie in neuem Code nicht assert_almost_equal, assert_approx_equal oder assert_array_almost_equal. Dies steht in den Docstrings dieser Funktionen.

It is recommended to use one of `assert_allclose`,
`assert_array_almost_equal_nulp` or `assert_array_max_ulp`
instead of this function for more consistent floating point
comparisons.

Weitere Informationen zum Schreiben von Unit-Tests finden Sie in den NumPy-Testrichtlinien.

Testen von erwarteten Ausnahmen/Warnungen#

Beim Schreiben eines neuen Tests, der eine Ausnahmeerzeugung oder eine Warnung einer Funktionsaufrufs prüft, ist der bevorzugte Stil die Verwendung von pytest.raises/pytest.warns als Kontextmanager, wobei der Code, der die Ausnahme auslösen soll, im durch den Kontextmanager definierten Codeblock steht. Das Schlüsselwortargument match wird mit genügend Teilen der erwarteten Nachricht versehen, die an die Ausnahme/Warnung angehängt sind, um sie von anderen Ausnahmen/Warnungen desselben Typs zu unterscheiden. Verwenden Sie nicht np.testing.assert_raises oder np.testing.assert_warns, da diese keinen match-Parameter unterstützen.

Zum Beispiel soll die Funktion scipy.stats.zmap einen ValueError auslösen, wenn die Eingabe nan enthält und nan_policy auf "raise" gesetzt ist. Ein Test dafür lautet:

scores = np.array([1, 2, 3])
compare = np.array([-8, -3, 2, 7, 12, np.nan])
with pytest.raises(ValueError, match='input contains nan'):
    stats.zmap(scores, compare, nan_policy='raise')

Das Argument match stellt sicher, dass der Test nicht erfolgreich ist, indem eine ValueError ausgelöst wird, die nicht mit der Eingabe von nan zusammenhängt.