Python ist eine Programmiersprache, die sich hervorragend für die Entwicklung eigenständiger Skripte eignet. Um mit einem ähnlichen Skript das gewünschte Ergebnis zu erzielen, müssen Sie mehrere zehn oder hundert Codezeilen schreiben. Und nachdem die Arbeit erledigt ist, können Sie einfach den geschriebenen Code vergessen und mit der Lösung des nächsten Problems fortfahren.
Wenn beispielsweise sechs Monate nach dem Schreiben eines bestimmten „einmaligen“ Skripts jemand den Autor fragt, warum dieses Skript fehlschlägt, ist dem Autor des Skripts dies möglicherweise nicht bekannt. Dies geschieht, weil das Skript keine Dokumentation geschrieben hat, weil Parameter verwendet werden, die im Code fest codiert sind, weil das Skript während der Arbeit nichts protokolliert und weil keine Tests zulässig sind um die Ursache des Problems schnell zu verstehen.

Es sollte beachtet werden, dass es nicht so schwierig ist, ein in Eile geschriebenes Skript in etwas viel Besseres zu verwandeln. Ein solches Skript lässt sich nämlich recht einfach in einen zuverlässigen und verständlichen Code verwandeln, der bequem zu verwenden ist, in einen Code, der sowohl den Autor als auch andere Programmierer einfach unterstützt.
Der Autor des Materials, dessen Übersetzung wir heute veröffentlichen, wird eine solche „Transformation“ am Beispiel des klassischen
Fizz Buzz-Tests demonstrieren. Diese Aufgabe besteht darin, eine Liste mit Zahlen von 1 bis 100 anzuzeigen und einige davon durch spezielle Zeilen zu ersetzen. Wenn die Zahl also ein Vielfaches von 3 ist - stattdessen müssen Sie die Zeichenfolge
Fizz
drucken, wenn die Zahl ein Vielfaches von 5 ist - die Zeichenfolge
Buzz
, und wenn beide Bedingungen erfüllt sind -
FizzBuzz
.
Quellcode
Hier ist der Quellcode für ein Python-Skript, das das Problem löst:
import sys for n in range(int(sys.argv[1]), int(sys.argv[2])): if n % 3 == 0 and n % 5 == 0: print("fizzbuzz") elif n % 3 == 0: print("fizz") elif n % 5 == 0: print("buzz") else: print(n)
Sprechen wir darüber, wie wir es verbessern können.
Die Dokumentation
Ich finde es hilfreich, vor dem Schreiben von Code Dokumentation zu schreiben. Dies vereinfacht die Arbeit und hilft, die Erstellung der Dokumentation nicht auf unbestimmte Zeit zu verzögern. Die Dokumentation für das Skript kann oben platziert werden. Zum Beispiel könnte es so aussehen:
Die erste Zeile enthält eine kurze Beschreibung des Zwecks des Skripts. Die verbleibenden Absätze enthalten zusätzliche Informationen zur Funktionsweise des Skripts.
Befehlszeilenargumente
Die nächste Aufgabe zur Verbesserung des Skripts besteht darin, die im Code fest codierten Werte durch die dokumentierten Werte zu ersetzen, die über Befehlszeilenargumente an das Skript übergeben werden. Dies kann mit dem
argparse- Modul erfolgen. In unserem Beispiel empfehlen wir dem Benutzer, einen Zahlenbereich anzugeben und die Werte für "Fizz" und "Buzz" anzugeben, die beim Überprüfen von Zahlen aus dem angegebenen Bereich verwendet werden.
import argparse import sys class CustomFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): pass def parse_args(args=sys.argv[1:]): """Parse arguments.""" parser = argparse.ArgumentParser( description=sys.modules[__name__].__doc__, formatter_class=CustomFormatter) g = parser.add_argument_group("fizzbuzz settings") g.add_argument("--fizz", metavar="N", default=3, type=int, help="Modulo value for fizz") g.add_argument("--buzz", metavar="N", default=5, type=int, help="Modulo value for buzz") parser.add_argument("start", type=int, help="Start value") parser.add_argument("end", type=int, help="End value") return parser.parse_args(args) options = parse_args() for n in range(options.start, options.end + 1):
Diese Änderungen sind für das Skript von großem Nutzen. Die Parameter sind jetzt ordnungsgemäß dokumentiert. Sie können ihren Zweck mit dem Flag
--help
. Darüber hinaus wird gemäß dem entsprechenden Befehl auch die Dokumentation angezeigt, die wir im vorherigen Abschnitt geschrieben haben:
$ ./fizzbuzz.py --help usage: fizzbuzz.py [-h] [--fizz N] [--buzz N] start end Simple fizzbuzz generator. This script prints out a sequence of numbers from a provided range with the following restrictions: - if the number is divisible by 3, then print out "fizz", - if the number is divisible by 5, then print out "buzz", - if the number is divisible by 3 and 5, then print out "fizzbuzz". positional arguments: start Start value end End value optional arguments: -h, --help show this help message and exit fizzbuzz settings: --fizz N Modulo value for fizz (default: 3) --buzz N Modulo value for buzz (default: 5)
Das
argparse
Modul ist ein sehr leistungsfähiges Werkzeug. Wenn Sie nicht damit vertraut sind, ist es hilfreich, die
Dokumentation zu lesen. Insbesondere gefällt mir seine Fähigkeit,
Unterbefehle und
Argumentgruppen zu definieren.
Protokollierung
Wenn Sie das Skript mit der Möglichkeit ausstatten, während der Ausführung einige Informationen anzuzeigen, stellt sich heraus, dass dies eine angenehme Ergänzung seiner Funktionalität darstellt. Das
Protokollierungsmodul ist für diesen Zweck gut geeignet. Zunächst beschreiben wir ein Objekt, das die Protokollierung implementiert:
import logging import logging.handlers import os import sys logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])
Anschließend können Sie die Details der während der Protokollierung angezeigten Informationen steuern. Der Befehl
logger.debug()
sollte also nur dann etwas ausgeben, wenn das Skript mit dem Schalter
--debug
ausgeführt wird. Wenn das Skript mit dem
--silent
, sollte das Skript nur Ausnahmemeldungen anzeigen.
parse_args()
den folgenden Code hinzu, um diese Funktionen zu implementieren:
Fügen Sie dem Projektcode die folgende Funktion hinzu, um die Protokollierung zu konfigurieren:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
Der Hauptskriptcode ändert sich wie folgt:
if __name__ == "__main__": options = parse_args() setup_logging(options) try: logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) for n in range(options.start, options.end + 1):
Wenn Sie das Skript ohne direkte Beteiligung des Benutzers ausführen
crontab
, z. B. mithilfe von
crontab
, können Sie die Ausgabe an
syslog
:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: if not sys.stderr.isatty(): facility = logging.handlers.SysLogHandler.LOG_DAEMON sh = logging.handlers.SysLogHandler(address='/dev/log', facility=facility) sh.setFormatter(logging.Formatter( "{0}[{1}]: %(message)s".format( logger.name, os.getpid()))) root.addHandler(sh) else: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
In unserem kleinen Skript scheint eine ähnliche Menge an Code erforderlich zu sein, um nur den Befehl
logger.debug()
. In echten Skripten wird dieser Code jedoch nicht mehr so aussehen und der Nutzen davon wird in den Vordergrund rücken, nämlich dass Benutzer mit seiner Hilfe in der Lage sein werden, den Fortschritt der Problemlösung herauszufinden.
$ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz
Tests
Unit-Tests sind ein nützliches Tool, um zu überprüfen, ob sich Anwendungen so verhalten, wie sie sollten. Einheitenskripte werden in Skripten selten verwendet, aber ihre Aufnahme in Skripte verbessert die Codezuverlässigkeit erheblich. Wir wandeln den Code innerhalb der Schleife in eine Funktion um und beschreiben in seiner Dokumentation mehrere interaktive Beispiele für seine Verwendung:
def fizzbuzz(n, fizz, buzz): """Compute fizzbuzz nth item given modulo values for fizz and buzz. >>> fizzbuzz(5, fizz=3, buzz=5) 'buzz' >>> fizzbuzz(3, fizz=3, buzz=5) 'fizz' >>> fizzbuzz(15, fizz=3, buzz=5) 'fizzbuzz' >>> fizzbuzz(4, fizz=3, buzz=5) 4 >>> fizzbuzz(4, fizz=4, buzz=6) 'fizz' """ if n % fizz == 0 and n % buzz == 0: return "fizzbuzz" if n % fizz == 0: return "fizz" if n % buzz == 0: return "buzz" return n
Sie können den korrekten Betrieb der Funktion mit
pytest
:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [100%] ========================== 1 passed in 0.05 seconds ==========================
Damit dies alles funktioniert, benötigen Sie die Erweiterung
.py
, die nach dem Skriptnamen steht. Ich mag es nicht, Erweiterungen zu Skriptnamen hinzuzufügen: Sprache ist nur ein technisches Detail, das dem Benutzer nicht angezeigt werden muss. Es scheint jedoch, dass das Ausstatten eines
pytest
mit einer Erweiterung der einfachste Weg ist, Systeme zum Ausführen von Tests wie
pytest
die im Code enthaltenen Tests finden zu lassen.
Wenn ein Fehler
pytest
eine Meldung an, die den Speicherort des entsprechenden Codes und die Art des Problems angibt:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py -k fizzbuzz.fizzbuzz ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz FAILED [100%] ================================== FAILURES ================================== ________________________ [doctest] fizzbuzz.fizzbuzz _________________________ 100 101 >>> fizzbuzz(5, fizz=3, buzz=5) 102 'buzz' 103 >>> fizzbuzz(3, fizz=3, buzz=5) 104 'fizz' 105 >>> fizzbuzz(15, fizz=3, buzz=5) 106 'fizzbuzz' 107 >>> fizzbuzz(4, fizz=3, buzz=5) 108 4 109 >>> fizzbuzz(4, fizz=4, buzz=6) Expected: fizz Got: 4 /home/bernat/code/perso/python-script/fizzbuzz.py:109: DocTestFailure ========================== 1 failed in 0.02 seconds ==========================
Unit-Tests können auch als regulärer Code geschrieben werden. Stellen Sie sich vor, wir müssen die folgende Funktion testen:
def main(options): """Compute a fizzbuzz set of strings and return them as an array.""" logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) return [str(fizzbuzz(i, options.fizz, options.buzz)) for i in range(options.start, options.end+1)]
Am Ende des Skripts fügen wir die folgenden
pytest
hinzu, die die
pytest
für die Verwendung
parametrisierter Testfunktionen verwenden :
Bitte beachten Sie, dass Tests nicht ausgeführt werden, wenn der
sys.exit()
mit einem Aufruf von
sys.exit()
endet, wenn er normal aufgerufen wird. Aus diesem
pytest
nicht benötigt, um das Skript auszuführen.
Die Testfunktion wird für jede Parametergruppe einmal aufgerufen. Die
args
Entität wird als Eingabe für die Funktion
parse_args()
verwendet. Dank dieses Mechanismus erhalten wir das, was wir zur Übergabe an die
main()
Funktion benötigen. Die
expected
Entität wird mit der
main()
verglichen.
pytest
sagt uns
pytest
, wenn alles wie erwartet funktioniert:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 7 items fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [ 14%] fizzbuzz.py::test_main[0 0-expected0] PASSED [ 28%] fizzbuzz.py::test_main[3 5-expected1] PASSED [ 42%] fizzbuzz.py::test_main[9 12-expected2] PASSED [ 57%] fizzbuzz.py::test_main[14 17-expected3] PASSED [ 71%] fizzbuzz.py::test_main[14 17 --fizz=2-expected4] PASSED [ 85%] fizzbuzz.py::test_main[17 20 --buzz=10-expected5] PASSED [100%] ========================== 7 passed in 0.03 seconds ==========================
Wenn ein Fehler auftritt,
pytest
nützliche Informationen darüber, was passiert ist:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py [...] ================================== FAILURES ================================== __________________________ test_main[0 0-expected0] __________________________ args = '0 0', expected = ['0'] @pytest.mark.parametrize("args, expected", [ ("0 0", ["0"]), ("3 5", ["fizz", "4", "buzz"]), ("9 12", ["fizz", "buzz", "11", "fizz"]), ("14 17", ["14", "fizzbuzz", "16", "17"]), ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]), ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]), ]) def test_main(args, expected): options = parse_args(shlex.split(args)) options.debug = True options.silent = True setup_logging(options) assert main(options) == expected E AssertionError: assert ['fizzbuzz'] == ['0'] E At index 0 diff: 'fizzbuzz' != '0' E Full diff: E - ['fizzbuzz'] E + ['0'] fizzbuzz.py:160: AssertionError ----------------------------- Captured log call ------------------------------ fizzbuzz.py 125 DEBUG compute fizzbuzz from 0 to 0 ===================== 1 failed, 6 passed in 0.05 seconds =====================
Die Ausgabe des
logger.debug()
ist
logger.debug()
in dieser Ausgabe enthalten. Dies ist ein weiterer guter Grund, Protokollierungsmechanismen in Skripten zu verwenden. Wenn Sie mehr über die großartigen Eigenschaften von
pytest
erfahren
pytest
, schauen Sie sich
dieses Material an.
Zusammenfassung
Sie können Python-Skripte zuverlässiger machen, indem Sie die folgenden vier Schritte ausführen:
- Rüsten Sie das Skript mit der Dokumentation aus, die sich oben in der Datei befindet.
- Verwenden Sie das
argparse
Modul, um die Parameter zu dokumentieren, mit denen das Skript aufgerufen werden kann. - Verwenden Sie das
logging
, um Informationen zum logging
anzuzeigen. - Schreiben Sie Unit-Tests.
Hier ist der vollständige Code für das hier beschriebene Beispiel. Sie können es als Vorlage für Ihre eigenen Skripte verwenden.
Interessante Diskussionen rund um dieses Material haben begonnen - Sie finden sie
hier und
hier . Das Publikum hat anscheinend Empfehlungen zur Dokumentation und zu Befehlszeilenargumenten gut aufgenommen, aber was ist mit Protokollierung und Tests, die einigen Lesern als "Schuss aus einer Kanone auf Spatzen" erschienen.
Hier ist das Material, das als Antwort auf diesen Artikel geschrieben wurde.
Liebe Leser! Planen Sie, die in dieser Veröffentlichung enthaltenen Empfehlungen zum Schreiben von Python-Skripten anzuwenden?
