Von Zed A. Shaw
Die 5 einfachen Regeln für das Spiel des Codes
Wenn du ein Spiel wie Go oder Schach spielst, weißt du, dass die Regeln ziemlich einfach sind, aber die Spiele, die sie ermöglichen, extrem komplex sind. Wirklich gute Spiele haben diese einzigartige Eigenschaft einfacher Regeln mit komplexen Interaktionen. Programmierung ist auch ein Spiel mit ein paar einfachen Regeln, die komplexe Interaktionen erzeugen, und in dieser Übung werden wir lernen, was diese Regeln sind.
Bevor wir das tun, muss ich betonen, dass du diese Regeln höchstwahrscheinlich nicht direkt verwenden wirst, wenn du codierst. Es gibt Sprachen, die diese Regeln direkt nutzen, und auch deine CPU verwendet sie, aber im täglichen Programmieren wirst du sie selten verwenden. Wenn das der Fall ist, warum dann die Regeln lernen?
Weil diese Regeln überall sind, und sie zu verstehen wird dir helfen, den Code, den du schreibst, zu verstehen. Es wird dir helfen, den Code zu debuggen, wenn er schiefgeht. Wenn du jemals wissen möchtest, wie der Code funktioniert, wirst du in der Lage sein, ihn "auseinanderzunehmen" bis zu seinen grundlegenden Regeln und wirklich zu sehen, wie er funktioniert. Diese Regeln sind ein Cheat code. Wortspiel völlig beabsichtigt.
Ich werde dich auch warnen, dass du nicht erwartest, dies sofort vollständig zu verstehen. Betrachte diese Übung als Vorbereitung für die restlichen Übungen in diesem Modul. Es wird erwartet, dass du diese Übung gründlich studierst, und wenn du feststeckst, gehe zu den nächsten Übungen über, um eine Pause zu machen. Du solltest zwischen dieser und den nächsten hin und her springen, bis die Konzepte "klicken" und es Sinn macht. Du solltest auch diese Regeln so gründlich wie möglich studieren, aber lass dich hier nicht festhalten. Kämpfe ein paar Tage, mache weiter, komme zurück und versuche es weiter. Solange du es weiter versuchst, kannst du eigentlich nicht "scheitern".
Regel 1: Alles ist eine Abfolge von Anweisungen
Alle Programme sind eine Folge von Anweisungen, die einem Computer sagen, etwas zu tun. Du hast Python bereits dabei gesehen, als du Code wie diesen eingegeben hast:
x = 10
y = 20
z = x + y
Dieser Code beginnt bei Zeile 1, geht zu Zeile 2 und so weiter bis zum Ende. Das ist eine Folge von Anweisungen, aber innerhalb von Python werden diese 3 Zeilen in eine andere Folge von Anweisungen umgewandelt, die so aussieht:
LOAD_CONST 0 (10) # lade die Zahl 10
STORE_NAME 0 (x) # speichere das in x
LOAD_CONST 1 (20) # lade die Zahl 20 STORE_NAME 1 (y) # speichere das in y
LOAD_NAME 0 (x) # lädt x (was 10 ist) LOAD_NAME 1 (y) # lädt y (was 20 ist) BINARY_ADD # addiert diese STORE_NAME 2 (z) # speichert das Ergebnis in z
Das sieht ganz anders aus als die Python-Version, aber ich wette, dass du wahrscheinlich herausfinden kannst, was diese Anweisungsfolge bewirkt. Ich habe Kommentare hinzugefügt, um jede Anweisung zu erklären, und du solltest in der Lage sein, es mit dem obenstehenden Python-Code zu verbinden.
Ich mache keinen Spaß. Nimm dir jetzt etwas Zeit, um jede Zeile des Python-Codes mit den Zeilen dieses "Bytecodes" zu verbinden. Mit den Kommentaren, die ich bereitgestellt habe, bin ich mir sicher, dass du es herausfinden kannst, und das könnte ein Licht in deinem Kopf über den Python-Code einschalten.
Es ist nicht notwendig, dies auswendig zu lernen oder jede dieser Anweisungen zu verstehen. Was Sie erkennen sollten, ist, dass Ihr Python-Code in eine Folge einfacher Anweisungen übersetzt wird, die dem Computer sagen, etwas zu tun. Diese Folge von Anweisungen wird als "Bytecode" bezeichnet, da sie normalerweise in einer Datei als eine Folge von Zahlen gespeichert wird, die ein Computer versteht. Die Ausgabe, die Sie oben sehen, wird normalerweise als "Assemblersprache" bezeichnet, da es sich um eine für Menschen "lesbare" (kaum) Version dieser Bytes handelt.
Diese einfacheren Anweisungen werden von oben nach unten verarbeitet, machen eine kleine Sache nach der anderen und gehen zum Ende, wenn das Programm beendet wird. Das ist genau wie dein Python-Code, aber mit einer einfacheren Syntax von ANWEISUNGSOPTIONEN
. Eine andere Möglichkeit, dies zu betrachten, ist, dass jeder Teil von x = 10
seine eigenen Anweisungen in diesem "Byte-Code" werden könnte.
Das ist die erste Regel des Spiels der Programmierung: Alles, was du schreibst, wird schließlich zu einer Folge von Bytes, die einem Computer als Anweisungen dafür zugeführt werden, was der Computer tun soll.
Wie kann ich dieses Ergebnis erhalten?
Um dieses Ergebnis selbst zu erhalten, verwenden Sie ein Modul namens dis, das für "disassemblieren" steht. Diese Art von Code wird traditionell als "Bytecode" oder "Assemblersprache" bezeichnet, daher bedeutet dis
, "dis-assemblieren". Um dis
zu verwenden, können Sie es importieren und die dis()
-Funktion wie folgt verwenden:
from dis import dis
dis('''
x = 10
y = 20
z = x + y
''')
In diesem Python-Code mache ich Folgendes:
- Ich importiere die
dis()
-Funktion aus demdis
-Modul. - Ich führe die
dis()
-Funktion aus, aber ich gebe ihr einen mehrzeiligen String mit'''
. - Dann schreibe ich den Python-Code, den ich zerlegen möchte, in diesen mehrzeiligen String.
- Schließlich beende ich den mehrzeiligen String und die
dis()
-Funktion mit''')
.
Wenn Sie dies in Jupyter ausführen, werden Sie sehen, dass der Bytecode wie oben angezeigt wird, aber vielleicht mit einigen Extras, die wir in einer Minute behandeln werden.
Wo werden diese Bytes gespeichert?
Wenn Sie Python (Version 3) ausführen, werden diese Bytes in einem Verzeichnis namens __pycache__
gespeichert. Wenn Sie diesen Code in eine ex19.py
-Datei einfügen und dann mit python ex19.py
ausführen, sollten Sie dieses Verzeichnis sehen.
Wenn Sie in dieses Verzeichnis schauen, sollten Sie eine Menge von Dateien sehen, die mit .pyc
enden und Namen haben, die dem Code ähnlich sind, der sie erzeugt hat. Diese .pyc
-Dateien enthalten Ihren kompilierten Python-Code als Bytes.
Wenn Sie dis()
ausführen, drucken Sie eine für Menschen lesbare Version der Zahlen in der .pyc
-Datei.
Regel 2: Sprünge machen die Sequenz nicht-linear
Eine Folge einfacher Anweisungen wie LOAD_CONST 10
ist nicht sehr nützlich. Yay! Du kannst die Zahl 10 laden! Erstaunlich! Wo der Code nützlich wird, ist, wenn du das Konzept des "Sprungs" hinzufügst, um diese Folge nicht-linear zu machen. Lass uns einen neuen Teil des Python-Codes ansehen:
while True:
x = 10
Um diesen Code zu verstehen, müssen wir eine spätere Übung vorwegnehmen, in der Sie etwas über die while-Schleife
lernen. Der Code while True:
sagt einfach "Führe den Code unter mir x = 10
aus, solange True
True
ist." Da True
immer True
sein wird, wird dies für immer wiederholt. Wenn Sie dies in Jupyter ausführen, wird es niemals enden.
Was passiert, wenn du dis()
diesen Code ausführst? Du siehst die neue Anweisung JUMP_ABSOLUTE
:
dis("while True: x = 10")
0 LOAD_CONST 1 (10)
2 STORE_NAME 0 (x)
4 JUMP_ABSOLUTE 0 (to 0)
Du hast die ersten beiden Anweisungen gesehen, als wir den x = 10
Code behandelt haben, aber jetzt am Ende haben wir JUMP_ABSOLUTE 0
. Beachte, dass es die Zahlen 0
, 2
und 4
links von diesen Anweisungen gibt? Im vorherigen Code habe ich sie herausgeschnitten, damit du nicht abgelenkt wirst, aber hier sind sie wichtig, weil sie Positionen in der Sequenz darstellen, wo jede Anweisung lebt. Alles, was JUMP_ABSOLUTE 0
tut, ist, Python zu sagen, "springe zur Anweisung an Position 0", die LOAD_CONST 1 (10)
ist.
Mit dieser einfachen Anweisung haben wir den langweiligen geraden Code in eine komplexere Schleife verwandelt, die nicht mehr gerade ist. Später werden wir sehen, wie Sprünge mit Tests kombiniert werden, um noch komplexere Bewegungen durch die Byte-Sequenz zu ermöglichen.
Warum ist das rückwärts?
Vielleicht ist Ihnen aufgefallen, dass der Python-Code wie "while True is True set x equal to 10" aussieht, aber die Ausgabe von dis()
mehr wie "set x equal to 10, jump to do it again." klingt. Das liegt an Regel #1, die besagt, dass wir nur eine Folge von Bytes erzeugen müssen. Es sind keine verschachtelten Strukturen oder eine komplexere Syntax als INSTRUCTION OPTIONS
erlaubt.
Um dieser Regel zu folgen, muss Python herausfinden, wie es seinen Code in eine Folge von Bytes übersetzt, die die gewünschte Ausgabe erzeugt. Das bedeutet, den tatsächlichen Wiederholungsteil ans Ende der Folge zu verschieben, damit er in einer Folge ist. Sie werden feststellen, dass diese "rückwärts"-Natur oft auftaucht, wenn man sich Bytecodes und Assemblersprachen ansieht.
Kann ein JUMP vorwärts gehen?
Ja, technisch gesehen ist eine JUMP-Anweisung einfach eine Aufforderung an den Computer, eine andere Anweisung in der Reihenfolge zu verarbeiten. Es kann die nächste, eine vorherige oder eine zukünftige sein. So funktioniert das: Der Computer verfolgt den "Index" der aktuellen Anweisung und erhöht einfach diesen Index.
Wenn du JUMP machst, sagst du dem Computer, dass dieser Index an einen neuen Ort im Code geändert werden soll. Im Code für unsere while-Schleife (unten) befindet sich der JUMP_ABSOLUTE
an Index 4
(siehe die 4 links). Nachdem er ausgeführt wurde, ändert sich der Index auf 0
, wo sich der LOAD_CONST
befindet, sodass der Computer diese Anweisung erneut ausführt. Dies wiederholt sich endlos.
0 LOAD_CONST 1 (10)
2 STORE_NAME 0 (x)
4 JUMP_ABSOLUTE 0 (to 0)
Regel 3: Tests steuern Sprünge
Ein SPRUNG ist nützlich für Schleifen, aber was ist mit Entscheidungen treffen? Eine häufige Sache in der Programmierung ist es, Fragen zu stellen wie:
"Wenn x größer als 0 ist, setze y auf 10."
Wenn wir das in einfachem Python-Code schreiben, könnte es so aussehen:
if x > 0:
y = 10
Einmal mehr deutet dies auf etwas hin, das du später lernen wirst, aber das ist einfach genug, um es herauszufinden:
- Python wird testen, ob
x
größer ist als>
0. - Wenn ja, wird Python die Zeile
y = 10
ausführen. - Sie sehen, wie diese Zeile unter dem
if x > 0:
eingerückt ist? Das nennt man einen "Block" und Python verwendet Einrückungen, um zu sagen: "Dieser eingerückte Code gehört zum obigen Code." - Wenn
x
NICHT größer als0
ist, wird Python über die Zeiley = 10
SPRINGEN, um sie zu überspringen.
Um dies mit unserem Python-Bytecode zu tun, benötigen wir eine neue Anweisung, die den Testteil implementiert. Wir haben den JUMP. Wir haben Variablen. Wir brauchen nur eine Möglichkeit, zwei Dinge zu vergleichen und dann einen JUMP basierend auf diesem Vergleich.
Lass uns diesen Code nehmen und dis()
verwenden, um zu sehen, wie Python das macht:
dis('''
x = 1
if x > 0:
y = 10
''')
0 LOAD_CONST 0 (1) # lade 1 2 STORE_NAME 0 (x) # x = 1
4 LOAD_NAME 0 (x) # lade x 6 LOAD_CONST 1 (0) # lade 0 8 COMPARE_OP 4 (>) # vergleiche x > 0 10 POP_JUMP_IF_FALSE 10 (to 20) # springe, wenn falsch
12 LOAD_CONST 2 (10) # nicht falsch, lade 10 14 STORE_NAME 1 (y) # y = 10 16 LOAD_CONST 3 (None) # fertig, lade None 18 RETURN_VALUE # beenden
spring hier, wenn falsch
20 LOAD_CONST 3 (None) # lade none 22 RETURN_VALUE # beenden
Der Schlüsselteil dieses Codes ist der COMPARE_OP
und POP_JUMP_IF_FALSE
:
4 LOAD_NAME 0 (x) # lade x
6 LOAD_CONST 1 (0) # lade 0
8 COMPARE_OP 4 (>) # vergleiche x > 0
10 POP_JUMP_IF_FALSE 10 (to 20) # springe, wenn falsch
Hier ist, was dieser Code macht:
- Verwenden Sie
LOAD_NAME
, um diex
-Variable zu laden. - Verwenden Sie
LOAD_CONST
, um die0
-Konstante zu laden. - Verwenden Sie
COMPARE_OP
, das den>
-Vergleich durchführt und einTrue
oderFalse
-Ergebnis für später hinterlässt. - Schließlich macht
POP_JUMP_IF_FALSE
dasif x > 0
funktionsfähig. Es "poppt" denTrue
- oderFalse
-Wert, um ihn zu erhalten, und wenn esFalse
liest, wird es zu Anweisung 20JUMP
. - Wenn das geschieht, wird der Code, der
y
gesetzt hat, übersprungen, wenn der VergleichFalse
ist, aber wenn der VergleichTrue
ist, führt Python einfach die nächste Anweisung aus, die diey = 10
-Sequenz startet.
Nehmen Sie sich etwas Zeit, um dies durchzugehen, um es zu verstehen. Wenn Sie einen Drucker haben, versuchen Sie, es auszudrucken und x
manuell auf verschiedene Werte zu setzen, und verfolgen Sie dann, wie der Code funktioniert. Was passiert, wenn Sie x = -1
setzen.
Was meinst du mit "pop"?
Im obigen Code überspringe ich genau, wie Python den Wert "poppt", um ihn zu lesen, aber es speichert ihn in etwas, das "Stack" genannt wird. Denk vorerst einfach daran, dass es sich um einen temporären Speicherort handelt, in den du Werte "pushst" und dann "popst". Du musst an diesem Punkt deines Lernens wirklich nicht viel tiefer gehen. Verstehe einfach, dass der Effekt darin besteht, das Ergebnis der letzten Anweisung zu erhalten.
Warte, werden Tests wie COMPAREOP
nicht auch in Schleifen verwendet?
Ja, und du könntest wahrscheinlich jetzt herausfinden, wie das funktioniert, basierend auf dem, was du weißt. Versuche, eine while-Schleife
zu schreiben und schau, ob du sie mit dem, was du jetzt weißt, zum Laufen bringen kannst. Mach dir keine Sorgen, wenn du es nicht kannst, da wir das in späteren Übungen behandeln werden.
Regel 4: Tests der Speichersteuerungen
Sie benötigen eine Möglichkeit, um sich ändernde Daten zu verfolgen, während der Code ausgeführt wird, und dies geschieht mit "Speicher." In der Regel befindet sich dieser Speicher im Arbeitsspeicher des Computers, und Sie erstellen Namen für die Daten, die Sie im Speicher ablegen. Sie haben dies getan, als Sie Code wie diesen geschrieben haben:
x = 10
y = 20
z = x + y
In jeder der oben genannten Zeilen erstellen wir ein neues Datenstück und speichern es im Speicher. Wir geben diesen Speicherstücken auch die Namen x
, y
und z
. Wir können dann diese Namen verwenden, um diese Werte aus dem Speicher "abzurufen", was wir in z = x + y
tun. Wir rufen einfach den Wert von x
und y
aus dem Speicher ab, um sie zusammenzuzufügen.
Das ist der Großteil der Geschichte, aber der wichtige Teil dieser kleinen Regel ist, dass du fast immer Speicher verwendest, um Tests zu steuern.
Sicher, du kannst Code wie diesen schreiben:
if 1 < 2:
print("aber...warum?")
Das ist jedoch sinnlos, da es nur die zweite Zeile nach einem sinnlosen Test ausführt. 1
ist immer kleiner als 2
, also ist es nutzlos.
Wo Tests wie COMPARE_OP
glänzen, ist, wenn Sie Variablen verwenden, um die Tests dynamisch basierend auf Berechnungen zu gestalten. Deshalb betrachte ich dies als eine "Regel des Spiels des Codes", denn Code ohne Variablen spielt das Spiel nicht wirklich.
Nehmen Sie sich die Zeit, die vorherigen Beispiele durchzugehen und die Stellen zu identifizieren, an denen LOAD
-Anweisungen verwendet werden, um Werte zu laden, und STORE
-Anweisungen verwendet werden, um Werte im Speicher zu speichern.
Regel 5: Eingabe-/Ausgabekontrollen Speicherung
Die letzte Regel des Spiels der Programmierung ist, wie dein Code mit der Außenwelt interagiert. Variablen zu haben ist großartig, aber ein Programm, das nur Daten enthält, die du in die Quelldatei eingegeben hast, ist nicht sehr nützlich. Was du brauchst, ist Eingabe und Ausgabe.
Eingabe ist, wie Sie Daten in Ihren Code von Dingen wie Dateien, der Tastatur oder dem Netzwerk erhalten. Sie haben bereits open()
und input()
verwendet, um dies im letzten Modul zu tun. Sie haben auf die Eingabe zugegriffen, jedes Mal, wenn Sie eine Datei geöffnet, den Inhalt gelesen und etwas damit gemacht haben. Sie haben auch Eingaben verwendet, wenn Sie input()
verwendet haben, um dem Benutzer eine Frage zu stellen.
Ausgabe ist, wie Sie die Ergebnisse Ihres Programms speichern oder übertragen. Die Ausgabe kann auf dem Bildschirm mit print()
, in einer Datei mit file.write()
oder sogar über ein Netzwerk erfolgen.
Das einzige Problem mit Eingabe und Ausgabe an diesem Punkt ist, dass der Bytecode-Ausgang etwas komplizierter ist. Lassen Sie uns einen einfachen ansehen:
from dis import dis
dis("input('Ja? ')")
0 LOAD_NAME 0 (Eingabe) 2 LOAD_CONST 0 ('Ja? ') 4 CALL_FUNCTION 1 6 RETURN_VALUE
Dieser dis()
-Lauf hilft nicht viel, da wir jetzt in ein fortgeschrittenes Thema eintauchen, das wir später behandeln werden, das "Funktionen" genannt wird, also lassen Sie uns dort aufhören und diese Regeln zusammenfassen.
Alles Zusammenbringen
Mit den 5 Regeln haben wir das folgende Spiel des Codes:
- Du liest Daten als Eingabe für dein Programm (Regel #5).
- Du speicherst diese Daten im Speicher (Variablen) (Regel #4).
- Du verwendest diese Variablen, um Tests durchzuführen... (Regel #3).
- ...damit du herum JUMPEN kannst... (Regel #2)
- ...die Reihenfolge der Anweisungen... (Regel #1)
- ...die Daten in neue Variablen umwandelnd (Regel #4)...
- ...die du dann zur Ausgabe für die Speicherung oder Anzeige schreibst. (Regel #5).
Während dies einfach erscheint, schaffen diese kleinen Regeln sehr komplizierte Software. Videospiele sind ein großartiges Beispiel für sehr komplizierte Software, die dies tut. Ein Videospiel liest deinen Controller oder deine Tastatur als Eingabe, aktualisiert Variablen, die die Modelle in der Szene steuern, und verwendet fortgeschrittene Anweisungen, die die Szene als Ausgabe auf deinem Bildschirm darstellen.
Ihr nächster Schritt besteht darin, das Python zu studieren, das all diese Regeln verwendet.
Die Übungen danach werden dann auf die Konzepte hier verweisen, um die Konzepte von while-Schleifen
, if-Anweisungen
, boolescher Logik und den fortgeschritteneren Anwendungen dieser Regeln zu erklären. Durch das Lernen der Regeln wird es hoffentlich einfacher, zu verstehen, wie jede Sache funktioniert, aber sag du es mir. Macht das Sinn?