Programmiersprachen und Programmierparadigmen
AnfängerEinheit 5 von 7 in Kurs: Informatik – Computer, Algorithmen, Internet und KI verstehen
Was du lernst
- 1Erkläre, wie aus Code einer höheren Programmiersprache Befehle entstehen, die ein Prozessor ausführtVerfolge ein Programm von dem Code, den ein Mensch schreibt, bis zu den Binärbefehlen, die ein Prozessor ausführt. Vergleiche die Möglichkeiten der Übersetzung: im Voraus, während das Programm läuft oder als Mischung aus beidem.
- Vergleiche Maschinencode, Assemblersprache und höhere ProgrammiersprachenBeschreibe die drei Ebenen, auf denen Programme geschrieben werden können, zeige, wie eine Zeile einer höheren Programmiersprache mehreren Maschinenbefehlen entspricht, und erkläre, warum die meiste Software auf der höchsten Ebene geschrieben wird.
- Erkläre die Aufgaben eines CompilersBeschreibe, wie ein Compiler ein ganzes Programm im Voraus übersetzt, benenne seine wichtigsten Phasen und erkläre, was mit Fehlern und der fertigen ausführbaren Datei geschieht.
- Erkläre Interpreter, Bytecode und Just-in-time-KompilierungBeschreibe, wie ein Interpreter ein Programm ausführt, wie Sprachen wie Java und Python Kompilierung und Interpretation verbinden und wie du die Vor- und Nachteile dieser Ansätze abwägst.
- Vergleiche Maschinencode, Assemblersprache und höhere Programmiersprachen
- Trenne die Regeln für die Schreibweise von Code von den Regeln für seine Wirkung, erkenne die Grundbausteine, die die meisten höheren Programmiersprachen gemeinsam haben, und vollziehe kurze Programme nach, um ihre Ergebnisse vorherzusagen.
- Unterscheide Syntax und SemantikErkläre den Unterschied zwischen der Form eines Programms und seiner Bedeutung, unterscheide Syntax-, Laufzeit- und logische Fehler und erkenne, wo Sprachen gleich aussehendem Code eine andere Bedeutung geben.
- Vollziehe die gemeinsamen Grundbausteine von Programmen in höheren Programmiersprachen nachErkenne die Konstrukte, die in fast jeder höheren Programmiersprache vorkommen (Variablen, Ausdrücke, Kontrollfluss und Funktionen mit ihrem Gültigkeitsbereich), und vollziehe ein kurzes Programm Schritt für Schritt nach, um seine Ausgabe vorherzusagen.
- Unterscheide Syntax und Semantik
- Beschreibe, wie Sprachen Werte nach Typen einordnen und prüfen, wo ein laufendes Programm seine Daten speichert und wie Speicher durch den Programmierer oder automatisch reserviert und freigegeben wird. Erkläre dabei die jeweiligen Vor- und Nachteile für Sicherheit und Geschwindigkeit.
- Vergleiche, wie Programmiersprachen Datentypen nutzen und überprüfenErkläre, was ein Datentyp ist, vergleiche statische und dynamische Typisierung sowie strenge und lockere Umwandlungsregeln und sage vorher, wie verbreitete Sprachen mit gemischten Typen und Zahlengrenzen umgehen.
- Beschreibe, wo ein laufendes Programm seine Daten speichertErkläre Variablen als benannte Speicherstellen, die Aufgaben von Aufrufstapel und Heap sowie den Unterschied zwischen dem Kopieren eines Wertes und dem gemeinsamen Nutzen einer Referenz.
- Vergleiche manuelle und automatische SpeicherverwaltungErkläre, wie Speicher in Sprachen freigegeben wird, in denen der Programmierer ihn verwaltet, und in Sprachen mit automatischer Speicherbereinigung oder Ownership-Regeln. Stelle einen Zusammenhang zwischen Speicherfehlern, Programmabstürzen und Sicherheitslücken her.
- Vergleiche, wie Programmiersprachen Datentypen nutzen und überprüfen
- Beschreibe Programme zunächst als Befehlsfolgen, die den gespeicherten Zustand ändern, und dann als Sammlungen von Objekten, die Daten und Verhalten bündeln. Nutze Klassen, Datenkapselung, Vererbung und Polymorphie, um Code zu strukturieren.
- Beschreibe imperative und prozedurale ProgrammierungErkläre imperative Programmierung als schrittweise Befehle, die den Zustand eines Programms ändern, zeige, wie Prozeduren und strukturierter Kontrollfluss diese Befehle ordnen, und erkenne die Risiken eines gemeinsamen veränderlichen Zustands.
- Modelliere Daten und Verhalten mithilfe von Klassen und ObjektenErkläre, wie die objektorientierte Programmierung Daten und die darauf ausführbaren Operationen in Objekten zusammenfasst, lies und entwirf eine einfache Klasse und schütze die Daten eines Objekts durch Datenkapselung.
- Verwende Klassen wieder und erweitere sie mithilfe von Vererbung, Polymorphie und KompositionNutze Vererbung, um spezialisierte Klassen zu erstellen, erkläre, wie Polymorphie ermöglicht, dass derselbe Aufruf sich bei unterschiedlichen Objekten anders verhält, und entscheide, wann du Objekte kombinierst, statt zu vererben.
- Beschreibe imperative und prozedurale Programmierung
- Erstelle Programme aus Funktionen, die neue Werte zurückgeben, statt alte zu ändern, und schreibe Programme, die das gewünschte Ergebnis angeben, während die Ausführungsumgebung der Sprache entscheidet, wie es berechnet wird.
- Erstelle Programme aus reinen Funktionen und unveränderlichen DatenErkläre reine Funktionen, Unveränderlichkeit, Funktionen erster und höherer Ordnung sowie Rekursion und schreibe eine Schleife als Abfolge von
map,filterundreduceum. - Beschreibe das gewünschte Ergebnis mithilfe deklarativer SprachenErkläre deklarative Programmierung als Angabe des gewünschten Ergebnisses statt des Weges dorthin und lies Beispiele in einer Datenbankabfragesprache, einer logischen Sprache und anderen deklarativen Notationen.
- Erstelle Programme aus reinen Funktionen und unveränderlichen Daten
- Löse dasselbe Problem in verschiedenen Stilen, erkenne, dass die meisten modernen Sprachen Paradigmen mischen, und wäge die technischen und praktischen Faktoren ab, die darüber entscheiden, welche Sprache zu einem Projekt passt.
- Löse dasselbe Problem mit verschiedenen ProgrammierparadigmenSchreibe dieselbe kleine Aufgabe im imperativen, objektorientierten, funktionalen und deklarativen Stil, vergleiche, was jede Version erleichtert, und erkenne Multiparadigmensprachen.
- Wähle eine passende Programmiersprache für eine AufgabeWäge die Faktoren ab, die darüber entscheiden, welche Sprache zu einem Projekt passt (Plattform, Anwendungsgebiet, Leistung, Sicherheit, Ökosystem und Menschen), und begründe eine Wahl für ein konkretes Szenario.
- Löse dasselbe Problem mit verschiedenen Programmierparadigmen
Programmierparadigmen einfach erklärt
1Vom Quellcode zu ausführbaren Befehlen
Ein Prozessor führt kleine Binärbefehle aus, während Menschen Programme meist in Sprachen schreiben, die auf das zu lösende Problem ausgerichtet sind. Übersetzungsprogramme verbinden diese beiden Ebenen. Verfolge dieselbe Berechnung durch diese Schichten und vergleiche anschließend das Erstellen eines Programms vor seiner Ausführung mit der Interpretation oder der Übersetzung von Teilen während der Ausführung. Der Weg beeinflusst Portierbarkeit, Startzeit, Geschwindigkeit und den Zeitpunkt, zu dem ein Fehler auftreten kann.
1.1 Drei Ebenen von Programmcode
Eine Zeile wie x = a + b bezeichnet eine Berechnung, ohne ein Prozessorregister zu erwähnen. Abstraktion macht dies möglich: Eine Sprache verbirgt Details der darunterliegenden Ebene, sodass sich ein Programmierer auf die Aufgabe des Programms konzentrieren kann.
Was der Prozessor ausführt
Maschinencode besteht aus Befehlen, die als Binärzahlen kodiert sind. Die Befehlssatzarchitektur (ISA) eines Prozessors legt seine Befehle und deren Kodierungen fest. x86-64 ist in PCs und Servern verbreitet; ARM kommt häufig in Smartphones, Tablets und vielen Laptops vor. Für eine ISA erstellter Code muss übersetzt oder emuliert werden, damit er auf einer anderen funktioniert.
Die Hexadezimaldarstellung verkürzt die Anzeige von Befehlsbytes. Auf x86-64 schreibt B8 01 00 00 00 die Zahl 1 in das Register EAX. Die Bytes 01 D8 addieren den Wert in EBX zu EAX. Register sind kleine Speicherstellen innerhalb des Prozessors. Jeder Befehl führt eine kleine Aktion aus, etwa Daten verschieben, addieren, vergleichen oder springen. Der Befehlszyklus führt diese Befehle aus.
Namen für dieselben kleinen Aktionen
Assemblersprache ersetzt Befehlsbytes durch lesbare Namen, sogenannte Mnemonics. Die beiden x86-64-Befehle lassen sich so schreiben:
mov eax, 1
add eax, ebx
Ein Assembler wandelt Assemblersprache in Maschinencode um. Eine gewöhnliche Befehlszeile erzeugt normalerweise einen Maschinenbefehl. Registernamen und Befehle gehören weiterhin zu einer bestimmten ISA, sodass die Verwendung von Wörtern statt Bytes Assemblersprache nicht portierbar macht.
Ein Problem in weniger Zeilen beschreiben
Eine höhere Programmiersprache bietet Variablen, Ausdrücke, Bedingungen, Schleifen und Funktionen. Python, C, Java und JavaScript erlauben es dem Programmierer, Operationen zu beschreiben, ohne Register auszuwählen. Eine Anweisung benötigt häufig mehrere Maschinenbefehle. Eine vereinfachte Übersetzung von x = a + b würde:
- Den Wert von
ain ein Register laden. - Den Wert von
bin ein anderes Register laden. - Die Registerwerte addieren.
- Das Ergebnis für
xspeichern.
Ein tatsächlicher Compiler kann diese Schritte zusammenfassen oder umordnen und dabei das erforderliche Verhalten beibehalten.
| Ebene | Was ein Programmierer schreibt | Wer oder was kümmert sich um die Hardwaredetails? |
|---|---|---|
| Maschinencode | Kodierte Prozessorbefehle | Der Programmierer oder Codegenerator |
| Assemblersprache | Mnemonics und Register | Der Programmierer, wobei ein Assembler die Befehle kodiert |
| Code einer höheren Programmiersprache | Variablen und Operationen auf den Daten des Problems | Ein Compiler, Interpreter oder eine Kombination |
Quellcode in einer höheren Programmiersprache lässt sich leichter lesen, ändern und prüfen. Portierbarer C-Quellcode kann einmal für ein x86-64-System und erneut für ein ARM-System kompiliert werden. Jede erstellte Version benötigt weiterhin die passende Unterstützung durch das Betriebssystem und passende Bibliotheken. Die Portierbarkeit des Quellcodes macht eine native ausführbare Datei nicht universell einsetzbar.
Warum die unteren Ebenen weiterhin wichtig sind
Assemblersprache bleibt nützlich in Startcode, einigen Betriebssystem- und Treiberroutinen, kleinen eingebetteten Systemen und bei der Sicherheitsanalyse von Programmen, deren Quellcode nicht verfügbar ist. Für gewöhnlichen Anwendungscode erzeugen moderne Compiler meist Maschinencode, dessen Leistung mit der von handgeschriebenem Assemblercode vergleichbar ist.
Der Übergang zu höheren Ebenen erfolgte allmählich. Assemblersprache verbreitete sich in den späten 1940er- und frühen 1950er-Jahren. Grace Hoppers A-0-System trug 1952 zur automatischen Übersetzung bei. IBM veröffentlichte FORTRAN 1957; COBOL folgte 1959, und Lisp entstand etwa zwischen 1958 und 1960. „Höher“ bezeichnet den Abstand zu Hardwaredetails, nicht die Schwierigkeit, die Sprache zu erlernen.
Wichtige Erkenntnisse
- Ein Prozessor führt Maschinenbefehle aus, die durch seine ISA festgelegt sind.
- Ein Assembler kodiert Assemblerbefehle; Assemblersprache bleibt an eine ISA gebunden.
- Anweisungen höherer Programmiersprachen verbergen Hardwareschritte und werden häufig zu mehreren Befehlen.
- Portierbarer Quellcode lässt sich für unterschiedliche Ziele übersetzen; nativer Maschinencode ist zielspezifisch.
- Maschinennaher Code bleibt dort nützlich, wo Hardwarezugriff oder Analyse ihn erfordern.
1.2 Wie ein Compiler ein Programm erstellt
Ein Compiler übersetzt Quellcode in Zielcode. Das Ziel kann nativer Maschinencode oder eine Zwischenform wie Bytecode sein. Die Ahead-of-time-Kompilierung (AOT) findet vor der Programmausführung statt; die Just-in-time-Kompilierung erfolgt während der Ausführung.
Vom Text zu Befehlen
Bei einer Erstellung im Voraus wird eine Quelldatei in mehreren Phasen in Befehle umgewandelt:
- Die lexikalische Analyse zerlegt Text in Token: bedeutungstragende Teile wie Namen, Operatoren und Zahlen.
- Die syntaktische Analyse prüft die Grammatik und erstellt eine Struktur, die zeigt, wie die Teile zusammenpassen.
- Die semantische Analyse prüft Bedeutungsregeln, darunter Deklarationen und erlaubte Typkombinationen.
- Die Optimierung findet einen effizienteren Weg, die erforderliche Arbeit zu erledigen.
- Die Codegenerierung erzeugt die Zielbefehle.
Betrachte die C-Anweisung total = price * 3;. Ihre sechs Token sind total, =, price, *, 3 und ;. Die Namen sind Bezeichner, 3 ist ein Ganzzahlliteral und das Semikolon beendet die Anweisung. Die syntaktische Analyse hält fest, dass die Multiplikation innerhalb der Zuweisung erfolgt: Berechne price * 3 und speichere dessen Wert anschließend in total.
Diese Phasen haben unterschiedliche Aufgaben. Eine korrekt geschriebene Folge von Token kann dennoch gegen die Grammatik verstoßen. Eine grammatisch korrekte Anweisung kann trotzdem einen Typ falsch verwenden. Token zu erkennen reicht nicht aus, um festzustellen, ob ein Programm gültig ist.
Dieselbe Arbeit mit weniger Aufwand erledigen
Angenommen, Quellcode enthält seconds_per_day = 60 * 60 * 24. Die Faktoren sind Konstanten, sodass der Compiler bei der Erstellung 60 mit 60 zu 3.600 und dann mit 24 zu 86.400 multiplizieren kann. Er kann den Endwert in die Ausgabe übernehmen, statt die Multiplikation zur Laufzeit zu wiederholen. Dies heißt Konstantenfaltung.
Eine Optimierung muss das von der Sprache geforderte Verhalten erhalten. Sie darf eine Berechnung nicht durch eine schnellere ersetzen, die andere als die geforderten Ergebnisse liefert. Das Ziel ist, Arbeit, Zeit oder Speicher zu sparen und dabei die Bedeutung des Programms beizubehalten.
Die Teile verbinden
Große Programme werden meist aus vielen Dateien erstellt. Ein Compiler kann für jede Quelldatei eine Objektdatei erzeugen. Eine Objektdatei enthält übersetzten Code, der möglicherweise noch Verbindungen zu anderem Code benötigt. Ein Linker verbindet die Teile und löst Aufrufe von Bibliotheksroutinen auf, etwa Ausgabe- oder Rechenfunktionen.
| Bindeverfahren | Herkunft des Bibliothekscodes zur Laufzeit |
|---|---|
| Statisches Binden | Code, der bei der Erstellung in die ausführbare Datei aufgenommen wird |
| Dynamisches Binden | Eine kompatible gemeinsam genutzte Bibliothek, die beim Start oder während der Ausführung geladen wird |
Die entstehende ausführbare Datei wird für eine Zielplattform erstellt, einschließlich einer ISA und der Konventionen des Betriebssystems. Der Benutzer benötigt den Ahead-of-time-Compiler nicht, um sie auszuführen. Gemeinsam genutzte Bibliotheken oder Laufzeitkomponenten können weiterhin erforderlich sein.
Verstehen, was eine erfolgreiche Erstellung belegt
In Java besteht int count = "five"; vor der Ausführung eine Typprüfung nicht. Text kann diese Ganzzahlvariable nicht initialisieren. Das Programm muss die erforderlichen Prüfungen zur Übersetzungszeit bestehen, bevor eine lauffähige Version vorliegt.
Andere Fehler überstehen die Übersetzung. Eine Division durch eine vom Benutzer eingegebene Null hängt von Laufzeitdaten ab. Ein Programm, das subtrahieren soll, stattdessen aber addiert, kann laufen und den falschen Wert zurückgeben. Die Übersetzung prüft Sprachregeln; Tests müssen zusätzlich prüfen, ob das Ergebnis zur beabsichtigten Aufgabe passt.
Wichtige Erkenntnisse
- Kompilierung übersetzt Quellcode in Zielcode, der kein Maschinencode sein muss.
- Compilerphasen prüfen Token, Struktur und Bedeutung, bevor sie Zielbefehle erzeugen.
- Optimierung erhält das erforderliche Verhalten; Konstantenfaltung wertet Konstanten bei der Erstellung aus.
- Ein Linker verbindet Objektdateien und Bibliothekscode.
- Eine erfolgreiche Erstellung kann trotzdem Laufzeitfehler oder logische Fehler enthalten.
1.3 Interpretation, Bytecode und JIT-Kompilierung
Ein Programm muss nicht zu einer nativen ausführbaren Datei werden, bevor ein Benutzer es startet. Ein Interpreter führt das Programm mithilfe eines anderen laufenden Programms aus. Viele Implementierungen verbinden Interpretation mit Kompilierung. Die hilfreiche Frage lautet daher, wie das jeweilige System Code ausführt.
Mit einem Interpreter ausführen
Ein Interpreter muss immer verfügbar sein, wenn er das Programm ausführt. Eine interaktive REPL wiederholt vier Aktionen: eine Eingabe lesen, sie auswerten, ein Ergebnis ausgeben und zur nächsten Eingabe zurückkehren. Sie gibt beim Ausprobieren kleiner Ausdrücke schnell Rückmeldung.
Interpretation bedeutet nicht zwangsläufig, rohen Quellcode Zeile für Zeile zu lesen. CPython, die Standardimplementierung von Python, übersetzt eine Datei zunächst in Bytecode. Eine virtuelle Maschine als Software führt dann diese internen Befehle aus. Der Bytecode gehört zu dieser Laufzeitumgebung; er ist nicht der native Befehlssatz des Prozessors.
Warum manche Fehler früher erscheinen
Vergleiche zwei Python-Dateien. Diese hat eine gültige Syntax:
print("start")
x = 10
print(x + "5")
Sie gibt „start“ aus, bindet x an 10 und löst dann einen TypeError aus. Die Addition dieser Ganzzahl zu einer Zeichenkette schlägt erst fehl, wenn der Ausdruck ausgeführt wird. Eine ähnliche Operation in einem nicht erreichten Zweig kann unbemerkt bleiben, bis der Zweig ausgeführt wird.
Entferne nun die schließende Klammer aus der letzten Zeile. CPython kann die Datei nicht übersetzen, weil ihre Syntax unvollständig ist. Es meldet einen Syntaxfehler, bevor eine Anweisung in dieser Datei ausgeführt wird, sodass „start“ nicht ausgegeben wird. Die Syntaxprüfung der ganzen Datei und die Laufzeitprüfung einer ausgeführten Operation finden zu unterschiedlichen Zeitpunkten statt.
Bytecode weitergeben und während der Ausführung spezialisieren
Java zeigt einen verbreiteten hybriden Weg. javac Hello.java erzeugt Hello.class, das Bytecode enthält. java Hello startet eine kompatible Java Virtual Machine (JVM). Unterschiedliche JVM-Implementierungen ermöglichen, dass dieser Bytecode auf verschiedenen Prozessorfamilien läuft. Der Bytecode ist zwischen kompatiblen Laufzeitumgebungen portierbar; jede Laufzeitumgebung muss selbst auf ihrer jeweiligen Plattform funktionieren. C# zielt in ähnlicher Weise auf eine von .NET verwendete Zwischensprache.
Ein Just-in-time-Compiler (JIT-Compiler) erzeugt während der Programmausführung Maschinencode. Die HotSpot-Laufzeitumgebung von Java beginnt gewöhnlich mit Interpretation und kompiliert häufig verwendete Methoden. JavaScript-Ausführungsumgebungen in Browsern kombinieren ebenfalls Ausführungstechniken, darunter die JIT-Kompilierung. Das Kompilieren häufig ausgeführter Teile kann die dauerhafte Geschwindigkeit verbessern, benötigt aber während der Ausführung Zeit und Speicher.
| Weg | Nützlicher Vorteil | Zu berücksichtigender Aufwand |
|---|---|---|
| Native Erstellung im Voraus | Die Arbeit wird vor dem Start übersetzt; der Benutzer benötigt keinen Compiler | Separate kompatible Versionen für Zielplattformen |
| Interpretation | Schnelle Rückmeldung beim Ändern und Ausführen mit installiertem Interpreter | Wiederholte Arbeit zur Laufzeit kann rechenintensiven Code verlangsamen |
| Bytecode mit virtueller Maschine und JIT | Portierbarer Zwischencode mit Optimierung zur Laufzeit | Installation der Laufzeitumgebung, Aufwärmphase und zusätzlicher Speicherbedarf |
Eine Schleife, die eine Milliarde Werte addiert, läuft in kompiliertem C typischerweise viel schneller als in CPython: CPython führt bei vielen Operationen Interpreterarbeit und Typprüfungen aus. Bei einem Skript, das 20 Dateien umbenennt, kann der Entwicklungsaufwand weit mehr ins Gewicht fallen als dieser Unterschied. Miss die tatsächliche Arbeitslast, bevor du aus der Bezeichnung einer Sprache auf ihre Geschwindigkeit schließt.
Kompiliert und interpretiert beschreiben Entscheidungen bei der Implementierung. CPython verwendet Bytecode-Interpretation; PyPy nutzt JIT-Techniken. Eine Sprache kann mehr als eine Implementierung haben, und eine Implementierung kann mehrere Ausführungsmethoden verwenden.
Wichtige Erkenntnisse
- Ein Interpreter führt ein Programm aus und muss dafür vorhanden sein.
- CPython übersetzt eine Datei in Bytecode, bevor es sie ausführt.
- Ein Syntaxfehler kann den Start einer Datei verhindern; ein Laufzeitfehler erscheint, wenn die betreffende Operation ausgeführt wird.
- Ein JIT-Compiler erzeugt während der Ausführung nativen Code, oft für häufig genutzte Teile.
- Ausführungsmethoden gehören zu Implementierungen, und die Leistung hängt von der Arbeitslast ab.
2Lesen, was kurze Programme tun
Beim Lesen von Code stellen sich zwei Fragen: ob seine Form den Regeln der Sprache folgt und was seine Operationen bedeuten. Ein gleich aussehender Ausdruck kann sich in zwei Sprachen unterschiedlich verhalten. Lerne, Fehler einzuordnen, und vollziehe anschließend Zuweisungen, Entscheidungen, Schleifen und Funktionsaufrufe in ihrer Ausführungsreihenfolge nach. Eine kleine Tabelle der sich ändernden Werte macht den Unterschied zwischen der Absicht des Programmierers und dem tatsächlichen Verhalten des Programms sichtbar.
2.1 Syntax, Bedeutung und drei Fehlerarten
Syntax legt die Regeln für gültigen Programmtext fest. Semantik gibt diesem Text seine Bedeutung. Ein Programm kann jede Grammatikregel befolgen und trotzdem das falsche Ergebnis berechnen. Die Prüfung seiner äußeren Form ist daher nur der erste Teil des Lesens.
Die Bestandteile und die Grammatik erkennen
Zu den Token gehören Schlüsselwörter wie if, Bezeichner wie total, Literale wie 3 oder "hello", Operatoren wie + und == sowie Satzzeichen. Eine Grammatik beschreibt, wie diese Teile Ausdrücke und Anweisungen bilden können.
Python verlangt einen Doppelpunkt nach if x > 3: und verwendet Einrückung, um die von dieser Bedingung gesteuerten Anweisungen zu kennzeichnen. C, Java und JavaScript verwenden geschweifte Klammern für Blöcke; C und Java verlangen außerdem nach vielen Anweisungen Semikolons. In diesen Sprachen hilft die Einrückung menschlichen Lesern, bestimmt aber nicht den Block.
Unterschiedliche Syntax kann eine ähnliche Aktion ausdrücken. Diese Anweisungen geben eine Begrüßung aus:
print("hello")
printf("hello\n");
System.out.println("hello");
Die C-Escape-Sequenz \n fügt einen Zeilenumbruch hinzu. Das Konzept der Ausgabe zu kennen hilft dabei, die jeweilige Ausdrucksweise jeder Sprache zu lernen.
Dieselben Symbole können Unterschiedliches bedeuten
| Ausdruck und Sprache | Ergebnis | Regel |
|---|---|---|
7 / 2 in Python oder JavaScript | 3.5 | Die Division liefert ein Ergebnis mit Nachkommateil |
7 / 2 mit Ganzzahloperanden in C oder Java | 3 | Die Ganzzahldivision schneidet in Richtung null ab |
-7 // 2 in Python | -4 | Die ganzzahlige Division rundet ab |
-7 / 2 mit Ganzzahlen in C | -3 | Das Abschneiden erfolgt in Richtung null |
"3" * 4 in Python | "3333" | Eine Zeichenkette wird wiederholt |
"3" * 4 in JavaScript | 12 | Text wird für die Multiplikation in eine Zahl umgewandelt |
Das vertraute Aussehen eines Symbols legt seine Bedeutung nicht für alle Sprachen fest.
Auch die Auswertungsreihenfolge ist wichtig. Die Rangfolge bewirkt, dass Multiplikation und Division vor der Addition stattfinden. Bei a = 4 und b = 6 dividiert average = a + b / 2 zuerst 6 durch 2 und addiert dann 4, was 7,0 ergibt. Die Klammern in average = (a + b) / 2 bewirken, dass zuerst die Summe gebildet wird, und ergeben 5,0.
Fehler unterscheiden
Ein Syntaxfehler verstößt gegen die Grammatik, etwa ein fehlender Python-Doppelpunkt. Ein Laufzeitfehler tritt während einer ausgeführten Operation auf, etwa bei einer Division durch null in Python. Ein logischer Fehler lässt die Ausführung zu, liefert aber ein unbeabsichtigtes Ergebnis, wie bei der falschen Durchschnittsberechnung.
Manche gültigen Ausdrücke überspringen absichtlich eine Operation. In Python verwendet x != 0 and 10 / x > 2 die Kurzschlussauswertung. Ist x null, ist die erste Bedingung falsch, sodass die rechte Seite nicht ausgewertet wird und keine Division stattfindet. Eine Umkehrung dieser Bedingungen würde diesen Schutz aufheben. Python schreibt and, or und not; C, Java und JavaScript verwenden &&, || und !.
Zuweisung und Vergleich sind eine weitere wichtige Unterscheidung. = speichert einen Wert; == prüft Gleichheit. C erlaubt if (x = 5): Es weist 5 zu und behandelt diesen Wert ungleich null als wahr. Python weist if x = 5: als ungültige Syntax zurück. Derselbe Fehler kann daher in unterschiedlichen Sprachen zu einem anderen Zeitpunkt erkannt werden.
Wichtige Erkenntnisse
- Syntax legt die gültige Form fest; Semantik legt die Bedeutung fest.
- Operatoren können in verschiedenen Sprachen unterschiedliche Bedeutungen haben.
- Rangfolge und Klammern bestimmen, welche Operationen zusammengefasst werden.
- Syntax-, Laufzeit- und logische Fehler erfordern unterschiedliche Prüfungen.
- Kurzschlussauswertung kann eine unsichere Operation überspringen.
- Eine Zuweisung speichert einen Wert; ein Gleichheitsvergleich prüft Werte.
2.2 Verfolge Werte durch Kontrollfluss und Aufrufe
Ein kurzes Programm lässt sich leichter verstehen, wenn du jeweils eine ausgeführte Anweisung nachverfolgst. Halte die aktuellen Werte sichtbar fest, statt von der ersten Zeile direkt zu einem vermuteten Ergebnis zu springen.
Ausdrücke und Zuweisungen
Ein Ausdruck liefert einen Wert: Beispiele sind price * 3, x > 10 und len(name). Eine Anweisung führt eine Aktion aus, etwa einen Wert zuweisen oder einen Block steuern. In total = price * 3 wird der Ausdruck auf der rechten Seite ausgewertet, bevor sein Ergebnis total zugewiesen wird.
Eine Variable ist ein Name, der sich auf einen Wert bezieht. Eine Zuweisung gibt dem Namen seinen aktuellen Wert; sie ist keine mathematische Aussage, dass beide Seiten schon immer gleich waren. Enthält count den Wert 4, liest count = count + 1 zunächst 4, addiert 1 und bindet count an 5.
Die nächste ausgeführte Anweisung verfolgen
Drei Strukturen ordnen den Kontrollfluss:
- Eine Sequenz führt Anweisungen der Reihe nach aus.
- Eine Verzweigung wählt anhand einer Bedingung einen Weg, etwa mit
ifundelse. - Eine Iteration wiederholt einen Block mit einer
for- oderwhile-Schleife.
Der Strukturierungssatz, der mit Böhm und Jacopini im Jahr 1966 verbunden ist, zeigt, dass sich mit diesen Strukturen jeder Algorithmus ausdrücken lässt. Eine for-Schleife durchläuft gewöhnlich die Elemente einer Sammlung. Eine while-Schleife wiederholt sich, solange ihre Bedingung wahr bleibt.
Betrachte diese Python-Schleife:
total = 0
for n in [3, 8, 5]:
total = total + n
Die eingerückte Zuweisung wird für jedes Element einmal ausgeführt. Eine Ablauftabelle hält den Zustand nach jeder Aktualisierung fest:
| Zeitpunkt | Aktuelles n | total |
|---|---|---|
| Vor der Schleife | Noch kein Element | 0 |
| Erste Aktualisierung | 3 | 3 |
| Zweite Aktualisierung | 8 | 11 |
| Dritte Aktualisierung | 5 | 16 |
Jede neue Summe baut auf der vorherigen auf. Würdest du total innerhalb der Schleife zurücksetzen, ergäbe sich eine andere Berechnung, weil frühere Additionen verloren gingen.
Eine Funktion betreten und verlassen
Eine Funktion fasst eine Aufgabe unter einem Namen zusammen. Ein Parameter ist ein Name in der Definition; ein Argument ist ein Wert, der bei einem Aufruf übergeben wird:
def area(width, height):
return width * height
result = area(3, 4)
Der Aufruf bindet width an 3 und height an 4. Die Multiplikation ergibt 12, und return gibt den Wert an den Aufrufer zurück. Die Ausführung wird bei der Zuweisung fortgesetzt, sodass result den Wert 12 erhält. Die Namen beschreiben eine Aufgabe, die mit anderen Argumenten wiederverwendet werden kann.
Ein Name hat einen Gültigkeitsbereich
Der Gültigkeitsbereich bestimmt, wo ein Name verwendet werden kann. Innerhalb einer Funktion erzeugte Namen sind gewöhnlich lokal für diesen Aufruf. Ein Name auf oberster Ebene ist innerhalb seines Moduls global. Getrennte Aufrufe haben getrennte lokale Bindungen, selbst wenn sie identische Namen verwenden.
count = 7
def reset():
count = 0
reset()
print(count)
Dies gibt 7 aus. Die Zuweisung innerhalb von reset erzeugt ein lokales count; sie ersetzt nicht die globale Bindung. Eine Funktion kann stattdessen ein Ergebnis zurückgeben und den Aufrufer dieses Ergebnis zuweisen lassen.
Gültigkeitsbereich und Lebensdauer beantworten unterschiedliche Fragen. Ein lokaler Name ist für Code außerhalb der Funktion nicht allein deshalb verfügbar, weil das Objekt, auf das er verwies, noch existiert. Zurückgegebene Objekte können weiterleben, und eine zurückgegebene verschachtelte Funktion kann den Zugriff auf Variablen ihres umschließenden Aufrufs behalten; diese erhaltene Umgebung ist Teil einer Closure.
Wichtige Erkenntnisse
- Werte den Ausdruck auf der rechten Seite aus, bevor du das Ziel einer Zuweisung aktualisierst.
- Vollziehe Sequenz, Verzweigung und Iteration in der tatsächlich ausgeführten Reihenfolge nach.
- Parameter sind Namen in der Definition; Argumente sind übergebene Werte.
- Ein Rückgabewert übermittelt ein Ergebnis an den Aufrufer.
- Lokale und globale Namen können getrennte Bindungen haben; der Gültigkeitsbereich unterscheidet sich von der Lebensdauer.
3Typen, Referenzen und Speicher
Werte brauchen einen Typ, einen Platz im Speicher und eine Lebensdauer. Diese Aspekte erklären, warum ein Text, der wie eine Zahl aussieht, Rechenoperationen zurückweisen kann, warum zwei Namen dieselbe veränderte Liste zeigen können und warum ungenutzte Daten manchmal Speicher belegt halten. Vergleiche die Prüfungen verschiedener Sprachen, verfolge einen Funktionsaufruf durch den Speicher und untersuche drei Arten, Speicher freizugeben. Jeder Ansatz verschiebt das Verhältnis von Kontrolle, Komfort und Sicherheit.
3.1 Typen bestimmen, welche Operationen sinnvoll sind
Ein Datentyp legt mögliche Werte und die darauf unterstützten Operationen fest. Eine Ganzzahl unterstützt ganzzahlige Arithmetik; ein Wahrheitswert steht für wahr oder falsch. Zeichenketten, Listen, Datensätze und Objekte verbinden oder ordnen andere Werte. Typen bestimmen auch mit, wie Werte im Speicher dargestellt werden, wobei die Größe eines Wertes vom Wert und von der Implementierung abhängen kann.
Wann werden Typen geprüft?
| Ansatz | Zeitpunkt der Prüfung | Beispiel |
|---|---|---|
| Statische Typisierung | Vor der Ausführung | Java weist int count = "five"; zurück |
| Dynamische Typisierung | Während der Ausführung | Python prüft, ob eine ausgeführte Operation zu ihren Operanden passt |
In Python ist count = 5 gefolgt von count = "five" gültig. Der Name verweist nun auf eine Zeichenkette. Die Auswertung von count + 1 löst dann einen TypeError aus. Bei dynamischer Typisierung haben Werte bestimmte Typen; sie schafft die Typen nicht ab.
Statische Typisierung verlangt keine sichtbare Typangabe an jeder Variablen. Typinferenz ermöglicht es einem Compiler, einen Typ aus dem umgebenden Code abzuleiten. Kotlin leitet für val n = 5 eine Ganzzahl ab, und Rust kann dies für let n = 5; tun. Pythons optionale Typhinweise sind anders: Separate Werkzeuge wie mypy können sie prüfen, aber die gewöhnliche Python-Ausführung erzwingt sie nicht.
Wann werden Werte umgewandelt?
Der Prüfzeitpunkt und die Strenge der Umwandlung sind getrennte Fragen. Python weist "3" + 4 zurück; die ausdrückliche Umwandlung in int("3") + 4 ergibt 7. JavaScript wendet für verschiedene Operatoren unterschiedliche Regeln zur automatischen Umwandlung an:
"3" + 4ergibt die Zeichenkette"34"."3" * 4ergibt die Zahl12."3" - 4ergibt die Zahl-1.
Die automatische Umwandlung heißt implizite Typumwandlung. Die Bezeichnungen „starke“ und „schwache“ Typisierung beschreiben informell die Strenge der Umwandlung; sie sind keine anderen Namen für statische und dynamische Typisierung. Python ist dynamisch und bei der obigen Addition gemischter Typen streng; C ist statisch und erlaubt dennoch viele implizite numerische Umwandlungen.
Pythons input() gibt eine Zeichenkette zurück. Gibt jemand „30“ ein, hat das Ergebnis weiterhin den Typ str. age + 1 schlägt fehl, während int(age) + 1 den Wert 31 ergibt, wenn age diesen Text enthält. Was auf dem Bildschirm erscheint, reicht nicht aus, um den Typ zu bestimmen.
Zahlen haben Darstellungsgrenzen
Eine vorzeichenbehaftete 32-Bit-Ganzzahl stellt Werte von −2.147.483.648 bis 2.147.483.647 dar. Javas int schlägt vom Maximum zum Minimum um, wenn 1 addiert wird. Ein vorzeichenbehafteter Überlauf in C ist undefiniertes Verhalten, sodass in C nicht von demselben Umschlagen ausgegangen werden darf. Ganzzahlen in Python wachsen, soweit der Speicher es erlaubt: 2 ** 100 ist genau 1.267.650.600.228.229.401.496.703.205.376.
Gleitkommazahlen stellen die meisten Dezimalbrüche binär nur näherungsweise dar. In Python und JavaScript ergibt 0.1 + 0.2 den Wert 0.30000000000000004. Ein direkter Vergleich dieses Ergebnisses mit 0.3 ergibt falsch.
Wenn eine Berechnung einen kleinen Fehler zulässt, vergleiche Werte mit einer für die Aufgabe gewählten Toleranz, zum Beispiel abs(a - b) < 1e-9. Eine so kleine absolute Toleranz eignet sich nur für manche Größenordnungen. Verwende für exakte Dezimalrechnungen im Rechnungswesen ganze kleinste Einheiten oder einen geeigneten Dezimaltyp. Eine Änderung des Vergleichs kann eine näherungsweise Darstellung nicht exakt machen.
Wichtige Erkenntnisse
- Ein Typ legt Werte und unterstützte Operationen fest.
- Statische und dynamische Typisierung beschreiben den Prüfzeitpunkt, während die implizite Typumwandlung die Umwandlungsregeln betrifft.
- Typinferenz unterstützt statische Typisierung; Python-Typhinweise benötigen separate Prüfwerkzeuge.
- Ganzzahlen mit fester Bitbreite können überlaufen; das Verhalten hängt von der Sprache ab.
- Rundung bei Gleitkommazahlen beeinflusst Gleichheitsprüfungen und die Wahl der Zahlendarstellung.
3.2 Verfolge Namen, Objekte und Funktionsaufrufe im Speicher
Der Hauptspeicher speichert die Daten eines laufenden Programms. Jedes Byte hat eine Adresse. Sprachen stellen Namen bereit, damit Programmierer mit Werten arbeiten können, ohne für jede Operation direkte Adressen zu schreiben.
Aufrufe nutzen Rahmen; Objekte haben eine Lebensdauer
In C belegt ein int gewöhnlich 4 Bytes, und &x liefert die Adresse von x. Höhere Programmiersprachen verbergen Adressen häufig hinter Referenzen. Wichtig ist die Unterscheidung zwischen einer Bindung, die ein Objekt benennt, und dem Objekt selbst.
Der Aufrufstapel verwaltet aktive Funktionsaufrufe. Jeder Aufruf hat einen Stapelrahmen mit Informationen wie Parametern, lokalen Bindungen und der Stelle, an der die Ausführung nach der Rückkehr fortgesetzt werden soll. Ruft main die Funktion area(3, 4) auf, wird ein area-Rahmen oberhalb des Rahmens des Aufrufers hinzugefügt. Die Funktion berechnet 12, kehrt zurück und ihr Rahmen wird entfernt. Bei gewöhnlichen verschachtelten Aufrufen kehrt der neueste Aufruf zuerst zurück.
Rekursion fügt für jede noch nicht abgeschlossene Ebene einen Aufruf hinzu. Häufen sich Aufrufe immer weiter an, kann der verfügbare Stapelspeicher erschöpft werden. Python löst normalerweise nach ungefähr 1.000 verschachtelten Aufrufen einen RecursionError aus, wobei die Grenze von seinen Einstellungen abhängt. Damit eine endliche rekursive Berechnung endet, muss eine Abbruchbedingung erreicht werden.
Der Heap enthält dynamisch allokierte Objekte, etwa eine wachsende Liste oder ein Objekt, das länger als die Funktion besteht, die es erzeugt hat. Bei der Speicherallokation wird Speicher reserviert. Eine zurückgegebene Liste kann auf dem Heap bleiben, nachdem der Aufruf, der sie erzeugt hat, beendet ist.
Das Kopieren einer Referenz teilt ein Objekt
Eine Referenz bezeichnet ein Objekt. Ein Zeiger in C oder C++ stellt ausdrücklich eine Adresse dar, über die auf Daten zugegriffen wird. Das Kopieren einer Referenz kopiert den Zugriff auf das Objekt, nicht alle Daten des Objekts.
a = [1, 2]
b = a
b.append(3)
Nach der ersten Zeile gibt es eine Liste. Die zweite Zeile bindet einen weiteren Namen an sie. Die letzte Zeile verändert diese Liste, sodass sowohl a als auch b den Wert [1, 2, 3] zeigen. Diese gemeinsame Nutzung heißt Aliasing. b = a.copy() würde eine separate äußere Liste erzeugen; bei dieser Liste von Ganzzahlen ließe das Anhängen an die neue Liste a unverändert.
Vergleiche Zuweisung und Veränderung:
x = 5
y = x
y = y + 1
Die Ganzzahl 5 ist unveränderlich. Die Addition ergibt 6, und nur y wird neu gebunden. x verweist weiterhin auf 5. Python-Zahlen, -Zeichenketten und -Tupel sind unveränderlich; Listen und Wörterbücher sind veränderlich. Der feste Inhalt eines Tupels kann eine Referenz auf eine veränderliche Liste enthalten, sodass die Unveränderlichkeit des äußeren Behälters nicht jedes verschachtelte Objekt unveränderlich macht.
Dieselben Regeln auf Argumente anwenden
Wenn Python eine Liste an eine Funktion übergibt, verweist der Parameter auf dasselbe Objekt. Der Aufruf von items.append(4) innerhalb der Funktion verändert diese Liste. Die Zuweisung items = [] bindet nur den lokalen Parameternamen an eine neue Liste. Der Name des Aufrufers bleibt an das ursprüngliche Objekt gebunden.
C übergibt Argumentwerte per Wertübergabe. Damit eine Funktion die Variable eines Aufrufers über deren Adresse aktualisieren kann, übergib einen Zeiger auf sie. Stelle dir beim Nachvollziehen in jeder Sprache zwei getrennte Fragen: „Auf welches Objekt verweist dieser Name?“ und „Ändert diese Operation das Objekt oder bindet sie einen Namen an etwas anderes?“
Wichtige Erkenntnisse
- Der Aufrufstapel verwaltet noch nicht abgeschlossene Aufrufe; der Heap enthält dynamisch allokierte Objekte.
- Ein Objekt kann länger bestehen als die Funktion, die es erzeugt hat.
- Das Kopieren einer Referenz ermöglicht die gemeinsame Nutzung eines Objekts; es vervielfältigt dessen Daten nicht.
- Eine Veränderung ändert ein Objekt, während eine erneute Bindung ändert, worauf ein Name verweist.
- Bei der Argumentübergabe gilt dieselbe Unterscheidung zwischen gemeinsam genutzten Objekten und lokalen Bindungen.
3.3 Gib Speicher frei, ohne noch benötigte Daten zu verlieren
Allokierter Speicher muss irgendwann wiederverwendbar werden. Die Herausforderung besteht darin, Daten nach ihrer letzten Verwendung freizugeben und zugleich jedes Objekt zu behalten, das das Programm noch benötigt. Sprachen weisen diese Verantwortung dem Programmierer, einem Laufzeitsystem oder vom Compiler geprüften Ownership-Regeln zu.
Manuelle Verwaltung und ihre Fehler
In C fordert malloc(n) n Bytes an und gibt bei Erfolg einen Zeiger auf den Block zurück. free(p) gibt einen durch Allokation erhaltenen Block frei. Der Programmierer muss dies zum richtigen Zeitpunkt und genau einmal tun. Auch C++ unterstützt manuelle Verwaltung, obwohl seine Standardwerkzeuge die Freigabe über die Lebensdauer von Objekten automatisieren können.
Verschiedene Fehler haben unterschiedliche Ursachen:
| Fehler | Was schiefging |
|---|---|
| Speicherleck | Nicht benötigter allokierter Speicher wird nicht freigegeben |
| Use-after-free | Ein Zeiger wird verwendet, nachdem der Speicher freigegeben wurde, auf den er verweist |
| Double Free | Derselbe allokierte Block wird zweimal freigegeben |
| Pufferüberlauf | Ein Schreibvorgang geht über die Schranken eines Speicherblocks hinaus |
Ein Zeiger, der weiterhin auf freigegebenen Speicher verweist, ist ein hängender Zeiger. Der Speicher könnte für etwas anderes wiederverwendet werden, sodass ein Zugriff darauf Daten beschädigen oder das Programm zum Absturz bringen kann.
Für ein C-Array int scores[10] sind die Positionen 0 bis 9 gültig. Das Schreiben auf scores[10] geht über das Array hinaus, und C bietet keine automatische Prüfung der Schranken. Python löst einen IndexError aus, wenn Index 10 für eine Liste mit zehn Elementen verwendet wird, und verhindert so, dass dieser Zugriff benachbarten Speicher überschreibt.
Erreichbarkeit verfolgen oder Referenzen zählen
Eine verfolgende automatische Speicherbereinigung beginnt bei noch aktiven Ausgangspunkten, etwa aktiven Referenzen, folgt Verbindungen zwischen Objekten und kann nicht erreichbare Objekte freigeben. Java, C#, Go und JavaScript verwenden verfolgende Verfahren. Die Bereinigung benötigt Arbeit zur Laufzeit und Speicher; je nach Verfahren kann sie Pausen verursachen.
Die Referenzzählung verfolgt die Referenzen auf ein Objekt. Für eine neue Python-Liste lautet die vereinfachte Abfolge: a = [1, 2] liefert eine Referenz; b = a liefert eine zweite; del a entfernt eine; b = None entfernt die andere. CPython gibt diese gewöhnliche Liste normalerweise frei, wenn ihre Anzahl null erreicht.
Eine Gruppe von Objekten kann die Referenzzahlen der jeweils anderen Objekte über null halten, selbst wenn kein aktiver Teil des Programms die Gruppe erreichen kann. Das ist ein Referenzzyklus. CPython ergänzt die Referenzzählung um eine Zyklenerkennung. Referenzzählung und Verfolgung sind beides Verfahren der automatischen Speicherverwaltung.
Ownership vor der Ausführung prüfen
Rust gibt jedem Wert einen Eigentümer und prüft Regeln für das Ausleihen von Referenzen. Endet die Ownership, gibt generierter Aufräumcode den Wert frei. Sicheres Rust weist Verwendungen von Referenzen zurück, die länger als ihr gültiges Ziel bestehen könnten, ohne eine verfolgende automatische Speicherbereinigung zu benötigen. Swift verwendet automatische Referenzzählung, die der Compiler einfügt.
Speichersicherheit ist auch über Abstürze hinaus wichtig. Microsoft berichtete 2019, dass ungefähr 70 % der Schwachstellen, denen das Unternehmen jährlich CVE-Kennungen zuwies, die Speichersicherheit betrafen; Chromium meldete für 2020 einen ähnlichen Anteil schwerwiegender Fehler. Im Dezember 2023 forderten CISA, NSA, FBI und internationale Partner Softwarehersteller auf, Übergänge zu speichersicheren Sprachen zu planen.
Automatische Speicherbereinigung behält weiterhin erreichbare Objekte. Eine ständig wachsende globale Liste kann jeden alten Datensatz am Leben halten, auch nachdem die Anwendung ihn nicht mehr verwendet. Eine automatische Speicherbereinigung kann nicht entscheiden, dass diese erreichbaren Datensätze für die Aufgabe unnötig geworden sind; das Programm muss aufhören, sie zu behalten.
Wichtige Erkenntnisse
- Manuelle Allokation erfordert eine Freigabe zum richtigen Zeitpunkt.
- Speicherlecks, Use-after-free, Double Free und Pufferüberläufe haben unterschiedliche Ursachen.
- Verfolgende Speicherbereinigung findet nicht erreichbare Objekte; Referenzzählung reagiert auf wegfallende Referenzen.
- CPython ergänzt die Referenzzählung um eine Zyklenerkennung.
- Ownership kann die Freigabe ohne verfolgende Speicherbereinigung ermöglichen.
- Automatische Speicherbereinigung kann Objekte nicht freigeben, die das Programm weiterhin behält.
4Veränderlichen Zustand und zusammenarbeitende Objekte strukturieren
Eine Möglichkeit, ein Programm zu beschreiben, sind Befehle, die Werte aktualisieren. Eine andere fasst diese Werte mit den Operationen zusammen, die sie steuern. Beide Sichtweisen wirken oft zusammen: Eine Objektmethode kann eine gewöhnliche Schleife enthalten. Verfolge zuerst den veränderlichen Zustand und erstelle dann ein kleines Kontomodell. So siehst du, wie Klassen Regeln schützen und wie Vererbung, Polymorphie und Komposition verwandten Objekten eine gemeinsame Schnittstelle ermöglichen, ohne ihnen die falschen Zuständigkeiten zuzuweisen.
4.1 Befehle, Prozeduren und gemeinsamer Zustand
Ein Programmierparadigma ist eine Art, Code zu strukturieren und über Berechnungen nachzudenken. Bei der imperativen Programmierung gibt ein Programm Befehle, die seinen Zustand ändern: die Werte, die Variablen und Objekte aktuell enthalten.
Zustandsänderungen sichtbar machen
Eine laufende Summe ist eine einfache imperative Berechnung:
total = 0
for n in [3, 8, 5]:
total = total + n
Der Anfangszustand ist total = 0. Die Addition von 3 ändert den Wert auf 3. Die Addition von 8 ändert ihn auf 11. Die Addition von 5 ändert ihn auf 16. Jede Zuweisung nutzt den Zustand, den der vorherige Schritt hinterlassen hat, sodass die Reihenfolge Teil des Verfahrens ist.
Dieser Stil entspricht den schrittweisen Änderungen, die der Prozessor an Registern und Speicher vornimmt. Er passt auch zu Algorithmen, die eine Sammlung gezielt an Ort und Stelle aktualisieren, etwa einer Sortierroutine, die Elemente vertauscht.
Aufgaben Namen geben
Prozedurale Programmierung gliedert imperativen Code in Funktionen oder Prozeduren. Ein Berichtsprogramm könnte read_scores(), average(scores) und print_report(avg) aufrufen. Die Namen lassen Lesende die Gesamtaufgabe erkennen, ohne sofort jeder internen Zeile folgen zu müssen. Jede Prozedur kann getrennt verstanden und getestet werden.
C, Pascal, Fortran und COBOL sind klassische prozedurale Sprachen. Auch Python unterstützt diesen Stil: Ein Skript kann benannte Funktionen verwenden, ohne Klassen zu definieren.
Strukturierte Programmierung ordnet den Kontrollfluss mit Sequenz, Verzweigung, Schleifen und Prozeduraufrufen. Sie vermeidet beliebige Sprünge, die die nächste ausgeführte Anweisung schwer vorhersehbar machen. Edsger Dijkstras Brief „Go To Statement Considered Harmful“ von 1968 trug zur Verbreitung dieses Ansatzes bei. Kontrollierte Ausstiege wie break, continue und return bleiben nützlich: Sie verlassen einen bestimmten Schleifendurchlauf, eine Schleife oder eine Funktion.
Wirkungen können über einen Rückgabewert hinausgehen
Ein Seiteneffekt verändert etwas über das von einer Funktion zurückgegebene Ergebnis hinaus. Eine Ausgabe, das Speichern einer Datei, das Senden einer Netzwerknachricht und das Ändern gemeinsam genutzter Daten sind Seiteneffekte. Programme benötigen solche Aktionen, um mit der Außenwelt zu interagieren.
Probleme entstehen, wenn nicht zusammengehöriger Code denselben Zustand unbemerkt verändert. Angenommen, apply_discount() senkt einen globalen price um 10 %, und add_shipping() addiert 5. Ausgehend von 100 ergibt erst der Rabatt 90 und anschließend der Versand 95. Zuerst der Versand ergibt 105 und anschließend der Rabatt 94,5. Beide Prozeduren führen ihre eigenen Schritte korrekt aus, aber das gemeinsame Ergebnis hängt von ihrer Reihenfolge ab.
Die Übergabe von Werten und die Rückgabe neuer Ergebnisse können diese Reihenfolge sichtbar machen:
price = add_shipping(apply_discount(100))
Der innere Aufruf läuft zuerst: Rabatt, dann Versand. Eine andere Verschachtelung drückt eine andere Regel aus. Ein klarer Datenfluss hilft zu erkennen, ob auch auf den Versand der Rabatt angewendet werden soll.
| Strukturierung | Hauptfrage beim Lesen |
|---|---|
| Imperative Befehle | Welcher Wert ändert sich als Nächstes? |
| Benannte Prozeduren | Welche Aufgabe erfüllt dieser Aufruf? |
| Gemeinsame globale Aktualisierungen | Welche frühere Aktion hat diesen Wert geändert? |
Imperativer Code ist innerhalb moderner Objektmethoden und Ereignisbehandlungsroutinen weiterhin verbreitet. Eine übergeordnete objektorientierte oder funktionale Gestaltung verhindert nicht, dass ein kleiner Teil direkte Befehle verwendet, wenn diese Schritte den Algorithmus verständlich machen.
Wichtige Erkenntnisse
- Imperativer Code beschreibt Schritte, die gespeicherten Zustand ändern.
- Prozeduren geben zusammengehörigen Aufgaben Namen und Grenzen.
- Strukturierter Kontrollfluss macht Ausführungswege leichter nachvollziehbar.
- Seiteneffekte umfassen Änderungen gemeinsam genutzter Daten und Interaktionen mit der Außenwelt.
- Gemeinsame Aktualisierungen können Ergebnisse von der Aufrufreihenfolge abhängig machen.
4.2 Klassen halten Daten und ihre Regeln zusammen
Objektorientierte Programmierung (OOP) modelliert ein Programm als zusammenarbeitende Objekte. Jedes Objekt verbindet Daten mit Operationen auf diesen Daten. Ein Konto kann seinen Inhaber und Kontostand speichern und gleichzeitig Ein- und Auszahlungsoperationen anbieten.
Den Bauplan von seinen Instanzen unterscheiden
Eine Klasse legt die Attribute und Methoden fest, die ihre Objekte bereitstellen. Ein Objekt oder eine Instanz ist ein einzelnes konkretes Exemplar dieser Klasse. BankAccount ist der Bauplan; das Konto von Ana mit 150 und das Konto von Ben mit 40 sind verschiedene Instanzen. Ihre Kontostände können sich unabhängig voneinander ändern.
Ein Attribut speichert Daten, die zu einem Objekt gehören. Eine Methode ist eine Operation, die über ein Objekt aufgerufen wird, etwa acct.deposit(50). self in einer Python-Instanzmethode verweist auf das Objekt, für das der Aufruf erfolgt.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self._balance = self._balance + amount
Der Aufruf BankAccount("Ana", 100) erzeugt eine neue Instanz. Ihre Methode __init__ initialisiert die Daten der Instanz. Ein Konstruktor richtet ein neues Objekt ein; Python trennt die Erzeugung von diesem Initialisierungsschritt. Der Aufruf acct.deposit(50) für dieses Konto ändert seinen Kontostand von 100 auf 150.
Die Regel an der Stelle der Aktualisierung platzieren
Datenkapselung bringt Daten und die Operationen zusammen, die ihre Regeln einhalten. Angenommen, ein Konto darf nie einen negativen Kontostand haben. Die Auszahlungsmethode prüft die vorgeschlagene Änderung, bevor sie sie ausführt:
def withdraw(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance = self._balance - amount
Diese Methode gehört in BankAccount. Bei einem Kontostand von 150 löst withdraw(200) einen Fehler aus und lässt 150 übrig. withdraw(30) gelingt und lässt 120 übrig. Der fehlgeschlagene Aufruf zieht nicht zuerst ab und entdeckt dann das Problem; die Prüfung erfolgt vor der Aktualisierung.
Anderer Code sollte diese Methoden verwenden, statt einen ungeprüften Kontostand zuzuweisen. Java und C# können den direkten Zugriff mit private einschränken. Pythons vorangestellter Unterstrich in _balance signalisiert gemäß einer Konvention ein internes Detail; er erzwingt keinen privaten Zugriff.
Das Geheimnisprinzip geht weiter: Aufrufer hängen von den Operationen ab, die eine Klasse anbietet, ohne ihre interne Darstellung kennen zu müssen. Der Kontostand könnte später anders gespeichert werden, während die öffentlichen Ein- und Auszahlungsoperationen ihre Bedeutung behalten.
Das Problem als Hinweis auf Zuständigkeiten nutzen
Ein Bibliothekssystem hat Bücher, Mitglieder und Ausleihen. Diese Nomen legen mögliche Klassen oder Attribute nahe; Aktionen wie Ausleihen und Verlängern legen Methoden nahe. Wähle für jede Klasse eine klare Zuständigkeit. Eine Kontoklasse, die außerdem jede E-Mail versendet und jeden Bericht formatiert, lässt sich schwerer ändern, weil nicht zusammengehörige Aufgaben vermischt werden.
Klassen und Objekte erschienen 1967 in Simula 67; Smalltalk trug in den 1970er-Jahren zur Verbreitung des Ansatzes bei. Java, C#, C++ und Python unterstützen OOP, aber die Verwendung einer dieser Sprachen zwingt nicht jedes Programm in Klassen. Die hilfreiche Entwurfsfrage lautet, welche Daten und Regeln zusammengehören.
Wichtige Erkenntnisse
- Eine Klasse legt Attribute und Methoden fest; jede Instanz hat ihre eigenen Instanzdaten.
- Methoden wirken auf das Objekt, für das sie aufgerufen werden.
- Datenkapselung hält Prüfung und Zustandsänderungen zusammen.
- Unterstriche kennzeichnen in Python gemäß einer Konvention interne Attribute; manche Sprachen erzwingen privaten Zugriff.
- Das Geheimnisprinzip ermöglicht es Aufrufern, von den öffentlichen Operationen statt von den Interna der Klasse abzuhängen.
- Eine Klasse sollte eine zusammenhängende Zuständigkeit haben.
4.3 Verhalten spezialisieren und Objekte kombinieren
Objekte müssen oft gemeinsame Operationen anbieten und sich zugleich in Einzelheiten unterscheiden. Vererbung, Polymorphie und Komposition lösen unterschiedliche Teile dieses Entwurfsproblems.
Eine bestehende Art von Objekt spezialisieren
Vererbung definiert eine Unterklasse aus einer Oberklasse. In Python gibt class SavingsAccount(BankAccount): der spezialisierten Klasse Zugriff auf das geerbte Kontoverhalten. Sie kann add_interest(rate) hinzufügen: Ein Kontostand von 1.000 wird durch eine Zinsgutschrift von 2 % zu 1.020.
Die beabsichtigte Beziehung ist eine Ist-ein-Beziehung. Ein Sparkonto ist weiterhin ein Bankkonto und muss die für Bankkonten erwarteten Regeln einhalten. Die Wiederverwendung einiger Zeilen reicht nicht aus, um zu behaupten, eine Art von Objekt sei eine andere.
Eine Unterklasse kann eine Methode mit ihrer eigenen Implementierung überschreiben. Angenommen, ein Girokonto berechnet für jede erfolgreiche Abhebung eine Gebühr von 1. Bei einem Kontostand von 100 kostet die Abhebung von 30 insgesamt 31 und lässt 69 übrig. Um die Kontoregel einzuhalten, muss die Methode die Abhebung zuzüglich der Gebühr prüfen, bevor sie den Kontostand ändert. Übersteigen diese Gesamtkosten den Kontostand, verweigert sie den gesamten Vorgang.
Einen Aufruf für unterschiedliche Objekte nutzbar machen
Polymorphie ermöglicht, dass dieselbe Operation für unterschiedliche Objekttypen funktioniert. Ein Kreis und ein Rechteck können beide area() anbieten:
for shape in shapes:
print(shape.area())
Die Schleife benötigt kein if für jede Formklasse. Jedes Objekt stellt seine eigene Implementierung bereit. Die Auswahl dieser Implementierung während der Ausführung heißt dynamische Methodenbindung.
Für Circle(2) beträgt die Fläche ungefähr 12,57 Quadrateinheiten. Für Rectangle(3, 4) beträgt sie 12 Quadrateinheiten. Ihre gemeinsame Fläche beträgt ungefähr 24,57 Quadrateinheiten. Ein späteres Triangle kann der Sammlung beitreten, indem es eine kompatible Operation area() bereitstellt; die Schleife selbst benötigt keinen weiteren Zweig.
Eine Schnittstelle legt die Operationen fest, die ein Typ anbieten muss. Javas interface Shape { double area(); } verlangt von implementierenden Klassen, die Flächenmethode bereitzustellen. Eine abstrakte Klasse kann einen Teil der gemeinsamen Implementierung vorgeben und andere Methoden den Unterklassen überlassen. Python verwendet oft Duck-Typing: Ein Objekt kann in der Schleife funktionieren, wenn es die benötigte Operation bereitstellt, auch ohne gemeinsame Oberklasse.
Einen Teil enthalten, statt zu ihm zu werden
Komposition speichert andere Objekte als Teile. Ein Auto hat einen Motor und Räder. Es kann die Methode start() seines Motors aufrufen, ohne von Engine zu erben.
| Beziehung | Geeigneter Entwurf | Grund |
|---|---|---|
| Sparkonto ist ein Konto | Vererbung | Das erwartete Verhalten eines Kontos bleibt erhalten |
| Auto hat einen Motor | Komposition | Der Motor ist ein Bestandteil, nicht die Kategorie des Autos |
| Verschiedene Formen bieten eine Flächenberechnung | Gemeinsame Operation oder Schnittstelle | Der Aufrufer kann die Operation einheitlich anfordern |
Tiefe Vererbungsketten können die Auswirkungen einer Änderung der Oberklasse über viele Klassen verbreiten. Eine Unterklasse, die geerbtes Verhalten deaktivieren muss, kann auf eine ungeeignete Ist-ein-Beziehung hinweisen. Komposition hält solche Abhängigkeiten oft kleiner und übersichtlicher.
Die vier häufig genannten Säulen der OOP passen hier zusammen: Datenkapselung schützt Daten durch Operationen, Abstraktion verbirgt Implementierungsdetails, Vererbung spezialisiert einen Typ und Polymorphie ermöglicht einem Aufrufer, eine gemeinsame Operation für verschiedene Implementierungen zu nutzen.
Wichtige Erkenntnisse
- Vererbung modelliert eine Ist-ein-Beziehung und muss erwartetes Verhalten erhalten.
- Überschreiben ersetzt eine Methodenimplementierung in einer spezialisierten Klasse.
- Polymorphie ermöglicht einer Operation, verschiedene Implementierungen aufzurufen.
- Eine Schnittstelle legt erforderliche Operationen fest; Duck-Typing beruht auf den Operationen, die ein Objekt bereitstellt.
- Komposition modelliert Objekte, die andere Objekte als Teile enthalten.
5Neue Werte berechnen und gewünschte Ergebnisse beschreiben
Ein Programm kann sich auf die Umwandlung von Werten oder auf das Ergebnis konzentrieren, das eine Ausführungsumgebung erzeugen soll. Reine Funktionen machen Abhängigkeiten sichtbar, während unveränderliche Daten Änderungen über gemeinsam genutzte Referenzen vermeiden. Deklarative Abfragen lassen eine spezialisierte Ausführungsumgebung die Ausführungsschritte wählen. Verfolge eine Datenverarbeitungskette und eine Datenbankabfrage, um zu sehen, was diese Ansätze erleichtern und wo Eingabe, Ausgabe und konkrete Maschinenbefehle weiterhin eine Rolle spielen.
5.1 Reine Funktionen und Datenverarbeitungsketten
Funktionale Programmierung erzeugt Ergebnisse durch die Kombination von Funktionen. Ein zentraler Grundsatz ist, aus Eingaben neue Werte zu erzeugen und dabei gemeinsame Veränderungen zu begrenzen. Lisp führte um 1958 weitverbreitete funktionale Ideen ein; Haskell, Erlang, Clojure, F# und OCaml entwickeln diese Ideen auf unterschiedliche Weise weiter.
Erkennen, was eine Funktion rein macht
Eine reine Funktion liefert für dieselben Argumente dasselbe Ergebnis und hat keine Seiteneffekte. def with_tax(price): return price * 1.2 veranschaulicht die Idee: with_tax(50) ergibt 60,0. Eine Funktion, die außerdem etwas an eine globale Liste anhängt oder eine Nachricht ausgibt, hat eine Wirkung über ihren Rückgabewert hinaus. Ein Ergebnis, das von der aktuellen Uhrzeit abhängt, hängt ebenfalls von mehr als den Argumenten ab.
Ein reiner Aufruf kann durch seinen Rückgabewert ersetzt werden, ohne das Programmverhalten zu ändern. Diese Eigenschaft heißt referenzielle Transparenz. Sie erleichtert isolierte Tests und erlaubt es, unabhängige Aufrufe parallel auszuführen, ohne dass sie um die Aktualisierung eines gemeinsamen Zustands konkurrieren.
Unveränderlichkeit hält einen bestehenden Wert unverändert. [n + 1 for n in nums] erzeugt eine neue Liste und lässt nums unberührt. Ein Programm kann veränderliche Informationen weiterhin darstellen, indem es neue Versionen erzeugt; es muss die alte Version nicht überschreiben.
Verhalten als Daten übergeben
Funktionen sind Funktionen erster Klasse, wenn sie Namen zugewiesen, als Argumente übergeben und als Werte zurückgegeben werden können. Eine Funktion höherer Ordnung nimmt eine Funktion entgegen oder gibt eine zurück. Ein Lambda-Ausdruck ist ein unbenannter Funktionsausdruck, etwa lambda n: n * n.
Drei verbreitete Operationen höherer Ordnung bilden eine Verarbeitungskette:
filterbehält Elemente, die einen Test erfüllen.mapwandelt jedes verbleibende Element um.reducefasst Elemente mithilfe eines Akkumulators zu einem Ergebnis zusammen.
Um die Quadrate der geraden Zahlen von 1 bis 6 zu summieren, bleiben nach dem Filtern 2, 4 und 6; das Quadrieren ergibt 4, 16 und 36; die Summe ist 56.
sum(map(lambda n: n * n,
filter(lambda n: n % 2 == 0, range(1, 7))))
Hier prüft % 2 == 0, ob eine Zahl gerade ist, und range(1, 7) liefert 1 bis 6. Haskell drückt dieselbe Berechnung als sum (map (^2) (filter even [1..6])) aus.
Wird reduce aus Pythons functools importiert, kombiniert reduce(lambda acc, n: acc + n, [3, 8, 5], 0) zunächst 0 mit 3 zu 3, dann mit 8 zu 11 und schließlich mit 5 zu 16. Die Verarbeitungskette beschreibt Umwandlungen ohne eine umgebende Schleife, die eine gemeinsam genutzte Summe aktualisiert.
Eine Umgebung erhalten oder ein kleineres Problem aufrufen
def multiplier(k): return lambda x: k * x gibt eine Funktion zurück, die den Zugriff auf k behält. Daher gibt multiplier(3)(10) den Wert 30 zurück, auch nachdem der äußere Aufruf beendet ist. Die zurückgegebene Funktion und die erhaltene Umgebung bilden eine Closure.
Bei der Rekursion ruft sich eine Funktion für ein kleineres Problem selbst auf. Die Fakultät gibt bei n == 0 den Wert 1 zurück; andernfalls gibt sie n * factorial(n - 1) zurück. Für 5 ergeben die Produkte 120. Eine endliche Berechnung benötigt einen erreichbaren Basisfall. Nicht abgeschlossene verschachtelte Aufrufe lösen in Python schließlich einen RecursionError aus; Sprachen mit echter Endaufrufoptimierung können für Aufrufe an letzter Stelle Stapelspeicher wiederverwenden.
Die bloße Verwendung von Funktionen macht Code nicht funktional: Prozedurale Funktionen können den Zustand verändern. Tatsächliche funktionale Programme lesen weiterhin Eingaben und schreiben Ausgaben; häufig halten sie Effekte nahe an den Rändern des Programms und die zentralen Berechnungen rein.
Wichtige Erkenntnisse
- Reine Funktionen hängen nur von ihren Argumenten ab und haben keine Seiteneffekte.
- Aktualisierungen unveränderlicher Daten erzeugen neue Werte.
- Funktionen erster Klasse können übergeben und zurückgegeben werden; Funktionen höherer Ordnung nehmen Funktionen entgegen oder geben sie zurück.
- Filter wählt aus, Map wandelt um und Reduce kombiniert.
- Eine Closure behält ihre Entstehungsumgebung.
- Eine endliche rekursive Berechnung benötigt einen Basisfall, den sie tatsächlich erreicht.
5.2 Ein Ergebnis angeben und eine Ausführungsumgebung es finden lassen
Deklarative Programmierung beschreibt das gewünschte Ergebnis und überlässt die Ausführungsschritte einer Ausführungsumgebung der Sprache. Funktionaler Code wird gewöhnlich neben Abfragesprachen und logischer Programmierung unter diese allgemeine Idee gefasst.
Eine Abfrage als Anfrage lesen
Eine relationale Datenbank speichert Daten in Tabellen. Angenommen, students enthält die Zeilen mit Namen und Note Ana 92, Ben 75, Chen 80 und Dara 64:
SELECT name
FROM students
WHERE grade >= 80
ORDER BY name;
FROM nennt die Tabelle, WHERE gibt die Bedingung an, SELECT wählt das zurückgegebene Feld und ORDER BY verlangt alphabetische Reihenfolge. Ana und Chen erfüllen die Bedingung, einschließlich der Note von Chen, die genau 80 beträgt. Das Ergebnis sind ihre Namen in dieser Reihenfolge.
Ein imperatives Programm würde eine Ergebnisliste erstellen, Zeilen durchlaufen, jede Note prüfen, passende Namen anhängen und die Liste sortieren. SQL gibt der Datenbank das geforderte Ergebnis vor, ohne diese Schleife zu schreiben.
Ein Anfrageoptimierer wählt einen Ausführungsplan. Er könnte die Tabelle durchlaufen oder einen verfügbaren Index nutzen, eine Nachschlagestruktur, die geeignete Suchen beschleunigen kann. Viele Indizes verwenden sortierte Strukturen; andere nutzen andere Anordnungen. Das Hinzufügen eines Index kann den Plan ändern, ohne die Abfrage zu ändern. Mehr Daten bedeuten weiterhin Arbeit: Die Angabe des Ergebnisses garantiert nicht, dass jede Abfrage wenig Aufwand verursacht.
Fragen, was aus Fakten und Regeln folgt
Logische Programmierung beschreibt Fakten und Beziehungen und sucht dann nach Werten, die eine Frage erfüllen. In Prolog:
parent(tom, bob).
parent(bob, ann).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
Die Regel bedeutet, dass X ein Großelternteil von Z ist, wenn es ein Y gibt, für das beide Elternbeziehungen gelten. Die Abfrage ?- grandparent(tom, Who). findet Who = ann: Tom ist ein Elternteil von Bob und Bob ein Elternteil von Ann. Großgeschriebene Namen stehen für Variablen; die kleingeschriebenen Namen bezeichnen bestimmte Personen.
Deklarative Werkzeuge in deiner Umgebung erkennen
Eine Tabellenkalkulationsformel =SUM(B2:B5) gibt eine Abhängigkeit an. Die Tabellenkalkulation berechnet sie neu, wenn sich relevante Zellen ändern. Der reguläre Ausdruck [0-9]{3}-[0-9]{4} beschreibt drei Ziffern, einen Bindestrich und vier Ziffern, wie in „555-0199“. Eine Suchfunktion für Textmuster führt die Suche aus.
CSS legt das Aussehen mit einer Regel wie h1 { color: navy; } fest; HTML zeichnet die Seitenstruktur aus. Es sind deklarative Notationen, die gewöhnlich als Stylesheet- und Auszeichnungssprachen eingeordnet werden. Ihre Aufgabe unterscheidet sich von der einer Allzwecksprache, die ein breites Spektrum von Berechnungen ausdrücken kann.
| Art der Sprache | Schwerpunkt | Beispiele |
|---|---|---|
| Domänenspezifische Sprache | Eine bestimmte Art von Aufgabe | SQL, reguläre Ausdrücke, CSS |
| Allzwecksprache | Viele Arten von Programmen | Python, Java, Haskell |
Deklarativer Code kann nahe an einer Problemstellung bleiben und einer Ausführungsumgebung ermöglichen, ihr Verfahren zu verbessern. Er gibt dem Programmierer auch weniger direkte Kontrolle über einzelne Schritte, wodurch sich ein langsamer Plan schwerer untersuchen lässt. Darunter werden weiterhin konkrete Befehle ausgeführt: Deklarativ beschreibt, wie der Programmierer die Aufgabe ausdrückt, nicht einen Prozessor, der ohne Schritte arbeitet.
Wichtige Erkenntnisse
- Deklarativer Code gibt ein gewünschtes Ergebnis an; eine Ausführungsumgebung wählt die Ausführungsschritte.
- SQL wählt Zeilen und Felder anhand von Bedingungen und einer verlangten Reihenfolge aus.
- Ein Anfrageoptimierer kann einen Abfrageplan ändern, ohne die Abfrage zu ändern.
- Logische Programmierung leitet Antworten aus Fakten und Regeln ab.
- Domänenspezifische Sprachen sind auf bestimmte Aufgaben ausgerichtet; Allzwecksprachen unterstützen viele Arten von Programmen.
6Einen Stil und eine Sprache für die Aufgabe wählen
Unterschiedliche Programmierstile können dieselbe Antwort liefern und die Arbeit dabei unterschiedlich strukturieren. Ein Vergleich anhand einer Aufgabe macht ihre Vor- und Nachteile leichter erkennbar. Bei der Sprachwahl kommen praktische Rahmenbedingungen hinzu: die Zielplattform, verfügbare Bibliotheken, Speicher- und Geschwindigkeitsvorgaben, Sicherheitsanforderungen und die Menschen, die den Code warten. Ein Projekt mit mehreren Teilen kann für jeden Teil eine andere Sprache und eine andere Mischung von Stilen verwenden.
6.1 Eine Aufgabe in mehreren Programmierstilen
Ein Paradigma ändert, wie eine Lösung ausgedrückt wird, nicht welches numerische Ergebnis eine korrekte Lösung liefern soll. Betrachte Bestellungen mit den Beträgen [120, 45, 300, 80]: Gesucht ist die Summe der Beträge, die echt größer als 100 sind.
Zustandsänderungen verfolgen
Die imperative Version zeigt die Schleife und die Akkumulatorvariable:
total = 0
for amount in amounts:
if amount > 100:
total = total + amount
Der erste Betrag ändert total auf 120. Der zweite lässt den Wert unverändert. Der dritte ändert ihn auf 420, und der vierte lässt ihn dort. Das Programm macht die Reihenfolge von Prüfung und Aktualisierung sichtbar.
Eine Sammlung umwandeln
Ein kompakter Python-Ausdruck im funktionalen Stil ist sum(a for a in amounts if a > 100). Er wählt 120 und 300 aus und fasst sie zu 420 zusammen. Eine andere Version ist sum(filter(lambda a: a > 100, amounts)). Hier beschreibt das Prädikat die Zugehörigkeit, und sum kombiniert die ausgewählten Werte.
Ein Generatorausdruck oder filter-Aufruf nutzt Pythons Multiparadigmenfunktionen; sein Vorkommen macht nicht jeden umgebenden Teil der Anwendung rein.
Regeln bei Objekten unterbringen
In einem objektorientierten Entwurf speichert ein Order ein amount und bietet is_large() an, das self.amount > 100 zurückgibt. Ein OrderBook enthält Bestellobjekte und bietet large_total() an. Diese Methode kann Bestellungen durchlaufen, bei jeder prüfen, ob sie die Bedingung erfüllt, und passende Beträge addieren.
Die Summe bleibt 420. Der Unterschied liegt darin, wo die Zuständigkeiten liegen: Die Regel für eine große Bestellung gehört zu Order, und die Arbeit auf Ebene der Sammlung gehört zu OrderBook. Andere Funktionen können die Regel wiederverwenden, ohne ihre Schwellenwertprüfung zu kopieren.
Die Datenbank fragen
Liegen die Daten bereits in einer relationalen Datenbank, kann SQL die Summe anfordern:
SELECT SUM(amount)
FROM orders
WHERE amount > 100;
Die Datenbank wählt den Plan für Abruf und Aggregation. Die Abfrage weist keinen Aufrufer an, jede Zeile zu durchlaufen oder eine lokale Akkumulatorvariable zu führen.
| Stil | Was das Beispiel hervorhebt |
|---|---|
| Imperativ | Geordnete Prüfungen und Aktualisierungen einer Summe |
| Objektorientiert | Bestellungen und Sammlungsobjekte mit festgelegten Zuständigkeiten |
| Funktional | Auswahl und Kombination von Werten |
| Deklaratives SQL | Das geforderte Ergebnis über gespeicherte Daten |
Stile dort kombinieren, wo sie helfen
Python unterstützt prozedurale, objektorientierte und funktionale Techniken. JavaScript bietet Objekte, Funktionen erster Klasse und Closures. Auch C++, C#, Kotlin, Scala, Swift und Rust mischen Stile. Java wird stark mit OOP verbunden, unterstützt aber seit Java 8 im Jahr 2014 Lambda-Ausdrücke; Haskell ist rein funktional.
Eine Webanwendung kann Klassen für Benutzer und Bestellungen, funktionale Umwandlungen für Berichtsdaten, imperative Aktualisierungen innerhalb von Methoden und SQL für Datenbankabfragen verwenden. Diese Entscheidungen wirken auf verschiedenen Ebenen.
Ereignisgesteuerte Programmierung ergänzt eine weitere Organisationsidee: Registriere eine Behandlungsroutine, die läuft, wenn ein Ereignis eintrifft. In JavaScript übergibt button.addEventListener("click", showMenu) die Funktion showMenu als Funktionswert. Ein späterer Klick ruft sie auf. Die Behandlungsroutine kann selbst Objekte, reine Umwandlungen oder imperative Schritte verwenden. Ihre ereignisgesteuerte Rolle sagt aus, wann sie läuft, legt aber nicht den einzigen möglichen Stil für ihren Rumpf fest.
Wichtige Erkenntnisse
- Korrekte Lösungen in verschiedenen Paradigmen können dasselbe Ergebnis berechnen.
- Ein Stil verändert, wo Schritte, Zustand, Regeln und Zuständigkeiten auftreten.
- Moderne Sprachen unterstützen gewöhnlich mehrere Paradigmen.
- Eine Ereignisbehandlungsroutine läuft als Reaktion auf ein Ereignis und kann intern andere Stile verwenden.
6.2 Unter Nebenbedingungen und Zielkonflikten wählen
Keine Programmiersprache passt für jedes Projekt am besten. Allzwecksprachen sind gewöhnlich Turing-vollständig: Mit unbegrenzter Zeit und unbegrenztem Speicher können sie dieselbe Klasse von Berechnungen ausdrücken. Die tatsächliche Wahl hängt davon ab, ob eine Lösung zur jeweiligen Plattform, den Ressourcen, Werkzeugen und Menschen passt.
Mit dem beginnen, was funktionieren muss
Die Plattform ist oft eine feste Nebenbedingung. Browser führen JavaScript aus und können kompilierte Module über WebAssembly ausführen. TypeScript ergänzt statische Prüfungen und wird zu JavaScript kompiliert. Swift ist eine Standardwahl für Apps auf Apple-Plattformen; Kotlin wird für Android empfohlen. SQL ist die übliche Sprache für relationale Abfragen.
Betrachte als Nächstes das Anwendungsgebiet und das Ökosystem. Python hat weitverbreitete Bibliotheken für Datenverarbeitung und maschinelles Lernen wie NumPy und pandas. R konzentriert sich auf Statistik. C und C++ sind bei Betriebssystemen, Spiele-Engines und eingebettetem Code verbreitet. Java und C# kommen häufig in großen Geschäftssystemen vor; Bash ist nützlich, um Betriebssystemaufgaben zu automatisieren.
Dies sind praktische Zuordnungen, keine exklusiven Einsatzgebiete. Eine geeignete Bibliothek kann weit mehr Entwicklungsarbeit sparen als ein kleiner Unterschied in der Syntax der Sprache.
Ressourcen- und Sicherheitsanforderungen untersuchen
Ein Mikrocontroller mit nur einigen Kilobyte Arbeitsspeicher benötigt möglicherweise genaue Kontrolle über den Speicher und den zusätzlichen Laufzeitaufwand. C, C++ oder Rust können geeignet sein, wenn die Zielplattform sie unterstützt. Auch anspruchsvolle Komponenten von Spielen oder Datenbanken sprechen oft für diese Sprachen, aber Ausführungsumgebungen und Ressourcenbudgets unterscheiden sich: Unity verwendet C#, und einige Datenbanken verwenden Java oder Go.
Bevor du nach Geschwindigkeit auswählst, finde den Engpass: den Teil, der die Leistung begrenzt. Ein Programm, das hauptsächlich auf eine Netzwerkantwort wartet, wird nicht allein deshalb erheblich schneller, weil eine kleine lokale Schleife effizienter kompiliert wird. Miss repräsentative Arbeitslasten auf der Zielplattform.
Speichersicherheit verringert Fehler wie ungültige Speicherzugriffe. Sicheres Rust prüft Ownership-Regeln zur Übersetzungszeit; verwaltete Sprachen wie Java, C#, Go und Python bieten Speicherverwaltung zur Laufzeit. Statische Typisierung erkennt viele unverträgliche Operationen vor der Ausführung, was bei einer großen Codebasis helfen kann. Weder Speichersicherheit noch statische Typisierung beweist, dass die Logik des Programms korrekt ist.
Die Kosten für die Wartung des Ergebnisses einbeziehen
Das Wissen des Teams, verfügbare Entwickler, Programme zur Fehlerbehebung, Testwerkzeuge, Bibliotheken, Lizenzen und vorhandener Code beeinflussen die Entscheidung. Eine vertraute, unterstützte Sprache kann die Kosten für Bereitstellung und Wartung senken. Ein funktionierendes System neu zu schreiben verursacht Migrationsarbeit und Risiken, selbst wenn die neue Sprache attraktive Eigenschaften hat.
Bei einem städtischen Wetternetz haben verschiedene Komponenten unterschiedliche Anforderungen:
| Komponente | Vernünftige Sprachwahl | Grund |
|---|---|---|
| Firmware kleiner Sensoren | C oder Rust | Kontrolle über den Speicher und unterstützte Hardwareziele |
| Gespeicherte Messwerte | SQL | Filtern und Aggregieren in der Datenbank |
| Bereinigung und Analyse | Python | Bibliotheken zur Datenverarbeitung |
| Interaktive Anzeige im Browser | JavaScript oder TypeScript | Browserunterstützung und Werkzeuge für Oberflächen |
Eine nützliche Begründung verknüpft jede Wahl mit einer Anforderung: „Das Team wartet den Java-Dienst bereits, und seine Bibliotheken unterstützen die neue Funktion.“ Ein Beliebtheitsrang allein kann diese Begründung nicht liefern. Umfragen können auf eine große Gemeinschaft oder viele verfügbare Fachkräfte hinweisen; sie belegen nicht, welche Sprache für eine bestimmte Arbeitslast am besten ist.
Wichtige Erkenntnisse
- Prüfe die Nebenbedingungen der Plattform vor persönlichen Vorlieben.
- Bibliotheken und Werkzeuge verbinden eine Sprache mit ihrem Anwendungsgebiet.
- Miss den Engpass, bevor du hauptsächlich nach Geschwindigkeit auswählst.
- Speichersicherheit und statische Prüfung verringern einige Fehler, während die Logik weiterhin getestet werden muss.
- Die Fähigkeiten des Teams, vorhandener Code und Wartungskosten gehören zur Entscheidung.
- Unterschiedliche Komponenten können unterschiedliche Sprachen rechtfertigen.
Programmierparadigmen: Beispiele
1Verfolge ein Programm vom Quellcode bis zur Ausführung
13 Schritte
- 1EinführungEine Berechnung durchläuft mehrere Schichten
Du schreibst eine Berechnung, aber ein Prozessor benötigt kleine kodierte Befehle, den sogenannten Maschinencode. Ein Übersetzungsprogramm ergänzt die fehlenden Einzelheiten.
Verfolge
x = a + bvom Quellcode aus zu den unteren Ebenen und das Programm anschließend vorwärts durch Erstellung und Ausführung. Halte zwei Fragen getrennt: In welcher Form liegt der Code vor, und welche Software führt ihn aus? Ein Compiler, ein Interpreter und ein JIT-Compiler beantworten die zweite Frage unterschiedlich. Diese Unterscheidung erklärt sowohl die Portierbarkeit als auch den Zeitpunkt von Fehlern. - 2BeispielEine Berechnung wird zu mehreren Aktionen
Stell dir für
x = a + bvier Aktionen auf einer niedrigeren Ebene vor:- Den Wert von
ain ein Register laden. - Den Wert von
bin ein anderes Register laden. - Die beiden Registerwerte addieren.
- Das Ergebnis für
xspeichern.
Die Anweisung der höheren Programmiersprache verbirgt die Registerauswahl und Speicherungsdetails. Die tatsächlich erzeugten Befehle können anders aussehen, weil ein Compiler die Arbeit optimieren kann. Die geforderte Berechnung bleibt dieselbe.
- Den Wert von
- 3ErklärungLesbare Befehle bleiben auf einen Prozessor ausgerichtet
Assemblersprache verwendet Befehlsnamen wie
movundadd. Ein Assembler kodiert sie als Maschinenbefehle.Für x86-64 schreibt
mov eax, 1die Zahl 1 in EAX;add eax, ebxaddiert EBX dazu. Diese Register und Kodierungen gehören zu dieser Befehlssatzarchitektur. Bytes durch lesbare Namen zu ersetzen hilft Menschen, macht die Befehle aber nicht zu einer portierbaren Beschreibung auf höherer Ebene. - 4HerausforderungDas Programm auf einen anderen Prozessor übertragen
Ein Team hat portierbaren C-Quellcode und eine native ausführbare x86-64-Datei. Es benötigt eine native ausführbare ARM-Datei. Welches Artefakt kann es als Ausgangspunkt verwenden, und welche Arbeit ist nötig?
---reveal---
Verwende den Quellcode und kompiliere ihn mit der erforderlichen Plattformunterstützung für das ARM-Ziel. Die ausführbare x86-64-Datei enthält Befehle für eine andere ISA. Portierbarer Quellcode ermöglicht eine weitere Übersetzung; er macht diese vorhandenen Maschinenbefehle nicht zu nativem ARM-Code.
- 5BeispielToken vor der Struktur erkennen
Betrachte
total = price * 3;in C.Die lexikalische Analyse findet sechs Token:
total,=,price,*,3und;. Die syntaktische Analyse gruppiert anschließendprice * 3als den Ausdruck, dessen Ergebnistotalzugewiesen wird.Dies sind unterschiedliche Aufgaben. Die Zerlegung in Token erkennt die Teile; die syntaktische Analyse stellt fest, wie sie zusammenpassen. Die semantische Analyse kann dann Fragen prüfen, etwa ob die Namen deklariert und die Operandentypen verträglich sind.
- 6BeispielBerechnungen mit Konstanten in die Erstellung verlegen
Der Quellcodeausdruck
60 * 60 * 24verwendet nur Konstanten. Konstantenfaltung kann ihn vor der Ausführung auswerten: zuerst 3.600, dann 86.400.Die Codegenerierung kann dieses Ergebnis statt Befehlen verwenden, die die Multiplikationen wiederholen. Dies ist eine Optimierung, weil sie die Ausführung der Arbeit ändert und dabei das geforderte Ergebnis erhält. Einen Wert durch eine lediglich zweckmäßige Näherung zu ersetzen wäre durch diese Regel nicht gerechtfertigt.
- 7ErklärungÜbersetzte Dateien mit Bibliotheksroutinen verbinden
Die Kompilierung kann separate Objektdateien erzeugen. Ein Linker verbindet diese Dateien und löst Referenzen auf Bibliotheksoperationen auf.
Statisches Binden nimmt Bibliothekscode in die erstellte ausführbare Datei auf. Dynamisches Binden ist auf eine kompatible gemeinsam genutzte Bibliothek angewiesen, die später geladen wird. Keiner der beiden Ansätze verlangt den Ahead-of-time-Compiler auf dem Computer des Benutzers, aber eine dynamisch gebundene ausführbare Datei benötigt weiterhin ihre Laufzeitabhängigkeiten.
- 8HerausforderungEine erfolgreiche Erstellung liefert das falsche Ergebnis
Ein C-Programm wird erfolgreich erstellt. Sein Bericht soll eine verbrauchte Menge von einer Ausgangsmenge abziehen, aber der Code addiert sie. Hat der Compiler eine Sprachregel nicht geprüft?
---reveal---
Nein. Eine Addition kann gültige Syntax mit verträglichen Typen sein und dabei die falsche Aufgabe ausdrücken. Das ist ein logischer Fehler. Der Compiler prüft Sprachregeln und erzeugt Code; ein Test mit bekanntem erwartetem Ergebnis ist nötig, um die Abweichung zwischen der Berechnung und ihrem beabsichtigten Zweck aufzudecken.
- 9ErklärungEine Laufzeitumgebung kann Zwischenbefehle ausführen
CPython übersetzt eine gültige Python-Datei in Bytecode und führt diesen anschließend in seiner virtuellen Maschine aus. Der Prozessor führt die Maschinenbefehle der Laufzeitumgebung aus, während die Laufzeitumgebung die Python-Operationen ausführt.
Auch Java verwendet Bytecode:
javacerzeugt Klassendateien für eine kompatible JVM. Bytecode gehört zu einem Befehlsmodell einer Laufzeitumgebung und sollte daher nicht mit einem universellen Prozessorbefehlssatz verwechselt werden. - 10BeispielZwei Zeitpunkte vergleichen, an denen die Ausführung stoppen kann
In dieser gültigen Python-Datei wird die erste Zeile ausgeführt:
print("start") x = 10 print(x + "5")Sie gibt „start“ aus, bevor die letzte Zeile einen
TypeErrorauslöst. Entferne nun die letzte schließende Klammer: CPython kann die Datei nicht übersetzen, sodass keine Anweisung in dieser Datei ausgeführt wird. Ein Syntaxfehler bei der Vorbereitung und ein Typfehler während einer ausgeführten Operation stoppen das Programm zu unterschiedlichen Zeitpunkten. - 11ErklärungHäufig ausgeführten Code während des Laufs kompilieren
Ein JIT-Compiler erzeugt Maschinencode, während das Programm läuft. Eine Laufzeitumgebung kann zunächst interpretieren, häufig verwendete Methoden beobachten und diese Methoden kompilieren.
Das verschiebt den Aufwand: Die Kompilierung benötigt während des Laufs Zeit und Speicher, während die wiederholte Ausführung danach schneller werden kann. Ein lange laufender Dienst kann von dieser Aufwärmphase stärker profitieren als ein winziges einmaliges Skript. Das Ergebnis hängt von der Implementierung und der Arbeitslast ab.
- 12HerausforderungPortierbarkeit von Bytecode und native Geschwindigkeit unterscheiden
Eine Java-Klassendatei läuft auf zwei unterschiedlichen ISAs über eine kompatible JVM. Während jedes Laufs kompiliert die Laufzeitumgebung eine häufig verwendete Methode. Warum kann eine Klassendatei gemeinsam genutzt werden, während sich der erzeugte Maschinencode unterscheidet?
---reveal---
Die Klassendatei enthält JVM-Bytecode, den beide kompatiblen Laufzeitumgebungen verstehen. Jeder JIT-Compiler ist auf seinen eigenen Prozessor ausgerichtet. Die Portierbarkeit entsteht durch das gemeinsame Zwischenformat und kompatible Laufzeitumgebungen; die während der Ausführung erzeugten Maschinenbefehle bleiben plattformspezifisch.
- 13ZusammenfassungEbenen und Ausführungswege getrennt halten
Maschinencode ist auf eine ISA ausgerichtet; Assemblersprache gibt ihren Befehlen lesbare Namen; Quellcode einer höheren Programmiersprache verbirgt mehr Hardwaredetails.
Ein Compiler übersetzt Code, ein Interpreter führt ihn aus, und ein JIT-Compiler kompiliert während der Ausführung. Eine Laufzeitumgebung kann diese Methoden kombinieren. Um ein tatsächliches System zu erklären, benenne seine Quell-, Zwischen- und nativen Formen und gib dann an, welche Phase die Syntax prüft und welche die Operation ausführt, die fehlschlagen kann.
2Verfolge die Bedeutung in einem kurzen Programm
10 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungForm und Bedeutung beantworten unterschiedliche Fragen
- 2BeispielGültiger Code kann die falsche Berechnung gruppieren
- 3ErklärungDie erste Bedingung kann die zweite schützen
- 4HerausforderungSage vorher, ob die Division ausgeführt wird
- 5ErklärungEine Zuweisung liest vor der Aktualisierung
- 6BeispielVollziehe jede Aktualisierung der Akkumulatorvariable nach
- 7BeispielVerfolge einen Aufruf und seinen Rückgabewert
- 8HerausforderungEine lokale Aktualisierung kann den Aufrufer unverändert lassen
- 9HerausforderungVerfolge Grammatik und Zustand gemeinsam
- 10ZusammenfassungLies den ausgeführten Weg
3Verfolge Typen und gemeinsam genutzte Objekte
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungVerfolge Werte, Objekte und Speicher
- 2ErklärungTypisierte Werte können zwischen Namen wechseln
- 3KernaussageInferenz und Umwandlung beantworten unterschiedliche Fragen
- 4HerausforderungLies Ziffern als den tatsächlich zurückgegebenen Wert
- 5ErklärungDie Darstellung beeinflusst numerische Ergebnisse
- 6ErklärungEin Aufrufrahmen kann vor einem Objekt enden
- 7BeispielZwei Namen verweisen auf eine Liste
- 8HerausforderungEin Parameter kann ein Objekt verändern und danach auf ein anderes verweisen
- 9ErklärungBestimme genau, wie Speicher falsch verwendet wird
- 10BeispielVerfolge Referenzen, bis die letzte verschwunden ist
- 11KernaussageEin Zyklus kann ohne aktiven Zugangspunkt bestehen
- 12ErklärungOwnership prüft Zugriffe vor der Ausführung
- 13HerausforderungFinde heraus, warum die Speicherbereinigung eine Liste nicht freigeben kann
- 14ZusammenfassungVerfolge drei unterschiedliche Entscheidungen
4Schütze Regeln und wähle Beziehungen zwischen Objekten
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungSchritte brauchen verlässliche Grenzen
- 2BeispielDie Reihenfolge der Operationen ändert den gemeinsamen Zustand
- 3ErklärungGestalte die Grenze einer Prozedur sinnvoll
- 4HerausforderungSage eine Aktualisierung in anderer Reihenfolge vorher
- 5ErklärungGib jedem Konto einen eigenen Zustand
- 6BeispielPrüfe, bevor du einen Kontostand änderst
- 7HerausforderungFinde eine umgangene Regel
- 8BeispielSpezialisiere, ohne den Vertrag zu verletzen
- 9ErklärungLass das Objekt seine Flächenmethode wählen
- 10VerbindungEnthalte die Komponente, die die Arbeit erledigt
- 11HerausforderungWähle zwei Beziehungen in einem neuen Entwurf
- 12ZusammenfassungStimme Grenzen auf Zuständigkeiten ab
5Wandle eine Sammlung um und frage ein Ergebnis ab
13 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungGib eine Umwandlung oder eine Anfrage an
- 2ErklärungReinheit beschreibt mehr als die zurückgegebene Zahl
- 3BeispielWähle zuerst die geraden Werte aus
- 4BeispielWandle die ausgewählten Werte um und kombiniere sie
- 5HerausforderungWende die Verarbeitungskette auf einen neuen Fall an
- 6ErklärungGib Verhalten zurück, das sich seinen Faktor merkt
- 7BeispielVerkleinere das Problem, bis ein direkter Fall gilt
- 8BeispielLies SQL als Anfrage über Zeilen
- 9KernaussageBehalte das Ergebnis bei, während sich der Plan ändert
- 10BeispielVerbinde zwei Fakten durch eine Regel
- 11HerausforderungLies eine neue Abfrage einschließlich ihres Schwellenwerts
- 12HerausforderungKombiniere eine Abfrage mit einer reinen Umwandlung
- 13ZusammenfassungHalte die Beziehung zwischen Eingabe und Ergebnis sichtbar
6Vergleiche Stile und begründe eine Sprachwahl
10 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungHalte die Aufgabe fest
- 2BeispielHalte die Aufgabe fest und ändere den Stil
- 3BeispielPlatziere dieselbe Regel in Objekten oder einer Abfrage
- 4HerausforderungEntscheide, ob ein Entwurf nur einen Stil hat
- 5ErklärungSchließe zuerst nicht unterstützte Ziele aus
- 6BeispielWähle für ein vollständiges Wettersystem
- 7TippMiss, was die Zeit verbraucht
- 8HerausforderungBegründe eine Wahl bei konkurrierenden Nebenbedingungen
- 9HerausforderungVerbinde die Sprachwahl mit der Programmstruktur
- 10ZusammenfassungGib einen Grund an, der zur Anforderung passt
Programmierparadigmen: Übungsaufgaben
Set 1: Lies Code und erkenne den Ausführungsweg10 Übungsfragen
Frage 1 von 10
mov und jmp. Auf welcher Sprachebene ist das Programm nun geschrieben?Antwort zeigenAntwort ausblenden
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Frage 5 von 10
Welchen Endwert speichert dieser Python-Code in total?
total = 1
for n in [4, 7, 2]:
total = total + n
Antwort zeigenAntwort ausblenden
Frage 6 von 10
label = "18" die Zuweisung label = 18. Der Typ, auf den der Name verweist, kann sich ändern, aber "18" + 2 löst einen Fehler aus. Wie werden diese beiden Tatsachen beschrieben?Antwort zeigenAntwort ausblenden
Frage 7 von 10
Was enthält first, nachdem dieser Python-Code ausgeführt wurde?
first = [4, 8]
second = first
second.append(6)
Antwort zeigenAntwort ausblenden
second verändert diese gemeinsam genutzte Liste, sodass auch first die hinzugefügte 6 zeigt. Damit [4, 8] erhalten bliebe, wäre eine separate Liste nötig, etwa durch Kopieren vor dem Anhängen.Frage 8 von 10
read_data(), calculate_total() und save_report() aufgeteilt, die jeweils geordnete Befehle enthalten. Welche Struktur veranschaulicht dies?Antwort zeigenAntwort ausblenden
Frage 9 von 10
n == 0 den Wert 0 zurück; andernfalls ruft sie sich mit n + 1 selbst auf. Warum beendet der geschriebene Basisfall die Berechnung nicht, wenn sie bei der positiven Zahl 2 beginnt?Antwort zeigenAntwort ausblenden
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Set 2: Wende Regeln für Typen, Objekte und Abfragen an10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
a = 8 und b = 12 die Ausdrücke a + b / 2 und (a + b) / 2 jeweils?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
balance und eine Methode deposit. Zwei Konten werden mit unterschiedlichen Anfangskontoständen erzeugt. Wo gehören diese unterschiedlichen Kontostände hin?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
[1, 4, 7, 10] die Werte größer als 3, zieht von jedem behaltenen Wert 1 ab und summiert sie anschließend. Was ist das Ergebnis?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Welche Werte haben x und y nach diesem Python-Code?
x = [12]
y = x
x = x + [3]
y.append(6)
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 3: Verfolge Aufrufe, Effekte und Objektverträge10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
minutes = 60 * 12 durch den vorberechneten Wert 720. Welche Optimierung wird verwendet und was muss sie erhalten?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Was gibt dieses Python-Programm aus?
score = 11
def change():
score = 4
return score + 2
print(change())
print(score)
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Welchen Wert hat items nach dem Aufruf?
items = [3]
def revise(values):
values.append(8)
values = [1]
values.append(5)
revise(items)
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
showDetails für ein Klickereignis. Warum kann dieselbe Behandlungsroutine trotzdem Objektmethoden und imperative Zuweisungen verwenden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 4: Denke über Ausführungs- und Entwurfsgrenzen hinweg10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
n = 0 die Ausdrücke n != 0 and 36 / n > 4 und 36 / n > 4 and n != 0. Was geschieht?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Welcher Endwert wird in total gespeichert?
def bump(x):
return x + 2
total = 0
for n in [2, 5, 8]:
if n > 3:
total = total + bump(n)
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
item.render() auf. Eine neue, nicht verwandte Klasse stellt eine kompatible Methode render() bereit. Der Python-Aufrufer verlangt keine Oberklasse. Warum kann das Objekt ohne typspezifischen Zweig an der Schleife teilnehmen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 5: Untersuche Grenzen und verbinde die Ideen10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Was gibt dieses Python-Programm aus?
value = 6
def step(value):
value = value + 3
return value
for n in [1, 2]:
result = step(value)
print(value, result)
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
double() multipliziert ihn mit 2; add_three() addiert 3. Wie groß ist ausgehend vom selben Wert immer der Unterschied zwischen erst Verdoppeln und dann Addieren und erst Addieren und dann Verdoppeln?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
readings hat die Zeilen (level, active): (50, true), (65, true), (20, false), (85, true) und (90, false). Eine Abfrage fordert SUM(level) unter der Bedingung active = true AND level > 50 an. Welche Summe ist nach dem Hinzufügen eines geeigneten Index erforderlich, und darf sich der Ausführungsplan ändern?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
parent(lea, noah). und parent(noah, sora). sowie die Regel grandparent(X, Z) :- parent(X, Y), parent(Y, Z).. Was findet grandparent(lea, Who)?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
[50, 65, 20, 85] die Beträge echt über 50 summieren. Wie müssen ihre korrekten Ergebnisse lauten?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Ablauftabelle
Abstraktion
Ahead-of-time-Kompilierung
Aliasing
b = a erklärt Aliasing, warum das Anhängen über b auch ändern kann, was über a sichtbar ist.Anfrageoptimierer
Anweisung
total = price * 3 weist einer Variablen das Ergebnis einer Multiplikation zu.Assembler
add eax, ebx in die entsprechenden x86-64-Befehlsbytes um.Assemblersprache
Attribut
Aufrufstapel
area fügt dem Aufrufstapel einen Eintrag hinzu und entfernt ihn, wenn das Ergebnis an den Aufrufer zurückkehrt.Ausdruck
width * height liefert den Wert, den eine Funktion zur Berechnung einer Rechteckfläche zurückgibt.Automatische Speicherbereinigung
B
Basisfall
Befehlssatzarchitektur
Bytecode
C
Closure
Compiler
D
Datenkapselung
Datentyp
Deklarative Programmierung
Domänenspezifische Sprache
Double Free
free für eine bereits freigegebene Allokation kann einen Double Free und unvorhersehbares Verhalten verursachen.Duck-Typing
area() bereitstellt.Dynamische Methodenbindung
shape.area() an die Rechteckimplementierung, wenn das aufgerufene Objekt ein Rechteck ist.Dynamische Typisierung
E
Engpass
Ereignisbehandlungsroutine
Ereignisgesteuerte Programmierung
F
Funktion erster Klasse
map als Operation übergeben werden, die auf jedes Element einer Sammlung anzuwenden ist.Funktion höherer Ordnung
filter nimmt ein Prädikat entgegen, das entscheidet, welche Eingabewerte behalten werden.Funktionale Programmierung
G
Geheimnisprinzip
Gleitkommazahl
0.1 + 0.2 und dem gespeicherten Wert von 0.3 verursachen.Gültigkeitsbereich
H
Hängender Zeiger
Heap
Höhere Programmiersprache
I
Imperative Programmierung
Implizite Typumwandlung
Interpreter
J
Just-in-time-Kompilierung
K
Klasse
Loan einer Bibliothek kann für jede Ausleihe das Mitglied, das Buch, das Fälligkeitsdatum und die Verlängerungsoperation festlegen.Komposition
Konstantenfaltung
60 * 60 * 24 vor dem Programmstart durch 86.400 zu ersetzen.Kurzschlussauswertung
and verhindern.L
Lambda-Ausdruck
lambda n: n * n stellt einer Umwandlungskette eine Quadrieroperation bereit.Laufzeitfehler
Lexikalische Analyse
price * 3 in einen Namen, einen Multiplikationsoperator und eine Zahl zerlegt.Linker
Logische Programmierung
Logischer Fehler
a + b / 2 zur Berechnung des Durchschnitts zweier Werte ist ein logischer Fehler, weil die Division vor der Addition stattfindet.M
Maschinencode
Methode
Multiparadigmensprache
Mutation
O
Objekt
Ownership
Ö
Ökosystem
P
Parameter
width erhält 3, wenn der Aufrufer area(3, 4) auswertet.Plattform
Polymorphie
area() für Kreise und Rechtecke ohne separaten Zweig für jede Form aufzurufen.Programmierparadigma
Prozedurale Programmierung
Pufferüberlauf
R
Referenz
Referenzzählung
Regulärer Ausdruck
[0-9]{3}-[0-9]{4} passt auf eine Gruppe aus drei Ziffern, gefolgt von einem Bindestrich und vier Ziffern.Reine Funktion
Rekursion
REPL
Rückruffunktion
showMenu als Rückruffunktion ermöglicht einem Schaltflächensystem, diese Funktion beim Klick des Benutzers aufzurufen.S
Schnittstelle
area() verlangen und dabei jeder Form überlassen, die Fläche anders zu berechnen.Seiteneffekt
Semantik
Speicherallokation
Speicherleck
Speichersicherheit
Sprachimplementierung
SQL
Stapelrahmen
Statische Typisierung
Strukturierte Programmierung
Syntaktische Analyse
total = price * 3; zur Zuweisung gehört.Syntax
if-Bedingung einen Doppelpunkt und für den gesteuerten Block eine Einrückung.T
Token
Turing-Vollständigkeit
Typinferenz
Ü
Überlauf
Überschreiben
U
Unveränderlichkeit
Use-after-free
V
Vererbung
Virtuelle Maschine
Z
Zuweisung
count = count + 1 liest den alten Zählwert, bevor sie ihn durch den erhöhten Wert ersetzt.Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.