Was macht Python-Code-Qualität und -Design?
Verwendung
- Schreiben oder überprüfen Sie Python, das funktioniert, aber schwer zu verstehen, erweitern oder warten ist.
- Ersetzen Sie eine direkte Übersetzung aus Java oder C++ durch ein sprachgerechtes Konstrukt.
- Wählen Sie eine Abstraktion, bewerten Sie Kopplung oder führen Sie ein Refactoring nach einer neuen Anforderung durch.
- Erkennen Sie die Absicht eines Entwurfsmusters, ohne seinen Namen als architektonische Verpflichtung zu betrachten.
- Dieser Umfang betrifft das Nachdenken über Code; er schreibt weder eine Ausführungsumgebung noch ein Framework vor.
Philosophie
Suchen Sie das kleinste Konstrukt, das das beobachtete Problem löst, nicht das allgemeinste vorstellbare Design. Ein Muster behält seine Absicht, wenn Python bereits seine Struktur bereitstellt; das Entfernen dieser Struktur ist eine Verbesserung, kein Mangel an Raffinesse. Eine gute Abstraktion macht Änderungen weniger kostspielig, ohne Informationen zu löschen, die ihre Clients benötigen. Operative Synthese aus Thinking in Python, von Bruce Eckel; Korpus a8feb860. Respektieren Sie die tatsächlich vom Projekt unterstützte Python-Version: Das aktuelle Ziel des Korpus autorisiert keine implizite Versionserhöhung.
Regeln
Lesbare Syntax und vorhersehbare Namen
- Stellen Sie sicher, dass die Einrückung der tatsächlichen Blockstruktur entspricht; verwenden Sie vier Leerzeichen und mischen Sie keine Tabulatoren und Leerzeichen.
- Beenden Sie Block-Header mit einem Doppelpunkt; vermeiden Sie Semikolons und mehrere Anweisungen, die den Code künstlich komprimieren.
- Verwenden Sie für eine lange Anweisung eine Fortsetzung innerhalb von Klammern, eckigen oder geschweiften Klammern anstelle einer schwer nachvollziehbaren Aufteilung.
- Verwechseln Sie Blöcke nicht mit Gültigkeitsbereichen: Eine Zuweisung in einem
ifoderforkann nach dem Block sichtbar bleiben. - Überprüfen Sie, ob eine Zuweisung auf jedem Pfad ausgeführt wird, bevor der Name verwendet wird; Einrückung allein garantiert keine Bindung.
- Verwenden Sie
snake_casefür Variablen, Funktionen, Methoden und Dateien;PascalCasefür Klassen; Großbuchstaben für Konstanten. - Halten Sie die Ausnahme von Klassen, die wie Funktionen benannt sind, für Objekte vor, die tatsächlich so verwendet werden; machen Sie dies nicht zu einem allgemeinen Stil.
- Leiten Sie keinen Laufzeitschutz aus einem Großbuchstabennamen ab: Er signalisiert dem Leser eine Absicht.
- Behalten Sie die konsistenten Konventionen des Projekts bei und verwenden Sie dessen vorhandenen Linter, anstatt willkürliche lokale Regeln zu erstellen.
Referenzen, Werte und Mutation
- Lesen Sie eine Zuweisung als Bindung eines Namens an ein Objekt; sie kopiert das Objekt nicht.
- Unterscheiden Sie explizit zwischen Neuzuweisung und Mutation: Das Neuzuweisen eines Namens lässt seine vorherigen Aliase unverändert, während das Mutieren des Objekts alle betreffen kann.
- Verwenden Sie
==für Wertgleichheit; reservieren Sieisfür beabsichtigte Identität, insbesondereis Noneund andere Singletons. - Verwenden Sie die Identität von Strings oder kleinen Ganzzahlen nicht als Werttest: Optimierungen zur gemeinsamen Nutzung bieten diesen Vertrag nicht.
- Ein Slice oder eine flache Kopie einer Liste erstellt die äußere Struktur neu, nicht die verschachtelten Elemente; überprüfen Sie, welche Objekte gemeinsam genutzt bleiben.
- Verwenden Sie eine tiefe Kopie nur, wenn auch die enthaltenen Objekte dupliziert werden müssen; eine Zuweisung kann diese Isolation nicht bieten.
- Ersetzen Sie
x += ynicht mechanisch durchx = x + y: Bei einer Liste ändert die erste Form sie an Ort und Stelle, und die zweite erzeugt eine neue. - Verstehen Sie für unveränderliche Zahlen, dass die erweiterte Zuweisung den Namen an das Ergebnis bindet, anstatt die gemeinsam genutzte Zahl zu ändern.
- Bevorzugen Sie unveränderliche Daten, wenn sie sich nicht weiterentwickeln müssen, ohne die Unveränderlichkeit eines Containers mit der seines Inhalts zu verwechseln.
Zahlen und Wahrheitswerte
- Unterscheiden Sie
/, die Division, die für ganzzahlige Operanden eine Gleitkommazahl erzeugt, von//, der Division, die zur negativen Unendlichkeit abrundet. - Testen Sie negative Fälle beim Portieren: Der ganzzahlige Quotient ist nicht die Kürzung gegen Null, und der Rest
%folgt dem Vorzeichen des Divisors. - Gehen Sie nicht davon aus, dass Python-Ganzzahlen eine feste Breite haben; sie überlaufen nicht wie begrenzte Ganzzahlen in anderen Sprachen.
- Trennen Sie die exakte ganzzahlige Arithmetik von der Gleitkommaarithmetik; importieren Sie die Garantien der einen nicht in die andere.
- Berücksichtigen Sie die Rundung auf gerade bei
round()und Formatierung; gehen Sie nicht davon aus, dass die Rundung immer von Null weg erfolgt. - Verwenden Sie
_-Trennzeichen in Literalen, um große Zahlen lesbar zu machen, ohne ihren Wert zu ändern. - Verwenden Sie
+= 1und-= 1zum Erhöhen oder Verringern; Python hat keine++- oder---Operatoren. - Ein Boolean ist auch eine Ganzzahl: Das Summieren von Bedingungen kann wahre Fälle zählen, aber
Truestellt keinen vonintverschiedenen numerischen Typ dar. - Bevorzugen Sie
if items:gegenüber einem Längentest, wenn die Frage einfach ist, ob der Container leer ist. - Denken Sie daran, dass der Wahrheitswert eines Objekts durch
__bool__()oder, falls nicht vorhanden, durch__len__()definiert werden kann. andundorwerten kurz aus und geben einen Operanden zurück, nicht unbedingt einen Boolean; kommentieren Sie ihr Ergebnis nicht allein aufgrund ihrer Namen.- Verwenden Sie
x or defaultnur, wenn alle falschen Werte ersetzt werden sollen; bewahren Sie Null oder leeren Text mit einem explizitenNone-Test. - Unterscheiden Sie für Masken bitweise Operatoren von logischen Operatoren;
~xinvertiert kein Wort fester Breite. - Erwarten Sie nicht, dass
@ein Produkt auf eingebauten Zahlen durchführt: Die Matrixmultiplikation hängt vom Typ ab und unterscheidet sich von der Dekoratorsyntax.
Strings und Interpolation
- Strings sind unveränderlich: Behalten Sie das Ergebnis einer Transformation, wenn es wiederverwendet werden muss.
- Wählen Sie Anführungszeichen, die das Escaping begrenzen; verwenden Sie dreifache Anführungszeichen, wenn Zeilenumbrüche Teil des Textes sind.
- Verwenden Sie einen Rohstring für wörtliche Backslashes, wobei Sie die Unmöglichkeit berücksichtigen, dieses Literal mit einem einzelnen Backslash zu beenden.
- Verwenden Sie integrierte Such-, Teilungs-, Ersetzungs- und Verknüpfungsoperationen anstelle von Zeichenmanipulationsschleifen ohne besonderen Bedarf.
- Verwenden Sie
strip,lstripoderrstripnicht, um ein exaktes Präfix zu entfernen: Ihr Argument bezeichnet Zeichen, kein unteilbares Teilzeichenfolge. - Verwenden Sie
removeprefix()undremovesuffix(), um eine exakte führende oder nachgestellte Teilzeichenfolge zu entfernen, wenn die Zielversion sie unterstützt. +konvertiert eine Zahl nicht implizit in einen String; verwenden Sie eine explizite Konvertierung oder einen f-String.- Bevorzugen Sie einen f-String, um gewöhnlichen Text zu erzeugen; sein Ausdruck wird ausgewertet und sein Ergebnis sofort dargestellt.
- Verwenden Sie die Formatspezifikation für Genauigkeit, Breite und Ausrichtung;
!rzeigt eine für die Diagnose nützliche Darstellung, und=kennzeichnet einen Ausdruck. - Schreiben Sie alte
%- oderstr.format()-Verwendungen nicht um, nur weil sie existieren: Sie bleiben gültig, und eine Migration muss den betreffenden Code verbessern. - Erwägen Sie einen t-String nur, wenn die Zielversion ihn unterstützt und ein Verbraucher Literale von Werten vor der Zusammenstellung unterscheiden muss.
- Ein t-String erzeugt ein strukturiertes Objekt, keinen fertigen String; der Verbraucher muss Interpolationen tatsächlich zitieren, escapen oder ablehnen, entsprechend seiner Domäne.
- Versprechen Sie keine Sicherheit, die allein auf der Interpolationssyntax basiert, oder zweckentfremden Sie eine illustrative Zeichenablehnung als allgemeinen Schutz.
- Ein Vorlagenverbraucher darf nicht von einer strikten Abwechslung zwischen Text und Werten ausgehen: Die Iteration kann leere Literale auslassen.
Abstraktionen, Abhängigkeiten und Kohäsion
- Überprüfen Sie, ob eine Funktion, ein Dictionary, ein Modul oder ein Sprachprotokoll ausreicht, bevor Sie eine neue Klasse hinzufügen.
- Eine Klasse, die nur eine Methode beherbergt, ist allein durch diese Organisation nicht gerechtfertigt.
- Benennen Sie die Änderungsachse basierend auf tatsächlichen Anforderungen; wiederholte Änderungen an derselben Stelle sind nützlichere Beweise als antizipierte Flexibilität.
- Trennen Sie, was sich ändert, von dem, was stabil bleibt; bauen Sie keinen Variationsmechanismus, wenn nichts variiert.
- Behandeln Sie ein Idiom, eine spezifische Lösung und ein allgemeines Muster als mögliche Stufen, nicht als Leiter, die erklommen werden muss.
- Identifizieren Sie für jede Abstraktion die Details, die sie löscht, und bestätigen Sie, dass kein legitimer Client sie wiederherstellen muss.
- Untersuchen Sie auch die hinter der Schnittstelle verborgene Arbeit: Kopieren, Sortieren oder indirekter Zugriff können bei der tatsächlichen Datengröße dominant werden.
- Bewerten Sie die Kopplung danach, was ein Teil weiß: Eltern-Details, konkrete Klasse, Schnittstelle, Aufrufsignatur oder einen einfachen Wert.
- Verwechseln Sie eine typisierte Abhängigkeit nicht mit dem Fehlen einer Abhängigkeit: Ein
Protocolvermeidet die deklarierte Vererbung, aber seine Mitglieder bleiben ein Vertrag. - Bevorzugen Sie eine Funktionssignatur, wenn der Client nur eine Operation benötigt; erstellen Sie keine Multi-Methoden-Schnittstelle ohne Verwendungszweck.
- Ein Muster verschiebt oft eine Abhängigkeit, anstatt sie zu entfernen; lokalisieren Sie den Teil, der immer noch die konkreten Klassen nennen muss.
- Akzeptieren Sie eine konkrete Abhängigkeit, wenn sie bewusst an einem Montagepunkt platziert ist, der kostengünstig zu ändern ist.
- Greifen Sie nicht auf die internen Details indirekt erhaltener Objekte zu; fordern Sie den erforderlichen Dienst über eine explizite Schnittstelle an.
- Gruppieren Sie Elemente, die demselben Thema dienen, in einer Komponente; trennen Sie Ideen, die sich unabhängig voneinander entwickeln können.
- Bevorzugen Sie Komposition, um einen Mitarbeiter zu variieren, ohne seine Implementierungsdetails zu erben; behalten Sie Vererbung bei, wenn Substituierbarkeit wirklich gegeben ist.
- Überprüfen Sie, ob ein Subtyp die Erwartungen der Clients des Basistyps erfüllt, nicht nur die Namen seiner Methoden.
- Faktorisieren Sie Doppelungen, die dieselbe Absicht aus demselben Grund ausdrücken; zufällige Ähnlichkeit ist nicht genug.
- Machen Sie häufige Fälle einfach, seltene Fälle möglich und Schnittstellen konsistent; eine Verhaltensausnahme erlegt einen Merkaufwand auf.
- Entfernen Sie eine Schicht, wenn das Entfernen weder notwendiges Verhalten noch nützliche Isolation eliminiert; wählen Sie zwischen zwei ausreichenden Lösungen nach ihrer Einfachheit.
- Machen Sie Berechnungen nach Möglichkeit rein: Gleiche Eingaben erzeugen dasselbe Ergebnis, ohne beobachtbare Änderungen an der Außenwelt.
Refactoring entlang der beiden Achsen der Erweiterung
- Unterscheiden Sie das Hinzufügen eines Typs vom Hinzufügen einer Operation; ein Design begünstigt oft das eine auf Kosten des anderen.
- Ein
match, das alle Subklassen einer offenen Familie aufzählt, erfordert die Pflege der Liste und kann einen neuen Typ stillschweigend ignorieren. - Wenn die Operation nur nach exakter Klasse gruppiert, vermeidet ein Dictionary, das mit
type(obj)indiziert ist, diese Aufzählung. - Behandeln Sie diesen Ersatz nicht als rein syntaktisch: Ein Subtyp erhält dann seinen eigenen Schlüssel, anders als bei einem Elternklassenmuster.
- Für Gruppen, die beim ersten Vorkommen erstellt werden, stellt
defaultdict(list)eine frische Liste bereit; eine Dictionary-Annotation erzeugt dieses Verhalten nicht. - Belassen Sie eine einheitliche Operation als gewöhnliche Funktion, die die relevanten Attribute liest; Dispatch fügt nichts hinzu, wenn der Algorithmus nicht nach Typ variiert.
- Behalten Sie Methoden in einer kleinen, kontrollierten Hierarchie bei, wenn jeder Typ natürlich seine Antworten trägt und die Operationen wenige bleiben.
- Erwägen Sie
functools.singledispatch, um Operationen hinzuzufügen, die nach Typ variieren, ohne eine externe Hierarchie zu ändern oder neue Operationen auf jede Klasse zu verteilen. - Bei
singledispatchwird die Implementierung basierend auf dem ersten Argument ausgewählt und kann gemäß der MRO vererbt werden; es ist keine exakte Klassentabelle. - Eine Standardantwort muss für nicht registrierte Typen wirklich gültig sein; andernfalls werfen Sie
NotImplementedError, um die Auslassung sichtbar zu machen. - Fügen Sie keine Methoden aus beliebigen Modulen hinzu: Das zwingt Leser, das Verhalten einer Klasse außerhalb ihrer Definition zu suchen.
- Messen Sie ein Refactoring an den geänderten Stellen und den beibehaltenen Daten, nicht nur an der Anzahl der entfernten Zeilen.
Verwendung von Muster-Vokabular ohne erzwungenen Katalog
- Beschreiben Sie zuerst das Problem: Erstellung, Schnittstellenanpassung, Algorithmusauswahl, Benachrichtigung, ein Baum oder Zustandswiederherstellung; der Name kommt danach.
- Reduzieren Sie Strategy und Command auf das Übergeben oder Speichern einer Funktion, wenn das ausreicht; eine Kette kann eine Liste von Funktionen sein, die der Reihe nach ausprobiert werden.
- Erkennen Sie, dass ein gemeinsam genutztes Modul, eine Tabelle von Klassen oder das Iterationsprotokoll bereits die Absicht von Singleton, Factory oder Iterator erfüllen kann.
- Unterscheiden Sie Adapter, der eine Schnittstelle konvertiert, Façade, der ein Subsystem vereinfacht, und Proxy, der den Zugriff auf ein anderes Objekt steuert.
- Unterscheiden Sie State, der das Verhalten je nach Zustand ändert, von einer Zustandsmaschine, die als Reaktion auf Eingaben Übergänge auswählt.
- Unterscheiden Sie das Injizieren eines Mitarbeiters von außen vom Nachschlagen dieses Mitarbeiters in einem Register; beide verlagern die Kontrolle, ohne Abhängigkeiten auf die gleiche Weise offenzulegen.
- Halten Sie architektonische, Persistenz-, Messaging- und Cloud-Namen auf der beschriebenen Absichtsebene: Ihre Anwesenheit in einem Katalog begründet kein Implementierungsrezept.
- Leiten Sie keine Empfehlung für Bereitstellung, Wiederherstellung oder Konsistenz aus einer bloßen Erwähnung von Circuit Breaker, Saga, Repository oder Event Sourcing ab.
Vorgehen
- Identifizieren Sie die Ziel-Python-Version, die bereits verwendeten Konventionen und Prüfungen; notieren Sie das zu erhaltende Verhalten.
- Formulieren Sie eine konkrete Anforderung und die beobachtete Änderungsachse; listen Sie die heute betroffenen Komponenten auf.
- Überprüfen Sie die gemeinsamen Referenzen, Wahrheitswerte, numerischen Operationen und String-Transformationen.
- Schreiben Sie die minimale Lösung, vergleichen Sie dann die vorgeschlagene Abstraktion: gelöschte Details, verborgene Arbeit und verschobene Abhängigkeiten.
- Testen Sie das Hinzufügen eines Typs und das Hinzufügen einer Operation getrennt; notieren Sie die in jeder Variante erforderlichen Änderungen.
- Führen Sie das Refactoring schrittweise durch, während Sie Verhaltenstests und Zählungen der eingehenden und ausgehenden Daten beibehalten.
- Entfernen Sie Schichten, die unnötig geworden sind, und führen Sie dann die Tests und Prüfungen des Projekts auf seinen unterstützten Versionen aus.
Fallstricke
- Ein Programm kann nach dem Verlust neuer Typen eine plausible Gesamtsumme liefern: Überprüfen Sie die Datenerhaltung, nicht nur die Darstellung der Ausgabe.
- Ein Fallback-Zweig oder eine Standardimplementierung kann eine vergessene Erweiterung verstecken; bevorzugen Sie einen expliziten Fehler, wenn keine neutrale Antwort korrekt ist.
- Das Verallgemeinern vor dem Vorliegen mehrerer realer Fälle erzeugt Optionen ohne Nutzennachweis; lehnen Sie das spekulative Framework ab.
- Eine kurze Abstraktion kann hohe Kosten verbergen oder ein unverzichtbares Detail löschen; das Zählen von Zeilen reicht nicht aus, um ihre Qualität zu beurteilen.
- Neuere Syntax und APIs aus dem Korpus sind in einem älteren Projekt nicht unbedingt verfügbar; erhöhen Sie die minimale Python-Version nicht, um sie zu übernehmen.
Verifikation
- Jede neue Klasse oder Schnittstelle schützt ein benanntes Bedürfnis; kein Muster ist nur wegen seines Namens vorhanden.
- Bedingte Pfade binden ihre Variablen vor der Verwendung; Identität, Mutation und Gleichheit werden nicht verwechselt.
- Null, leere Werte,
None, negative Zahlen und verschachtelte Daten wurden getestet, wenn ihre Unterscheidung wichtig ist. - Neue Subklassen verschwinden nicht stillschweigend, und exakte Klassen- oder Vererbungsentscheidungen sind validiert.
- Die Änderungsachse ist dokumentiert; keine Abstraktion fügt eine Variation ohne beobachteten Bedarf hinzu.
- Die minimale unterstützte Version bleibt gültig; keine Syntax oder API, die eine Versionserhöhung erfordert, wird verwendet.
- Verhaltenstests bleiben grün; Ein- und Ausgabedaten werden nach dem Refactoring überprüft.
- Entfernte Schichten hinterlassen weder unnötige Isolation noch fehlendes notwendiges Verhalten.
- Das Vokabular der Muster beschreibt das Problem, nicht die Lösung; kein Muster ist eine architektonische Verpflichtung.
- Die Kopplung ist explizit und an einem Montagepunkt lokalisiert; kein Client greift auf interne Details zu.