Wie man Python-Funktionen noch besser macht

Tatsächlich spiegelt der Titel dieses wunderbaren Artikels von Jeff Knapp, Autor von " Writing Idiomatic Python ", seine Essenz voll und ganz wider. Lesen Sie sorgfältig und zögern Sie nicht zu kommentieren.

Da wir den wichtigen Begriff wirklich nicht in lateinischen Buchstaben im Text belassen wollten, erlaubten wir uns, das Wort "docstring" als "docstring" zu übersetzen, nachdem wir diesen Begriff in mehreren russischsprachigen Quellen entdeckt hatten .

In Python ist wie in den meisten modernen Programmiersprachen eine Funktion die Hauptmethode zum Abstrahieren und Einkapseln. Sie als Entwickler haben wahrscheinlich bereits Hunderte von Funktionen geschrieben. Aber Funktionen zu Funktionen - Zwietracht. Wenn Sie "schlechte" Funktionen schreiben, wirkt sich dies außerdem sofort auf die Lesbarkeit und Unterstützung Ihres Codes aus. Was ist also eine „schlechte“ Funktion und was noch wichtiger ist, wie man sie zu einer „guten“ macht?

Aktualisieren Sie das Thema


Die Mathematik ist voll von Funktionen, es ist jedoch schwierig, sie abzurufen. Kehren wir also zu unserer Lieblingsdisziplin zurück: der Analyse. Sie haben wahrscheinlich Formeln wie f(x) = 2x + 3 . Dies ist eine Funktion namens f , die ein Argument x annimmt und dann zweimal x + 3 "zurückgibt". Obwohl es den Funktionen, die wir in Python gewohnt sind, nicht allzu ähnlich ist, ist es dem folgenden Code völlig ähnlich:

 def f(x): return 2*x + 3 

Funktionen gibt es in der Mathematik schon lange, aber in der Informatik sind sie vollständig transformiert. Diese Kraft ist jedoch nicht umsonst gegeben: Sie müssen verschiedene Fallstricke überwinden. Lassen Sie uns diskutieren, was eine "gute" Funktion sein sollte und welche "Schnickschnack" typisch für Funktionen sind, für die möglicherweise ein Refactoring erforderlich ist.

Geheimnisse guter Funktion


Was unterscheidet eine „gute“ Python-Funktion von einer mittelmäßigen? Sie werden überrascht sein, wie viele Interpretationen das Wort „gut“ zulässt. Als Teil dieses Artikels werde ich die Python-Funktion als "gut" betrachten, wenn sie die meisten Elemente in der folgenden Liste erfüllt (manchmal ist es nicht möglich, alle Elemente für eine bestimmte Funktion zu vervollständigen):

  • Es ist klar benannt
  • Entspricht dem Grundsatz der alleinigen Pflicht
  • Enthält Dock
  • Gibt den Wert zurück
  • Besteht aus nicht mehr als 50 Zeilen
  • Sie ist idempotent und wenn möglich rein

Für viele von Ihnen mögen diese Anforderungen übermäßig hart erscheinen. Ich verspreche jedoch: Wenn Ihre Funktionen diesen Regeln entsprechen, werden sie so schön, dass sie sogar ein Einhorn mit einer Träne durchbohren. Im Folgenden werde ich jedem der Elemente der obigen Liste einen Abschnitt widmen, und dann werde ich die Geschichte vervollständigen, indem ich erzähle, wie sie miteinander harmonieren und dabei helfen, gute Funktionen zu schaffen.

Benennen

Hier ist mein Lieblingszitat zu diesem Thema, das oft fälschlicherweise Donald zugeschrieben wird, aber tatsächlich Phil Carleton gehört :
Die Informatik hat zwei Herausforderungen: die Ungültigmachung des Caches und die Benennung.
Egal wie albern es klingt, das Benennen ist wirklich eine knifflige Sache. Hier ist ein Beispiel für einen "schlechten" Funktionsnamen:

 def get_knn_from_df(df): 

Jetzt kommen mir fast überall schlechte Namen vor, aber dieses Beispiel stammt aus dem Bereich der Datenwissenschaft (genauer gesagt maschinelles Lernen), wo Praktiker normalerweise Code in ein Jupyter-Notizbuch schreiben und dann versuchen, aus diesen Zellen ein verdauliches Programm zusammenzustellen.

Das erste Problem mit dem Namen dieser Funktion besteht darin, dass Abkürzungen verwendet werden. Es ist besser, vollständige englische Wörter zu verwenden, als Abkürzungen und nicht bekannte Abkürzungen . Der einzige Grund, warum ich die Wörter kürzen möchte, besteht darin, keine Zeit damit zu verschwenden, zu viel Text einzugeben. Jeder moderne Editor verfügt jedoch über eine Autovervollständigungsfunktion , sodass Sie den vollständigen Namen der Funktion nur einmal eingeben müssen. Die Abkürzung ist ein Problem, da sie häufig für einen Themenbereich spezifisch ist. Im obigen Code bedeutet knn "K-nächste Nachbarn" und df "DataFrame", eine Datenstruktur, die üblicherweise in der Pandas- Bibliothek verwendet wird. Wenn ein Programmierer, der diese Abkürzungen nicht kennt, den Code liest, versteht er fast nichts im Funktionsnamen.

Der Name dieser Funktion weist zwei weitere kleinere Mängel auf. Erstens ist das Wort "get" überflüssig. Bei den meisten kompetent benannten Funktionen ist sofort klar, dass diese Funktion etwas zurückgibt, was sich speziell im Namen widerspiegelt. Das from_d f-Element wird ebenfalls nicht benötigt. Entweder im Funktionsdock oder (wenn es sich an der Peripherie befindet) in der Typanmerkung wird der Typ des Parameters beschrieben, wenn diese Informationen nicht bereits aus dem Parameternamen ersichtlich sind .

Wie benennen wir diese Funktion um? Nur:

 def k_nearest_neighbors(dataframe): 

Jetzt versteht sogar ein Laie, was in dieser Funktion berechnet wird, und der Parametername (dataframe) lässt keinen Zweifel daran, welches Argument an ihn übergeben werden soll.

Alleinige Verantwortung


Bei der Entwicklung der Idee von Bob Martin werde ich sagen, dass das Prinzip der alleinigen Verantwortung für Funktionen gilt, die nicht weniger als Klassen und Module sind (über die Herr Martin ursprünglich geschrieben hat). Nach diesem Prinzip (in unserem Fall) sollte eine Funktion eine einzige Verantwortung haben. Das heißt, sie muss eine und nur eine Sache tun. Einer der zwingendsten Gründe dafür: Wenn eine Funktion nur eines tut, muss sie im einzigen Fall neu geschrieben werden: Wenn genau dies auf eine neue Weise getan werden muss. Es wird auch klar, wann eine Funktion gelöscht werden kann; Wenn wir bei Änderungen an einer anderen Stelle verstehen, dass die alleinige Pflicht einer Funktion nicht mehr relevant ist, werden wir sie einfach los.

Es ist besser, ein Beispiel zu geben. Hier ist eine Funktion, die mehr als eine „Sache“ macht:

 def calculate_and print_stats(list_of_numbers): sum = sum(list_of_numbers) mean = statistics.mean(list_of_numbers) median = statistics.median(list_of_numbers) mode = statistics.mode(list_of_numbers) print('-----------------Stats-----------------') print('SUM: {}'.format(sum) print('MEAN: {}'.format(mean) print('MEDIAN: {}'.format(median) print('MODE: {}'.format(mode) 

Zwei: Berechnet eine Reihe von Statistiken für eine Liste von Zahlen und zeigt sie in STDOUT . Eine Funktion verstößt gegen eine Regel: Es muss einen bestimmten Grund geben, warum sie möglicherweise geändert werden muss. In diesem Fall gibt es zwei offensichtliche Gründe, warum dies erforderlich ist: Entweder müssen Sie neue oder andere Statistiken berechnen oder Sie müssen das Ausgabeformat ändern. Daher ist es besser, diese Funktion in Form von zwei separaten Funktionen neu zu schreiben: Eine führt Berechnungen durch und gibt ihre Ergebnisse zurück, die andere empfängt diese Ergebnisse und zeigt sie in der Konsole an. Eine Funktion (oder besser gesagt, sie hat zwei Verantwortlichkeiten) mit Innereien gibt das Wort und seinen Namen an .

Diese Trennung vereinfacht auch das Testen der Funktion erheblich und ermöglicht es Ihnen, sie nicht nur in zwei Funktionen innerhalb desselben Moduls aufzuteilen, sondern diese beiden Funktionen gegebenenfalls auch in völlig unterschiedliche Module zu trennen. Dies trägt weiter zu saubereren Tests bei und vereinfacht die Codeunterstützung.

In der Tat sind Funktionen, die genau zwei Dinge ausführen, selten. Häufiger stoßen Sie auf Funktionen, die viel, viel mehr Operationen ausführen. Aus Gründen der Lesbarkeit und Testbarkeit sollten solche "Multi-Station" -Funktionen wiederum in Single-Tasking unterteilt werden, von denen jede einen einzelnen Aspekt der Arbeit enthält.

Docstrings


Es scheint, dass jeder weiß, dass es ein PEP-8- Dokument gibt, das Empfehlungen zum Stil des Python-Codes enthält, aber es gibt viel weniger Leute unter uns, die PEP-257 kennen , in denen dieselben Empfehlungen für Dockstrings gegeben werden. Um den Inhalt von PEP-257 nicht noch einmal zu erzählen, sende ich Sie selbst zu diesem Dokument - lesen Sie in Ihrer Freizeit. Seine Hauptideen sind jedoch wie folgt:

  • Jede Funktion benötigt eine Dokumentzeichenfolge.
  • Es sollte Grammatik und Zeichensetzung beachten; schreibe vollständige Sätze
  • Die Dokumentzeichenfolge beginnt mit einer kurzen (in einem Satz) Beschreibung der Funktionsweise der Funktion.
  • Die Dokumentzeichenfolge ist eher in einem vorschreibenden als in einem beschreibenden Stil formuliert

All diese Punkte sind beim Schreiben von Funktionen leicht zu befolgen. Das Schreiben von Docstrings sollte zur Gewohnheit werden und Sie sollten versuchen, sie zu schreiben, bevor Sie mit dem Code der Funktion selbst fortfahren. Wenn Sie keine eindeutige Dokumentzeichenfolge schreiben können, die die Funktion beschreibt, ist dies ein guter Grund, darüber nachzudenken, warum Sie diese Funktion schreiben.

Rückgabewerte


Funktionen können (und sollten ) als kleine eigenständige Programme interpretiert werden. Sie nehmen einige Eingaben in Form von Parametern vor und geben das Ergebnis zurück. Parameter sind natürlich optional. Rückgabewerte sind jedoch aus Sicht der internen Struktur von Python erforderlich . Wenn Sie sogar versuchen, eine Funktion zu schreiben, die keinen Wert zurückgibt, können Sie dies nicht. Wenn die Funktion nicht einmal Werte zurückgibt, "zwingt" der Python-Interpreter sie, None . Glaubst du nicht? Probieren Sie es selbst aus:

 ❯ python3 Python 3.7.0 (default, Jul 23 2018, 20:22:55) [Clang 9.1.0 (clang-902.0.39.2)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> def add(a, b): ... print(a + b) ... >>> b = add(1, 2) 3 >>> b >>> b is None True 

Wie Sie sehen können, ist der Wert von b im Wesentlichen None . Selbst wenn Sie eine Funktion ohne return-Anweisung schreiben, gibt sie dennoch etwas zurück. Und das sollte es auch. Das ist doch ein kleines Programm, oder? Wie nützlich sind Programme, aus denen keine Schlussfolgerung gezogen werden kann - und daher ist es unmöglich zu beurteilen, ob dieses Programm korrekt ausgeführt wurde? Aber am wichtigsten ist, wie werden Sie ein solches Programm testen ?

Ich habe nicht einmal Angst, Folgendes zu sagen: Jede Funktion sollte einen nützlichen Wert zurückgeben, zumindest aus Gründen der Testbarkeit. Der Code, den ich schreibe, sollte getestet werden (dies wird nicht besprochen). Stellen Sie sich vor, wie ungeschickt das Testen der obigen add Funktion add kann (Hinweis: Sie müssen die Eingabe / Ausgabe umleiten, danach wird bald alles schief gehen). Darüber hinaus können wir durch die Rückgabe eines Werts Methoden verketten und daher Code wie folgt schreiben:

 with open('foo.txt', 'r') as input_file: for line in input_file: if line.strip().lower().endswith('cat'): # ...     -  

String if line.strip().lower().endswith('cat'): weil jede der String-Methoden ( strip() , lower() , endswith() ) als Ergebnis des Aufrufs der Funktion einen String zurückgibt.

Hier sind einige häufige Gründe, die ein Programmierer Ihnen geben kann, wenn er erklärt, warum eine von ihm geschriebene Funktion keinen Wert zurückgibt:
„Es ist nur [eine Art Operation, die sich auf die Eingabe / Ausgabe bezieht, zum Beispiel das Speichern eines Werts in einer Datenbank]. Hier kann ich nichts Nützliches zurückgeben. “
Ich stimme nicht zu. Die Funktion kann True zurückgeben, wenn der Vorgang erfolgreich abgeschlossen wurde.
"Hier ändern wir einen der verfügbaren Parameter und verwenden ihn als Referenzparameter."
Hier sind zwei Punkte. Geben Sie zunächst Ihr Bestes, um dies nicht zu tun. Zweitens ist es bestenfalls überraschend und im schlimmsten Fall einfach gefährlich, eine Funktion mit einer Art Argument zu versehen, nur um herauszufinden, dass sie sich geändert hat. Versuchen Sie stattdessen, wie bei Zeichenfolgenmethoden, eine neue Instanz des Parameters zurückzugeben, die bereits die darauf angewendeten Änderungen widerspiegelt. Auch wenn dies nicht möglich ist, können Sie, da das Erstellen einer Kopie eines Parameters mit übermäßigen Kosten verbunden ist, auf die oben vorgeschlagene Option " True wenn der Vorgang erfolgreich abgeschlossen wurde" zurücksetzen.
„Ich muss mehrere Werte zurückgeben. Es gibt keinen einzigen Wert, dessen Rückgabe in diesem Fall ratsam wäre. “
Dieses Argument ist etwas weit hergeholt, aber ich habe es gehört. Die Antwort ist natürlich genau das, was der Autor tun wollte - wusste aber nicht wie: Verwenden Sie ein Tupel, um mehrere Werte zurückzugeben .

Schließlich ist das stärkste Argument, dass es auf jeden Fall besser ist, einen nützlichen Wert zurückzugeben, dass der Aufrufer diese Werte immer zu Recht ignorieren kann. Kurz gesagt, die Rückgabe eines Werts aus einer Funktion ist mit ziemlicher Sicherheit eine gute Idee, und es ist höchst unwahrscheinlich, dass wir auf diese Weise etwas beschädigen, selbst in den vorhandenen Codebasen.

Funktionslänge


Ich habe mehr als einmal zugegeben, dass ich ziemlich dumm bin. Ich kann ungefähr drei Dinge gleichzeitig in meinem Kopf behalten. Wenn Sie mich die 200-Zeilen-Funktion lesen lassen und fragen, was sie bewirkt, werde ich sie wahrscheinlich mindestens 10 Sekunden lang anstarren. Die Länge einer Funktion wirkt sich direkt auf ihre Lesbarkeit und damit auf ihre Unterstützung aus . Versuchen Sie daher, Ihre Funktionen kurz zu halten. 50 Zeilen - ein Wert, der vollständig von der Decke genommen wurde, aber mir vernünftig erscheint. (Ich hoffe), dass die meisten Funktionen, die Sie gerade schreiben, viel kürzer sind.

Wenn eine Funktion dem Prinzip der alleinigen Verantwortung entspricht, ist sie wahrscheinlich kurz genug. Wenn es unten liest oder idempotent ist (wir werden darüber sprechen) - dann wird es wahrscheinlich auch kurz werden. All diese Ideen werden harmonisch miteinander kombiniert und tragen dazu bei, guten, sauberen Code zu schreiben.

Was tun, wenn Ihre Funktion zu lang ist? REFACTOR! Sie müssen wahrscheinlich die ganze Zeit Refactoring durchführen, auch wenn Sie den Begriff nicht kennen. Refactoring ändert einfach die Struktur eines Programms, ohne dessen Verhalten zu ändern. Daher ist das Extrahieren mehrerer Codezeilen aus einer langen Funktion und deren Umwandlung in eine unabhängige Funktion eine der Arten des Refactorings. Es stellt sich heraus, dass dies auch der häufigste und schnellste Weg ist, um lange Funktionen produktiv zu verkürzen. Da Sie diesen neuen Funktionen entsprechende Namen geben, ist der resultierende Code viel einfacher zu lesen. Ich habe ein ganzes Buch über Refactoring geschrieben (eigentlich mache ich das die ganze Zeit), daher werde ich hier nicht auf Details eingehen. Wenn Sie eine zu lange Funktion haben, sollten Sie sie umgestalten.

Idempotenz und funktionelle Sauberkeit


Der Titel dieses Abschnitts mag ein wenig einschüchternd wirken, aber konzeptionell ist der Abschnitt einfach. Eine idempotente Funktion mit denselben Argumenten gibt immer denselben Wert zurück, unabhängig davon, wie oft sie aufgerufen wird. Das Ergebnis hängt nicht von nicht lokalen Variablen, der Variabilität von Argumenten oder von Daten ab, die aus Eingabe- / Ausgabestreams stammen. Die folgende Funktion add_three(number) ist idempotent:

 def add_three(number): """ ** + 3.""" return number + 3 

Unabhängig davon, wie oft wir add_three(7) aufrufen, add_three(7) die Antwort immer 10. Ein anderer Fall ist eine Funktion, die nicht idempotent ist:

 def add_three(): """ 3 + ,  .""" number = int(input('Enter a number: ')) return number + 3 

Diese offen erfundene Funktion ist nicht idempotent, da der Rückgabewert der Funktion von der Eingabe / Ausgabe abhängt, nämlich von der vom Benutzer eingegebenen Nummer. Bei unterschiedlichen Aufrufen von add_three() Rückgabewerte natürlich unterschiedlich. Wenn wir diese Funktion zweimal aufrufen, kann der Benutzer im ersten Fall 3 und im zweiten - 7 add_three() , und dann add_three() zwei Aufrufe von add_three() 6 bzw. 10 zurück.

Außerhalb der Programmierung gibt es auch Beispiele für Idempotenz - zum Beispiel ist der Aufwärtsknopf des Aufzugs nach diesem Prinzip ausgelegt. Durch erstmaliges Drücken „benachrichtigen“ wir den Aufzug, dass wir hochfahren möchten. Da die Taste idempotent ist, passiert nichts Schlimmes, egal wie oft Sie sie später drücken. Das Ergebnis wird immer das gleiche sein.

Warum Idempotenz so wichtig ist


Testbarkeit und Usability-Unterstützung. Idempotente Funktionen sind einfach zu testen, da sie in jedem Fall garantiert dasselbe Ergebnis liefern, wenn Sie sie mit denselben Argumenten aufrufen. Beim Testen muss überprüft werden, ob die Funktion bei einer Vielzahl von Aufrufen immer den erwarteten Wert zurückgibt. Darüber hinaus sind diese Tests schnell: Die Testgeschwindigkeit ist ein wichtiges Thema, das bei Unit-Tests häufig übersehen wird. Und Refactoring bei der Arbeit mit idempotenten Funktionen ist im Allgemeinen ein einfacher Weg. Es spielt keine Rolle, wie Sie den Code außerhalb der Funktion ändern - das Ergebnis des Aufrufs mit denselben Argumenten ist immer dasselbe.

Was ist eine "reine" Funktion?


In der funktionalen Programmierung gilt eine Funktion als rein, wenn sie erstens idempotent ist und zweitens die beobachteten Nebenwirkungen nicht verursacht. Vergessen Sie nicht: Eine Funktion ist idempotent, wenn sie immer das gleiche Ergebnis mit einem bestimmten Satz von Argumenten zurückgibt. Dies bedeutet jedoch nicht, dass die Funktion keine Auswirkungen auf andere Komponenten haben kann, z. B. nicht lokale Variablen oder Eingabe- / Ausgabestreams. Wenn beispielsweise die idempotente Version der obigen Funktion add_three(number) das Ergebnis an die Konsole ausgibt und erst dann add_three(number) , wird es weiterhin als idempotent betrachtet, da diese Zugriffsoperation beim Zugriff auf den Eingabe- / Ausgabestream keinen Einfluss auf den zurückgegebenen Wert hat von der Funktion. Der Aufruf von print() ist nur ein Nebeneffekt : Die Interaktion mit dem Rest des Programms oder Systems als solchem ​​erfolgt zusammen mit dem Rückgabewert.

Lassen Sie uns unser Beispiel ein wenig mit add_three(number) . Sie können den folgenden Code schreiben, um festzustellen, wie oft add_three(number) aufgerufen wurde:

 add_three_calls = 0 def add_three(number): """ ** + 3.""" global add_three_calls print(f'Returning {number + 3}') add_three_calls += 1 return number + 3 def num_calls(): """,     *add_three*.""" return add_three_calls 

Jetzt führen wir die Ausgabe an die Konsole aus (dies ist ein Nebeneffekt) und ändern die nicht lokale Variable (ein weiterer Nebeneffekt). Da jedoch keiner dieser Werte den von der Funktion zurückgegebenen Wert beeinflusst, ist er ohnehin idempotent.

Reine Funktion hat keine Nebenwirkungen. Es verwendet nicht nur keine "externen Daten" bei der Berechnung des Werts, sondern interagiert nicht mit dem Rest des Programms / Systems, sondern berechnet nur den angegebenen Wert und gibt ihn zurück. Obwohl unsere neue Definition von add_three(number) idempotent bleibt, ist diese Funktion daher nicht mehr rein.

In reinen Funktionen gibt es keine Protokollierungsanweisungen oder print() -Aufrufe. Während der Arbeit greifen sie nicht auf die Datenbank zu und verwenden keine Internetverbindungen. Greifen Sie nicht auf nicht lokale Variablen zu oder ändern Sie sie nicht. Und rufen Sie keine anderen nicht reinen Funktionen auf .

Kurz gesagt, sie haben keine „schreckliche Fernwirkung“, wie es die Worte von Einstein ausdrücken (aber im Kontext der Informatik, nicht der Physik). Sie verändern den Rest des Programms oder Systems in keiner Weise. In der imperativen Programmierung (was Sie tun, wenn Sie Code in Python schreiben) sind solche Funktionen am sichersten. Sie sind bekannt für ihre Testbarkeit und einfache Unterstützung. Da sie idempotent sind, ist das Testen solcher Funktionen garantiert so schnell wie das Ausführen. Die Tests selbst sind ebenfalls einfach: Sie müssen keine Verbindung zur Datenbank herstellen oder externe Ressourcen simulieren, die Erstkonfiguration des Codes vorbereiten und am Ende der Arbeit nichts mehr bereinigen.

Ehrlich gesagt sind Idempotenz und Sauberkeit sehr wünschenswert, aber nicht erforderlich. , , , . , , , , . , , .

Fazit


Das ist alles. , – . . , . – ! . , , , « ». .

Source: https://habr.com/ru/post/de426381/


All Articles