Kapitel 1. Einführung in robustes Python
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
In diesem Buch geht es darum, dass du dein Python besser verwalten kannst. Wenn deine Codebasis wächst, brauchst du einen speziellen Werkzeugkasten mit Tipps, Tricks und Strategien, um wartbaren Code zu erstellen. Dieses Buch führt dich zu weniger Fehlern und zufriedeneren Entwicklern. Du wirst die Art und Weise, wie du deinen Code schreibst, genau unter die Lupe nehmen und die Auswirkungen deiner Entscheidungen kennenlernen. Wenn ich darüber spreche, wie Code geschrieben wird, erinnere ich mich an diese weisen Worte von C.A.R. Hoare:
Es gibt zwei Möglichkeiten, ein Softwaredesign zu entwerfen: Die eine Möglichkeit ist, es so einfach zu machen, dass es keine offensichtlichen Mängel aufweist, und die andere Möglichkeit ist, es so kompliziert zu machen, dass es keine offensichtlichen Mängel aufweist. Die erste Methode ist viel schwieriger.1
In diesem Buch geht es darum, Systeme auf die erste Art zu entwickeln. Es wird schwieriger sein, ja, aber keine Angst. Ich begleite dich auf deinem Weg, dein Python-Spiel so zu verbessern, dass es, wie C.A.R. Hoare oben sagt, offensichtlich keine Mängel in deinem Code gibt. Letztlich geht es in diesem Buch darum, robustes Python zu schreiben.
In diesem Kapitel werden wir uns damit beschäftigen, was Robustheit bedeutet und warum du dich damit beschäftigen solltest. Wir gehen darauf ein, welche Vor- und Nachteile deine Kommunikationsmethode mit sich bringt und wie du deine Absichten am besten darstellen kannst. In "The Zen of Python" heißt es, dass es bei der Entwicklung von Code "einen - und vorzugsweise nur einen - offensichtlichen Weg geben sollte". Du lernst, wie du herausfinden kannst, ob dein Code auf eine offensichtliche Art und Weise geschrieben ist und was du tun kannst, um das zu ändern. Zuerst müssen wir uns mit den Grundlagen beschäftigen. Was ist Robustheit überhaupt?
Robustheit
Jedes Buch braucht mindestens eine Definition aus dem Wörterbuch, also bringe ich das gleich mal vorweg. Merriam-Webster bietet viele Definitionen für Robustheit:
-
Stärke oder eine kräftige Gesundheit haben oder aufweisen
-
Kraft, Stärke oder Festigkeit haben oder zeigen
-
stark geformt oder konstruiert
-
in der Lage sind, unter einer Vielzahl von Bedingungen fehlerfrei zu arbeiten
Das sind fantastische Beschreibungen dessen, was wir anstreben sollten. Wir wollen ein gesundes System, das die Erwartungen über Jahre hinweg erfüllt. Wir wollen, dass unsere Software Stärke zeigt; es sollte offensichtlich sein, dass dieser Code den Test der Zeit bestehen wird. Wir wollen ein starkes System, das auf einem soliden Fundament aufgebaut ist. Und vor allem wollen wir ein System, das fehlerfrei funktioniert; das System sollte nicht anfällig werden, wenn Änderungen vorgenommen werden.
Es ist üblich, sich eine Software wie einen Wolkenkratzer vorzustellen, ein großartiges Bauwerk, das wie ein Bollwerk gegen alle Veränderungen und ein Vorbild für die Unsterblichkeit steht. Die Wahrheit ist leider viel chaotischer. Softwaresysteme entwickeln sich ständig weiter. Fehler werden behoben, Benutzeroberflächen werden optimiert und Funktionen werden hinzugefügt, entfernt und wieder neu hinzugefügt. Frameworks verändern sich, Komponenten veralten und Sicherheitslücken treten auf. Software verändert sich. Die Entwicklung von Software ähnelt eher der Zersiedelung in der Stadtplanung als dem Bau eines statischen Gebäudes. Wie kannst du bei sich ständig ändernden Codebasen deinen Code robust machen? Wie kannst du eine starke Grundlage schaffen, die gegen Fehler resistent ist?
Die Wahrheit ist, dass du Veränderungen akzeptieren musst. Dein Code wird aufgespalten, zusammengefügt und überarbeitet werden. Neue Anwendungsfälle werden große Teile des Codes verändern - und das ist in Ordnung. Nimm es an. Verstehe, dass es nicht ausreicht, dass dein Code einfach geändert werden kann; es könnte besser sein, ihn zu löschen und neu zu schreiben, wenn er veraltet ist. Das schmälert nicht seinen Wert; er wird immer noch ein langes Leben im Rampenlicht haben. Deine Aufgabe ist es, es einfach zu machen, Teile des Systems umzuschreiben. Sobald du die Vergänglichkeit deines Codes akzeptierst, wird dir klar, dass es nicht ausreicht, fehlerfreien Code für die Gegenwart zu schreiben, sondern dass du den zukünftigen Besitzern der Codebasis die Möglichkeit geben musst, deinen Code ohne Bedenken zu ändern. Genau darum geht es in diesem Buch.
Du wirst lernen, starke Systeme zu bauen. Diese Stärke kommt nicht aus der Starrheit, wie sie eine Eisenstange aufweist. Sie kommt vielmehr aus der Flexibilität. Dein Code muss stark sein wie ein hoher Weidenbaum, der sich im Wind wiegt, sich biegt, aber nicht bricht. Deine Software muss Situationen meistern, die du dir nie vorstellen könntest. Deine Codebasis muss in der Lage sein, sich an neue Umstände anzupassen, denn du wirst sie nicht immer selbst pflegen. Die zukünftigen Betreuer müssen wissen, dass sie in einer gesunden Codebasis arbeiten. Deine Codebasis muss ihre Stärke kommunizieren. Du musst Python-Code so schreiben, dass er weniger fehleranfällig ist, auch wenn künftige Betreuer ihn auseinandernehmen und wieder zusammensetzen.
Robusten Code zu schreiben bedeutet, bewusst an die Zukunft zu denken. Du willst, dass deine zukünftigen Betreuer deinen Code sehen und deine Absichten leicht verstehen können, anstatt dich bei nächtlichen Debugging-Sitzungen zu verfluchen. Du musst deine Gedanken, Überlegungen und Vorsichtsmaßnahmen vermitteln. Künftige Entwickler werden deinen Code in neue Formen bringen müssen - und sie werden das tun wollen, ohne sich Sorgen zu machen, dass er bei jeder Änderung wie ein schwankendes Kartenhaus zusammenbricht.
Einfach ausgedrückt: Du willst nicht, dass deine Systeme fehlschlagen, vor allem nicht, wenn etwas Unerwartetes passiert. Tests und Qualitätssicherung spielen dabei eine große Rolle, aber beides schließt die Qualität nicht vollständig aus. Sie sind eher dazu geeignet, Lücken in den Erwartungen aufzudecken und ein Sicherheitsnetz zu bieten. Stattdessen musst du dafür sorgen, dass deine Software den Test der Zeit besteht. Dazu musst du einen sauberen und wartbaren Code schreiben.
Sauberer Code drückt seine Absicht klar und präzise aus, und zwar in dieser Reihenfolge. Wenn du dir eine Codezeile ansiehst und dir sagst: "Ah, das ergibt Sinn", dann ist das ein Zeichen für sauberen Code. Je mehr du durch einen Debugger gehen musst, je mehr du dir eine Menge anderen Code ansehen musst, um herauszufinden, was passiert, je mehr du anhalten und auf den Code starren musst, desto weniger sauber ist er. Sauberer Code wird nicht durch clevere Tricks begünstigt, wenn der Code dadurch für andere Entwickler/innen unlesbar wird. Wie C.A.R. Hoare schon sagte, willst du deinen Code nicht so kompliziert gestalten, dass er bei einer visuellen Inspektion schwer zu verstehen ist.
Wartbarer Code ist Code, der... nun ja, leicht gewartet werden kann. Die Wartung beginnt sofort nach dem ersten Commit und dauert an, bis kein einziger Entwickler mehr auf das Projekt schaut. Entwickler beheben Fehler, fügen Funktionen hinzu, lesen den Code, extrahieren den Code zur Verwendung in anderen Bibliotheken und vieles mehr. Wartungsfreundlicher Code macht diese Aufgaben reibungslos. Software lebt jahrelang, wenn nicht sogar Jahrzehnte. Konzentriere dich heute auf deine Wartbarkeit.
Du willst nicht der Grund dafür sein, dass Systeme fehlschlagen, egal ob du aktiv daran arbeitest oder nicht. Du musst proaktiv daran arbeiten, dass dein System den Test der Zeit besteht. Du brauchst eine Teststrategie als Sicherheitsnetz, aber du musst auch in der Lage sein, Stürze zu vermeiden. Mit all dem im Hinterkopf biete ich dir meine Definition von Robustheit in Bezug auf deine Codebasis an:
Eine robuste Codebasis ist trotz ständiger Änderungen belastbar und fehlerfrei.
Warum ist Robustheit so wichtig?
Es wird viel Energie darauf verwendet, dass eine Software das tut, was sie soll, aber es ist nicht einfach zu wissen, wann man fertig ist. Die Meilensteine der Entwicklung lassen sich nicht leicht vorhersagen. Menschliche Faktoren wie UX, Barrierefreiheit und Dokumentation machen die Sache noch komplizierter. Wenn du dann noch Tests durchführst, um sicherzustellen, dass du alle bekannten und unbekannten Verhaltensweisen abdeckst, kannst du dich auf lange Entwicklungszyklen einstellen.
Der Zweck von Software ist es, einen Wert zu schaffen. Es liegt im Interesse aller Beteiligten, diesen Wert so früh wie möglich voll auszuschöpfen. Angesichts der Ungewissheit, mit der manche Entwicklungszeitpläne behaftet sind, besteht oft ein zusätzlicher Druck, die Erwartungen zu erfüllen. Wir alle haben schon einmal einen unrealistischen Zeitplan oder eine unrealistische Frist erlebt. Leider verlängern viele der Werkzeuge, die Software unglaublich robust machen, nur kurzfristig unseren Entwicklungszyklus.
Es stimmt, dass es ein Spannungsverhältnis zwischen der sofortigen Bereitstellung von Werten und der Robustheit des Codes gibt. Wenn deine Software "gut genug" ist, warum noch mehr Komplexität hinzufügen? Um diese Frage zu beantworten, solltest du bedenken, wie oft die Software überarbeitet werden muss. Die Bereitstellung von Software ist in der Regel keine statische Angelegenheit; es kommt selten vor, dass ein System einen Wert liefert und nie wieder geändert wird. Es liegt in der Natur der Sache, dass sich Software ständig weiterentwickelt. Die Codebasis muss darauf vorbereitet sein, häufig und über lange Zeiträume hinweg Werte zu liefern. Hier kommen robuste Praktiken der Softwareentwicklung ins Spiel. Wenn du nicht in der Lage bist, Funktionen schnell und ohne Qualitätseinbußen bereitzustellen, musst du deine Methoden überdenken, um deinen Code wartbarer zu machen.
Wenn du dein System zu spät oder fehlerhaft auslieferst, entstehen dir Echtzeitkosten. Denke über deine Codebasis nach. Frag dich, was passiert, wenn dein Code in einem Jahr nicht mehr funktioniert, weil jemand deinen Code nicht verstanden hat. Wie viel Wert verlierst du dann? Dein Wert kann in Geld, Zeit oder sogar Leben gemessen werden. Frage dich, was passiert, wenn der Wert nicht rechtzeitig geliefert wird? Was sind die Folgen? Wenn die Antworten auf diese Fragen beängstigend sind, ist das eine gute Nachricht: Die Arbeit, die du machst, ist wertvoll. Das macht aber auch deutlich, warum es so wichtig ist, zukünftige Fehler zu vermeiden.
Mehrere Entwickler arbeiten gleichzeitig an der gleichen Codebasis. Viele Softwareprojekte werden die meisten dieser Entwickler überdauern. Du musst einen Weg finden, um mit den jetzigen und zukünftigen Entwicklern zu kommunizieren, ohne dass du persönlich anwesend sein musst, um es zu erklären. Künftige Entwickler werden auf deinen Entscheidungen aufbauen. Jede falsche Fährte, jedes Kaninchenloch und jedes Yak-Rasierabenteuer2 Abenteuer wird sie ausbremsen, was den Wert beeinträchtigt. Du brauchst Einfühlungsvermögen für diejenigen, die nach dir kommen. Du musst in ihre Fußstapfen treten. Dieses Buch ist dein Einstieg, um über deine Mitarbeiter/innen und Betreuer/innen nachzudenken. Du musst über nachhaltige technische Praktiken nachdenken. Du musst Code schreiben, der Bestand hat. Der erste Schritt, um Code zu schreiben, der Bestand hat, ist, dass du in der Lage bist, über deinen Code zu kommunizieren. Du musst sicherstellen, dass zukünftige Entwickler deine Absichten verstehen.
Was ist deine Intention?
Warum solltest du dich bemühen, sauberen und wartbaren Code zu schreiben? Warum solltest du so viel Wert auf Robustheit legen? Der Kern dieser Antworten liegt in der Kommunikation. Du lieferst keine statischen Systeme. Der Code wird sich ständig ändern. Du musst auch berücksichtigen, dass sich die Betreuer mit der Zeit ändern. Wenn du Code schreibst, ist es dein Ziel, einen Mehrwert zu liefern. Es geht auch darum, deinen Code so zu schreiben, dass andere Entwicklerinnen und Entwickler genauso schnell einen Mehrwert liefern können. Um das zu erreichen, musst du in der Lage sein, deine Überlegungen und Absichten zu kommunizieren, ohne deine zukünftigen Betreuer jemals zu treffen.
Werfen wir einen Blick auf einen Codeblock in einem hypothetischen Altsystem. Ich möchte, dass du schätzt, wie lange du brauchst, um zu verstehen, was dieser Code macht. Es ist in Ordnung, wenn du nicht mit allen Konzepten vertraut bist oder das Gefühl hast, dass der Code verworren ist (das ist er absichtlich!).
# Take a meal recipe and change the number of servings
# by adjusting each ingredient
# A recipe's first element is the number of servings, and the remainder
# of elements is (name, amount, unit), such as ("flour", 1.5, "cup")
def
adjust_recipe
(
recipe
,
servings
):
new_recipe
=
[
servings
]
old_servings
=
recipe
[
0
]
factor
=
servings
/
old_servings
recipe
.
pop
(
0
)
while
recipe
:
ingredient
,
amount
,
unit
=
recipe
.
pop
(
0
)
# please only use numbers that will be easily measurable
new_recipe
.
append
((
ingredient
,
amount
*
factor
,
unit
))
return
new_recipe
Diese Funktion nimmt ein Rezept und passt jede Zutat an, um eine neue Anzahl von Portionen zu erhalten. Dieser Code wirft jedoch viele Fragen auf.
-
Wofür ist die
pop
? -
Was bedeutet
recipe[0]
? Warum sind das die alten Portionen? -
Warum brauche ich einen Kommentar für Zahlen, die leicht messbar sind?
Das ist sicher ein bisschen fragwürdig, Python. Ich kann es dir nicht verübeln, wenn du das Bedürfnis hast, es umzuschreiben. So geschrieben sieht es viel schöner aus:
def
adjust_recipe
(
recipe
,
servings
):
old_servings
=
recipe
.
pop
(
0
)
factor
=
servings
/
old_servings
new_recipe
=
{
ingredient
:
(
amount
*
factor
,
unit
)
for
ingredient
,
amount
,
unit
in
recipe
}
new_recipe
[
"servings"
]
=
servings
return
new_recipe
Diejenigen, die sauberen Code bevorzugen, bevorzugen wahrscheinlich die zweite Version (ich tue das jedenfalls). Keine rohen Schleifen. Variablen ändern sich nicht. Ich gebe ein Wörterbuch statt einer Liste von Tupeln zurück. All diese Änderungen können je nach den Umständen als positiv angesehen werden. Aber vielleicht habe ich gerade drei subtile Fehler eingeführt.
-
Im ursprünglichen Codeschnipsel habe ich das ursprüngliche Rezept gelöscht. Jetzt tue ich das nicht mehr. Selbst wenn es nur ein Bereich des aufrufenden Codes ist, der sich auf dieses Verhalten verlässt, habe ich die Annahmen des aufrufenden Codes gebrochen.
-
Indem ich ein Wörterbuch zurückgegeben habe, habe ich die Möglichkeit entfernt, doppelte Zutaten in einer Liste zu haben. Das kann sich auf Rezepte auswirken, die aus mehreren Teilen bestehen (z. B. ein Hauptgericht und eine Soße), die beide dieselbe Zutat verwenden.
-
Wenn eine der Zutaten "Portionen" genannt wird, habe ich gerade eine Kollision mit der Namensgebung eingeführt.
Ob es sich dabei um Bugs handelt oder nicht, hängt von zwei miteinander verbundenen Dingen ab: der Absicht des Autors und dem aufrufenden Code. Der Autor wollte ein Problem lösen, aber ich weiß nicht, warum er den Code so geschrieben hat, wie er es getan hat. Warum werden Elemente gepoppt? Warum ist "servings" ein Tupel innerhalb der Liste? Warum wird eine Liste verwendet? Vermutlich wusste der ursprüngliche Autor den Grund und teilte ihn seinen Kollegen vor Ort mit. Die Kollegen schrieben den Aufrufcode auf der Grundlage dieser Annahmen, aber im Laufe der Zeit ging diese Absicht verloren. Ohne Kommunikation mit der Zukunft bleiben mir zwei Möglichkeiten, diesen Code beizubehalten:
-
Sieh dir den gesamten aufrufenden Code an und vergewissere dich vor der Implementierung, dass man sich nicht auf dieses Verhalten verlässt. Viel Glück, wenn es sich um eine öffentliche API für eine Bibliothek mit externen Aufrufern handelt. Ich würde eine Menge Zeit damit verbringen, was mich frustrieren würde.
-
Nimm die Änderung vor und warte ab, was die Folgen sind (Kundenbeschwerden, fehlerhafte Tests usw.). Wenn ich Glück habe, wird nichts Schlimmes passieren. Wenn nicht, würde ich viel Zeit damit verbringen, Anwendungsfälle zu korrigieren, was mich frustrieren würde.
Keine der beiden Optionen fühlt sich in einer Wartungsumgebung produktiv an (vor allem, wenn ich den Code ändern muss). Ich will keine Zeit verschwenden; ich will meine aktuelle Aufgabe schnell erledigen und zur nächsten übergehen. Es wird noch schlimmer, wenn ich mir überlege, wie ich den Code aufrufen soll. Denke darüber nach, wie du mit bisher ungesehenem Code interagierst. Du siehst vielleicht andere Beispiele für den Aufruf von Code, kopierst sie, um sie deinem Anwendungsfall anzupassen, und merkst gar nicht, dass du eine bestimmte Zeichenkette namens "servings" als erstes Element deiner Liste übergeben musst.
Das ist die Art von Entscheidungen, bei denen du dich am Kopf kratzen wirst. Wir haben sie alle schon in größeren Codebasen gesehen. Sie werden nicht böswillig geschrieben, sondern entwickeln sich im Laufe der Zeit mit den besten Absichten. Die Funktionen fangen einfach an, aber wenn die Anwendungsfälle wachsen und mehrere Entwickler daran mitarbeiten, wird der Code oft unübersichtlich und verdeckt die ursprüngliche Absicht. Das ist ein sicheres Zeichen dafür, dass die Wartbarkeit leidet. Du musst deine Absicht in deinem Code von Anfang an ausdrücken.
Was wäre also, wenn der ursprüngliche Autor bessere Benennungsmuster und eine bessere Verwendung von Typen verwendet hätte? Wie würde der Code dann aussehen?
def
adjust_recipe
(
recipe
,
servings
):
"""
Take a meal recipe and change the number of servings
:param recipe: a `Recipe` indicating what needs to be adusted
:param servings: the number of servings
:return Recipe: a recipe with serving size and ingredients adjusted
for the new servings
"""
# create a copy of the ingredients
new_ingredients
=
list
(
recipe
.
get_ingredients
())
recipe
.
clear_ingredients
()
for
ingredient
in
new_ingredients
:
ingredient
.
adjust_proportion
(
Fraction
(
servings
,
recipe
.
servings
))
return
Recipe
(
servings
,
new_ingredients
)
Das sieht viel besser aus, ist besser dokumentiert und drückt die ursprüngliche Absicht klar aus. Der ursprüngliche Entwickler hat seine Ideen direkt im Code verschlüsselt. Anhand dieses Schnipsels weißt du, dass Folgendes wahr ist:
-
Ich verwende eine
Recipe
Klasse. Damit kann ich bestimmte Vorgänge abstrahieren. Vermutlich gibt es innerhalb der Klasse selbst eine Invariante, die doppelte Bestandteile zulässt. (Mehr über Klassen und Invarianten erfährst du in Kapitel 10.) So entsteht ein gemeinsames Vokabular, das das Verhalten der Funktion klarer macht. -
Servings sind jetzt ein expliziter Teil einer
Recipe
Klasse und müssen nicht mehr das erste Element der Liste sein, was bisher als Sonderfall behandelt wurde. Das vereinfacht den Aufruf des Codes erheblich und verhindert versehentliche Kollisionen. -
Es ist ganz offensichtlich, dass ich die Zutaten des alten Rezepts löschen möchte. Kein zweideutiger Grund, warum ich eine
.pop(0)
machen muss. -
Die Zutaten sind eine eigene Klasse und behandeln Brüche statt einer expliziten
float
. Es ist für alle Beteiligten klarer, dass ich mit Bruchteilen arbeite, und ich kann ganz einfach Dinge wielimit_denominator()
aufrufen, wenn man Maßeinheiten einschränken will (anstatt sich auf einen Kommentar zu verlassen).
Ich habe Variablen durch Typen ersetzt, z. B. einen Rezepttyp und einen Zutatentyp. Außerdem habe ich Operationen definiert (clear_ingredients
, adjust_proportion
), um meine Absicht zu verdeutlichen. Mit diesen Änderungen habe ich das Verhalten des Codes für zukünftige Leser/innen kristallklar gemacht. Sie müssen nicht mehr mit mir reden, um den Code zu verstehen. Stattdessen verstehen sie, was ich tue, ohne mit mir sprechen zu müssen. Das ist asynchrone Kommunikation in ihrer schönsten Form.
Asynchrone Kommunikation
Es ist seltsam, in einem Python-Buch über asynchrone Kommunikation zu schreiben, ohne async
und await
zu erwähnen. Aber Ich fürchte, ich muss die asynchrone Kommunikation an einem viel komplexeren Ort diskutieren: in der realen Welt.
Asynchrone Kommunikation bedeutet, dass das Produzieren von Informationen und das Konsumieren dieser Informationen unabhängig voneinander sind. Es gibt eine zeitliche Lücke zwischen der Produktion und dem Konsum. Das können ein paar Stunden sein, wenn die Mitarbeiter in verschiedenen Zeitzonen arbeiten. Oder es können Jahre sein, wenn zukünftige Betreuer/innen versuchen, tief in das Innenleben des Codes einzutauchen. Du kannst nicht vorhersagen, wann jemand deine Logik verstehen muss. Es kann sein, dass du zu dem Zeitpunkt, an dem sie die von dir produzierten Informationen konsumieren, gar nicht mehr an dieser Codebasis (oder für dieses Unternehmen) arbeitest.
Im Gegensatz dazu steht die synchrone Kommunikation. Synchrone Kommunikation ist der Austausch von Ideen live (in Echtzeit). Diese Form der direkten Kommunikation ist eine der besten Möglichkeiten, um deine Gedanken auszudrücken, aber leider lässt sie sich nicht skalieren, und du wirst nicht immer da sein, um Fragen zu beantworten.
Um zu beurteilen, wie geeignet die einzelnen Kommunikationsmethoden sind, wenn es darum geht, Absichten zu verstehen, schauen wir uns zwei Achsen an: Nähe und Kosten.
Die Nähe gibt an, wie nah die Kommunizierenden sich zeitlich sein müssen, damit die Kommunikation fruchtbar ist. Manche Kommunikationsmethoden eignen sich besonders gut für die Übertragung von Informationen in Echtzeit. Andere Kommunikationsmethoden eignen sich hervorragend für die Kommunikation Jahre später.
Die Kosten sind das Maß für den Aufwand der Kommunikation. Du musst die Zeit und das Geld, die du für die Kommunikation aufbringst, gegen den Wert abwägen, der dir geboten wird. Deine zukünftigen Konsumenten müssen dann die Kosten für den Konsum der Informationen gegen den Wert abwägen, den sie zu liefern versuchen. Code zu schreiben und keine anderen Kommunikationskanäle bereitzustellen, ist deine Ausgangsbasis; du musst dies tun, um einen Wert zu schaffen. Um die Kosten für zusätzliche Kommunikationskanäle zu bewerten, berücksichtige ich Folgendes:
- Entdeckbarkeit
-
Wie einfach war es, diese Informationen außerhalb eines normalen Arbeitsablaufs zu finden? Wie flüchtig ist das Wissen? Ist es einfach, nach Informationen zu suchen?
- Wartungskosten
-
Wie genau sind die Informationen? Wie oft müssen sie aktualisiert werden? Was geht schief, wenn diese Informationen veraltet sind?
- Produktionskosten
-
Wie viel Zeit und Geld wurde in die Erstellung der Mitteilung investiert?
In Abbildung 1-1 sind die Kosten und die erforderliche Nähe einiger gängiger Kommunikationsmethoden dargestellt, die auf meinen eigenen Erfahrungen basieren.
Es gibt vier Quadranten, aus denen das Kosten-Nähe-Diagramm besteht.
- Geringe Kosten, große Nähe erforderlich
-
Sie sind billig zu produzieren und zu konsumieren, aber sie sind nicht zeitlich skalierbar. Direkte Kommunikation und Instant Messaging sind gute Beispiele für diese Methoden. Betrachte sie als Momentaufnahmen; sie sind nur dann wertvoll, wenn die Nutzer/innen aktiv zuhören. Verlasse dich nicht auf diese Methoden, um in die Zukunft zu kommunizieren.
- Hohe Kosten, große Nähe erforderlich
-
Das sind kostspielige Veranstaltungen, die oft nur einmal stattfinden (z. B. Meetings oder Konferenzen). Diese Veranstaltungen sollten zum Zeitpunkt der Kommunikation einen hohen Wert haben, weil sie in der Zukunft keinen großen Nutzen bringen. Wie oft hast du schon an einem Treffen teilgenommen, das sich wie Zeitverschwendung anfühlte? Du spürst den direkten Verlust an Wert. Besprechungen verursachen für jeden Teilnehmer multiplizierende Kosten (aufgewendete Zeit, Veranstaltungsraum, Logistik usw.). Codeüberprüfungen werden selten angeschaut, wenn sie einmal abgeschlossen sind.
- Hohe Kosten, geringe Nähe erforderlich
-
Sie sind zwar kostspielig, aber diese Kosten können sich im Laufe der Zeit durch den Wert, den sie liefern, amortisieren, da sie nur eine geringe Nähe benötigen. E-Mails und agile Boards enthalten eine Fülle von Informationen, sind aber für andere nicht auffindbar. Sie eignen sich gut für größere Konzepte, die nicht häufig aktualisiert werden müssen. Es wird zu einem Albtraum, wenn du versuchst, das ganze Rauschen zu durchforsten, nur um die gesuchte Information zu finden. Videoaufzeichnungen und Entwurfsdokumentationen eignen sich gut, um Momentaufnahmen zu verstehen, aber es ist kostspielig, sie auf dem neuesten Stand zu halten. Verlasse dich nicht auf diese Kommunikationsmethoden, um die täglichen Entscheidungen zu verstehen.
- Geringe Kosten, geringe Nähe erforderlich
-
Sie sind billig zu erstellen und leicht zu konsumieren. Codekommentare, Versionskontrolle und Projekt-READMEs fallen alle in diese Kategorie, da sie neben dem Quellcode stehen, den wir schreiben. Die Nutzer können diese Informationen noch Jahre nach ihrer Erstellung einsehen. Alles, was einem Entwickler während seines täglichen Workflows begegnet, ist von Natur aus auffindbar. Diese Kommunikationsmethoden sind der erste Ort, an dem jemand nach dem Quellcode sucht. Dein Code ist jedoch eines deiner besten Dokumentationswerkzeuge, denn er ist die lebendige Aufzeichnung und die einzige Quelle der Wahrheit für dein System.
Diskussionsthema
Das Diagramm in Abbildung 1-1 wurde auf der Grundlage allgemeiner Anwendungsfälle erstellt. Denke über die Kommunikationswege nach, die du und dein Unternehmen nutzen. Wo würdest du sie im Diagramm einzeichnen? Wie einfach ist es, genaue Informationen zu erhalten? Wie aufwendig ist es, Informationen zu produzieren? Deine Antworten auf diese Fragen können zu einem etwas anderen Diagramm führen, aber die einzige Quelle der Wahrheit ist die ausführbare Software, die du lieferst.
Kostengünstige Kommunikationsmethoden mit geringer räumlicher Nähe sind die besten Mittel, um mit der Zukunft zu kommunizieren. Du solltest dich bemühen, die Kosten für die Produktion und den Verbrauch der Kommunikation zu minimieren. Du musst sowieso Software schreiben, um Werte zu schaffen, also ist die kostengünstigste Option, deinen Code zu deinem primären Kommunikationsmittel zu machen. Deine Codebasis ist die bestmögliche Option, um deine Entscheidungen, Meinungen und Umgehungsmöglichkeiten klar zum Ausdruck zu bringen.
Damit diese Behauptung stimmt, muss der Code aber auch billig zu konsumieren sein. Deine Absicht muss in deinem Code klar zum Ausdruck kommen. Dein Ziel ist es, die Zeit zu minimieren, die ein Leser deines Codes braucht, um ihn zu verstehen. Im Idealfall muss ein Leser nicht deine Implementierung lesen, sondern nur deine Funktionssignatur. Durch die Verwendung von guten Typen, Kommentaren und Variablennamen sollte kristallklar sein, was dein Code tut.
Beispiele für Intent in Python
Nachdem ich nun erklärt habe, was Absicht ist und worauf es ankommt, wollen wir uns die Beispiele aus der Sicht von Python ansehen. Wie kannst du sicherstellen, dass du deine Absichten richtig ausdrückst? Ich werde mir zwei verschiedene Beispiele dafür ansehen, wie eine Entscheidung die Absicht beeinflusst: Sammlungen und Iteration.
Sammlungen
Wenn du eine Sammlung auswählst, übermittelst du damit bestimmte Informationen. Du musst die richtige Sammlung für die anstehende Aufgabe auswählen. Andernfalls werden die Betreuer aus deinem Code die falsche Absicht ableiten.
Der folgende Code nimmt eine Liste von Kochbüchern und stellt eine Zuordnung zwischen Autoren und der Anzahl der geschriebenen Bücher her:
def
create_author_count_mapping
(
cookbooks
:
list
[
Cookbook
]):
counter
=
{}
for
cookbook
in
cookbooks
:
if
cookbook
.
author
not
in
counter
:
counter
[
cookbook
.
author
]
=
0
counter
[
cookbook
.
author
]
+=
1
return
counter
Was sagt dir meine Verwendung von Sammlungen? Warum übergebe ich kein Wörterbuch oder eine Menge? Warum gebe ich keine Liste zurück? Ausgehend von meiner derzeitigen Verwendung von Collections kannst du Folgendes vermuten:
-
Ich gebe eine Liste von Kochbüchern ein. Es kann sein, dass es in dieser Liste doppelte Kochbücher gibt (vielleicht zähle ich ein Regal mit Kochbüchern in einem Geschäft mit mehreren Exemplaren).
-
Ich gebe ein Wörterbuch zurück. Die Nutzer können nach einem bestimmten Autor suchen oder das gesamte Wörterbuch durchgehen. Ich muss mir keine Gedanken über doppelte Autoren in der zurückgegebenen Sammlung machen.
Was wäre, wenn ich mitteilen wollte, dass keine Duplikate an diese Funktion übergeben werden sollen? Eine Liste kommuniziert die falsche Absicht. Stattdessen hätte ich ein Set wählen sollen, um zu kommunizieren, dass dieser Code auf keinen Fall mit Duplikaten umgehen wird.
Mit der Wahl einer Sammlung verrätst du den Lesern deine Absichten. Im Folgenden findest du eine Liste gängiger Sammlungsarten und die Absicht, die sie vermitteln:
- Liste
-
Dies ist eine Sammlung, über die man iterieren kann. Sie ist veränderbar: Sie kann jederzeit geändert werden. In den seltensten Fällen wirst du erwarten, dass du bestimmte Elemente aus der Mitte der Liste abrufen musst (mit einem statischen Listenindex). Es kann doppelte Elemente geben. Die Kochbücher in einem Regal könnten in einer Liste gespeichert sein.
- String
-
Eine unveränderliche Sammlung von Zeichen. Der Name eines Kochbuchs wäre ein String.
- Generator
-
Eine Sammlung, über die iteriert wird und in die niemals indiziert wird. Jeder Elementzugriff erfolgt träge, so dass jede Schleifeniteration Zeit und/oder Ressourcen in Anspruch nehmen kann. Sie sind ideal für rechenintensive oder unendliche Sammlungen. Eine Online-Datenbank mit Rezepten könnte als Generator zurückgegeben werden; du willst nicht alle Rezepte der Welt abrufen, wenn der Nutzer sich nur die ersten 10 Ergebnisse einer Suche ansehen will.
- Tupel
-
Eine unveränderliche Sammlung. Du erwartest nicht, dass sie sich ändert, also ist es wahrscheinlicher, dass du bestimmte Elemente aus der Mitte des Tupels extrahierst (entweder durch Indizes oder Auspacken). Es wird sehr selten darüber iteriert. Die Informationen über ein bestimmtes Kochbuch könnten als Tupel dargestellt werden, z. B.
(cookbook_name, author, pagecount)
. - Set
-
Eine iterierbare Sammlung, die keine Duplikate enthält. Du kannst dich nicht auf die Reihenfolge der Elemente verlassen. Die Zutaten in einem Kochbuch könnten als Menge gespeichert werden.
- Wörterbuch
-
Eine Zuordnung von Schlüsseln zu Werten. Die Schlüssel sind im gesamten Wörterbuch eindeutig. Wörterbücher werden in der Regel mit dynamischen Schlüsseln durchlaufen oder indiziert. Der Index eines Kochbuchs ist ein gutes Beispiel für eine Zuordnung von Schlüssel zu Wert (vom Thema zur Seitenzahl).
Verwende nicht die falsche Sammlung für deine Zwecke. Zu oft bin ich schon über eine Liste gestolpert, die keine Duplikate haben sollte, oder über ein Wörterbuch, das eigentlich nicht für die Zuordnung von Schlüsseln zu Werten verwendet wurde. Jedes Mal, wenn es eine Diskrepanz zwischen dem gibt, was du beabsichtigst, und dem, was im Code steht, verursachst du einen Wartungsaufwand. Die Maintainer müssen innehalten, herausfinden, was du wirklich gemeint hast, und dann ihre fehlerhaften Annahmen (und auch deine fehlerhaften Annahmen) umgehen.
Das sind die grundlegenden Sammlungen, aber es gibt noch mehr Möglichkeiten, seine Absicht auszudrücken. Hier sind einige spezielle Sammlungsarten, die noch ausdrucksstärker sind, wenn es darum geht, die Zukunft zu kommunizieren:
frozenset
-
Eine Menge, die unveränderlich ist.
OrderedDict
-
Ein Wörterbuch, das die Reihenfolge der Elemente nach der Einfügezeit beibehält. Ab CPython 3.6 und Python 3.7 behalten die eingebauten Wörterbücher auch die Reihenfolge der Elemente nach der Einfügezeit bei.
defaultdict
-
Ein Wörterbuch, das einen Standardwert liefert, wenn der Schlüssel fehlt. Ich könnte mein früheres Beispiel zum Beispiel wie folgt umschreiben:
from
collections
import
defaultdict
def
create_author_count_mapping
(
cookbooks
:
list
[
Cookbook
]):
counter
=
defaultdict
(
lambda
:
0
)
for
cookbook
in
cookbooks
:
counter
[
cookbook
.
author
]
+=
1
return
counter
Dies führt ein neues Verhalten für Endnutzer ein: Wenn sie das Wörterbuch nach einem Wert abfragen, der nicht existiert, erhalten sie eine 0. Das kann in manchen Anwendungsfällen von Vorteil sein, aber wenn nicht, kannst du stattdessen einfach
dict(counter)
zurückgeben. Counter
-
Eine besondere Art von Wörterbuch, mit dem gezählt werden kann, wie oft ein Element vorkommt. Dadurch wird unser obiger Code stark vereinfacht und lautet wie folgt:
from
collections
import
Counter
def
create_author_count_mapping
(
cookbooks
:
list
[
Cookbook
]):
return
Counter
(
book
.
author
for
book
in
cookbooks
)
Nimm dir eine Minute Zeit, um über das letzte Beispiel nachzudenken. Du wirst feststellen, dass wir durch die Verwendung von Counter
einen viel prägnanteren Code erhalten, ohne die Lesbarkeit zu beeinträchtigen. Wenn deine Leserinnen und Leser mit Counter
vertraut sind, ist die Bedeutung dieser Funktion (und wie die Implementierung funktioniert) sofort klar. Dies ist ein großartiges Beispiel dafür, wie man durch eine bessere Auswahl von Sammlungstypen seine Absicht für die Zukunft kommunizieren kann. Ich werde Sammlungen in Kapitel 5 genauer untersuchen.
Es gibt noch viele weitere Typen zu entdecken, z.B. array
, bytes
, und range
. Wann immer du auf eine neue Art von Sammlung stößt, ob eingebaut oder nicht, frage dich, wie sie sich von anderen Sammlungen unterscheidet und was sie den zukünftigen Lesern vermittelt.
Iteration
Iteration ist ein weiteres Beispiel dafür, dass die von dir gewählte Abstraktion die Absicht bestimmt, die du vermittelst.
Wie oft hast du schon so einen Code gesehen?
text
=
"This is some generic text"
index
=
0
while
index
<
len
(
text
):
(
text
[
index
])
index
+=
1
Dieser einfache Code gibt jedes Zeichen in einer eigenen Zeile aus. Für einen ersten Durchgang in Python für dieses Problem ist das völlig in Ordnung, aber die Lösung entwickelt sich schnell zu einem eher pythonischen Code (Code, der in einem idiomatischen Stil geschrieben ist, der die Einfachheit betonen soll und für die meisten Python-Entwickler erkennbar ist):
for
character
in
text
:
(
character
)
Nimm dir einen Moment Zeit und überlege, warum diese Option vorzuziehen ist. Die for
Schleife ist die geeignetere Wahl; sie kommuniziert die Absichten klarer. Genau wie bei den Auflistungstypen kommuniziert das von dir gewählte Schleifenkonstrukt explizit verschiedene Konzepte. Hier ist eine Liste mit einigen gängigen Schleifenkonstrukten und was sie vermitteln:
for
Schleifen-
for
Schleifen werden verwendet, um über jedes Element in einer Sammlung oder einem Bereich zu iterieren und eine Aktion/einen Seiteneffekt auszuführen.for
cookbook
in
cookbooks
:
print
(
cookbook
)
while
Schleifen-
while
Schleifen werden verwendet, um so lange zu iterieren, wie eine bestimmte Bedingung erfüllt ist.while
is_cookbook_open
(
cookbook
):
narrate
(
cookbook
)
- Umfassungen
-
Comprehensions werden verwendet, um eine Sammlung in eine andere umzuwandeln (normalerweise hat das keine Nebeneffekte, vor allem wenn die Comprehension faul ist).
authors
=
[
cookbook
.
author
for
cookbook
in
cookbooks
]
- Rekursion
-
Rekursion wird verwendet, wenn die Unterstruktur einer Sammlung mit der Struktur einer Sammlung identisch ist (zum Beispiel ist jedes Kind eines Baums auch ein Baum).
def
list_ingredients
(
item
):
if
isinstance
(
item
,
PreparedIngredient
):
list_ingredients
(
item
)
else
:
print
(
ingredient
)
Du willst, dass jede Zeile deines Codes einen Wert hat. Außerdem willst du, dass jede Zeile künftigen Entwicklern klar vermittelt, worin dieser Wert besteht. Das führt dazu, dass du so wenig wie möglich Kauderwelsch, Gerüste und überflüssigen Code verwenden musst. Im obigen Beispiel durchlaufe ich jedes Element und führe einen Nebeneffekt aus (Drucken eines Elements), was die for
Schleife zu einem idealen Schleifenkonstrukt macht. Ich verschwende keinen Code. Im Gegensatz dazu müssen wir bei der while
Schleife explizit verfolgen, wie die Schleife verläuft, bis eine bestimmte Bedingung eintritt. Mit anderen Worten: Ich muss eine bestimmte Bedingung verfolgen und bei jeder Iteration eine Variable ändern. Das lenkt von dem Wert ab, den die Schleife bietet, und stellt eine unerwünschte kognitive Belastung dar.
Gesetz der geringsten Überraschung (Law of Least Surprise)
Ablenkungen von der Absicht sind schlecht, aber es gibt eine Klasse von Kommunikation, die noch schlimmer ist: wenn der Code deine zukünftigen Mitarbeiter aktiv überrascht. Du solltest dich an das Gesetz der geringsten Überraschung halten: Wenn jemand den Code durchliest, sollte er so gut wie nie von einem Verhalten oder einer Implementierung überrascht werden (und wenn er doch überrascht wird, sollte ein großer Kommentar neben dem Code stehen, der erklärt, warum das so ist). Deshalb ist es so wichtig, die Absicht zu kommunizieren. Ein klarer, sauberer Code senkt die Wahrscheinlichkeit von Missverständnissen.
Hinweis
Das Gesetz der geringsten Überraschung ( Law of Least Surprise) besagt, dass ein Programm immer so auf den Benutzer reagieren sollte, dass es ihn am wenigsten überrascht.3 Überraschendes Verhalten führt zu Verwirrung. Verwirrung führt zu falschen Annahmen. Falsche Annahmen führen zu Fehlern. Und das führt zu unzuverlässiger Software.
Bedenke, dass du völlig korrekten Code schreiben und trotzdem jemanden überraschen kannst. Zu Beginn meiner Laufbahn habe ich einen fiesen Fehler verfolgt, der aufgrund eines beschädigten Speichers zum Absturz führte. Wenn ich den Code unter einen Debugger legte oder zu viele Druckanweisungen einfügte, wurde das Timing so beeinflusst, dass der Fehler nicht auftrat (ein echter "Heisenbug").4 Es gab buchstäblich Tausende von Codezeilen, die mit diesem Fehler zu tun hatten.
Also musste ich den Code manuell in zwei Hälften teilen, um herauszufinden, in welcher Hälfte der Absturz tatsächlich auftrat, indem ich die andere Hälfte entfernte, und das Ganze dann in dieser Codehälfte wiederholen. Nachdem ich mir zwei Wochen lang die Haare ausgerissen hatte, beschloss ich schließlich, eine harmlos klingende Funktion namens getEvent
zu untersuchen. Es stellte sich heraus, dass diese Funktion tatsächlich ein Ereignis mit ungültigen Daten gesetzt hatte. Ich muss nicht erwähnen, dass ich sehr überrascht war. Die Funktion war völlig korrekt, aber weil ich die Absicht des Codes nicht erkannte, habe ich den Fehler mindestens drei Tage lang übersehen. Wenn du deine Mitarbeiter überraschst, kostet das ihre Zeit.
Ein großer Teil dieser Überraschung kommt von der Komplexität. Es gibt zwei Arten von Komplexität: notwendige Komplexität und zufällige Komplexität. Notwendige Komplexität ist die Komplexität, die deinem Fachgebiet innewohnt. Deep-Learning-Modelle sind notwendigerweise komplex; sie sind nichts, was du in ein paar Minuten durchschauen und verstehen kannst. Die Optimierung des objektrelationalen Mappings (ORM) ist notwendigerweise komplex; es gibt eine große Vielfalt an möglichen Benutzereingaben, die berücksichtigt werden müssen. Du wirst nicht in der Lage sein, die notwendige Komplexität zu beseitigen, also ist es am besten, wenn du versuchst, sie einzudämmen, damit sie sich nicht in deiner Codebasis ausbreitet und zu einer unbeabsichtigten Komplexität wird.
Im Gegensatz dazu ist unbeabsichtigte Komplexität eine Komplexität, die zu überflüssigen, verschwenderischen oder verwirrenden Aussagen im Code führt. Das passiert, wenn sich ein System im Laufe der Zeit weiterentwickelt und Entwickler Funktionen einbauen, ohne den alten Code zu überprüfen, um festzustellen, ob ihre ursprünglichen Behauptungen zutreffen. Ich habe einmal an einem Projekt gearbeitet, bei dem das Hinzufügen einer einzigen Befehlszeilenoption (und die damit verbundene Möglichkeit, sie programmatisch zu setzen) nicht weniger als 10 Dateien betraf. Warum sollte das Hinzufügen eines einfachen Wertes überhaupt Änderungen in der gesamten Codebasis erfordern?
Du weißt, dass du unfallbedingte Komplexität hast, wenn du schon einmal Folgendes erlebt hast:
-
Dinge, die einfach klingen (das Hinzufügen von Benutzern, das Ändern eines UI-Controls usw.), sind nicht trivial zu implementieren.
-
Schwierigkeiten bei der Einarbeitung neuer Entwickler, um deine Codebasis zu verstehen. Neue Entwickler in einem Projekt sind der beste Indikator dafür, wie wartungsfreundlich dein Code ist - du brauchst nicht Jahre zu warten.
-
Die Kosten für das Hinzufügen von Funktionen sind immer hoch, aber du verpasst den Zeitplan trotzdem.
Entferne zufällige Komplexität und isoliere deine notwendige Komplexität, wo immer es möglich ist. Das werden die Stolpersteine für deine zukünftigen Mitarbeiter sein. Diese Quellen der Komplexität verschlimmern die Fehlkommunikation, da sie die Absicht in der gesamten Codebasis verschleiern und zerstreuen.
Diskussionsthema
Welche zufälligen Komplexitäten gibt es in deiner Codebasis? Wie schwierig wäre es, einfache Konzepte zu verstehen, wenn du in die Codebasis hineingeworfen würdest, ohne mit anderen Entwicklern zu kommunizieren? Was kannst du tun, um die in dieser Übung identifizierten Komplexitäten zu vereinfachen (vor allem, wenn sie in häufig wechselndem Code vorkommen)?
Im weiteren Verlauf des Buches werde ich mich mit verschiedenen Techniken zur Kommunikation von Absichten in Python beschäftigen.
Schlussgedanken
Robuster Code ist wichtig. Sauberer Code ist wichtig. Dein Code muss während der gesamten Lebensdauer der Codebasis wartbar sein. Um das zu gewährleisten, musst du aktiv und vorausschauend überlegen, was du wie kommunizierst. Du musst dein Wissen so nah wie möglich am Code verankern. Es wird sich wie eine Last anfühlen, ständig nach vorne zu schauen, aber mit etwas Übung wird es ganz natürlich, und du wirst die Vorteile ernten, wenn du in deiner eigenen Codebasis arbeitest.
Jedes Mal, wenn du ein Konzept aus der realen Welt in Code umsetzt, schaffst du eine Abstraktion, sei es durch die Verwendung einer Sammlung oder durch die Entscheidung, Funktionen getrennt zu halten. Jede Abstraktion ist eine Entscheidung, und jede Entscheidung kommuniziert etwas, ob sie nun beabsichtigt ist oder nicht. Ich möchte dich ermutigen, über jede Codezeile, die du schreibst, nachzudenken und dich zu fragen: "Was wird ein zukünftiger Entwickler daraus lernen?" Du bist es zukünftigen Entwicklern schuldig, sie in die Lage zu versetzen, genauso schnell Werte zu liefern, wie du es heute kannst. Andernfalls wird deine Codebasis aufgebläht, die Zeitpläne werden nicht eingehalten und die Komplexität nimmt zu. Es ist deine Aufgabe als Entwickler, dieses Risiko zu minimieren.
Achte auf potenzielle Hotspots, wie z.B. falsche Abstraktionen (z.B. Collections oder Iteration) oder unbeabsichtigte Komplexität. Dies sind die Hauptbereiche, in denen die Kommunikation mit der Zeit zusammenbrechen kann. Wenn sich solche Hotspots in Bereichen befinden, die sich häufig ändern, solltest du sie jetzt angehen.
Im nächsten Kapitel wirst du das, was du in diesem Kapitel gelernt hast, auf ein grundlegendes Python-Konzept anwenden: Typen. Die Typen, die du auswählst, drücken deine Absicht gegenüber zukünftigen Entwicklern aus, und die Wahl der richtigen Typen führt zu einer besseren Wartbarkeit.
1 Charles Antony Richard Hoare. "The Emperor's Old Clothes. Commun. ACM 24, 2 (Feb. 1981), 75-83. https://doi.org/10.1145/358549.358561.
2 Yak-Shaving beschreibt eine Situation, in der du häufig nicht zusammenhängende Probleme lösen musst, bevor du das ursprüngliche Problem überhaupt in Angriff nehmen kannst. Den Ursprung des Begriffs erfährst du unter https://oreil.ly/4iZm7.
3 Geoffrey James. Das Tao des Programmierens. https://oreil.ly/NcKNK.
4 Ein Fehler, der ein anderes Verhalten zeigt, wenn er beobachtet wird. SIGSOFT '83: Proceedings of the ACM SIGSOFT/SIGPLAN software development symposium on High-level debugging.
Get Robustes Python now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.