Notes 6

Sie können diese Seite ausdrucken oder mit einem PDF-Drucker in ein PDF umwandeln, um Ihre eigenen Notizen hinzuzufügen.

Willkommen!

  • In den vergangenen Wochen haben Sie grundlegende Bausteine der Programmierung kennengelernt.
  • Sie lernten die Low-Level-Programmiersprache C kennen.
  • Heute werden wir mit einer höheren Programmiersprache namens Python arbeiten.
  • Während Sie diese neue Sprache lernen, werden Sie feststellen, dass Sie besser in der Lage sind, sich selbst neue Programmiersprachen beizubringen.

Der Python-Moment

  • Erinnern Sie sich an die Filter-Aufgabe? Box-Blur, Edge-Detection – Pixel für Pixel durchgehen, Nachbarn berechnen, Speicher verwalten…

  • In Python sieht das so aus:

    from PIL import Image, ImageFilter
    
    before = Image.open("bridge.bmp")
    after = before.filter(ImageFilter.BoxBlur(10))
    after.save("out.bmp")
  • Das ist alles. Vier Zeilen.

  • Wie ist das möglich? Python ist eine höhere Programmiersprache, die viele der mühsamen Details für uns übernimmt. In den nächsten 90 Minuten werden Sie verstehen, warum.

Python

  • Die Menschen haben im Laufe der Jahrzehnte erkannt, wie frühere Designentscheidungen verbessert werden können.
  • Python ist eine Programmiersprache, die auf dem aufbaut, was Sie bereits in C gelernt haben.
  • Anders als C ist Python eine interpretierte Sprache, bei der Sie Ihr Programm nicht separat kompilieren müssen. Stattdessen führen Sie Ihr Programm im Python Interpreter aus.
  • Schauen Sie sich das Short-Video zu Python-Grundlagen an, das die wichtigsten Unterschiede zwischen C und Python erklärt.

Hallo

  • Bis zu diesem Punkt sah der Code in etwa wie folgt aus:

    // Ein Programm, das der Welt "Hallo" sagt
    
    #include <stdio.h>
    
    int main(void)
    {
        printf("Hallo, Welt");
    }
  • Heute ist das Programmieren einfacher.

  • Zum Beispiel wird der obige Code in Python wie folgt dargestellt:

    # Ein Programm, das der Welt Hallo sagt
    
    print("Hallo, Welt")

    Beachten Sie, dass das Semikolon wegfällt und dass keine Bibliothek benötigt wird.

  • Python kann insbesondere das, was in C recht kompliziert war, relativ einfach umsetzen.

Filter (Rückblick)

  • Sie haben am Anfang der Vorlesung gesehen, wie wenig Code für einen Blur-Filter in Python nötig ist.
  • Das PIL-Modul (Python Imaging Library) stellt Image und ImageFilter zur Verfügung, mit denen Sie Bilder laden, filtern und speichern können.
  • Mit Python können Sie bei der Programmierung noch mehr von den Details abstrahieren als in C und anderen niedrigeren Programmiersprachen.

CS50-Bibliothek

  • Wie in C kann auch in Python eine cs50-Bibliothek verwendet werden, um den Einstieg zu erleichtern.

  • Die folgenden Funktionen sollten Ihnen bekannt vorkommen:

      get_float
      get_int
      get_string
  • Anders als in C können Sie in Python nur die tatsächlich benötigten Funktionen aus der CS50-Bibliothek importieren:

    from cs50 import get_float, get_int, get_string

Strings

  • In C erinnern Sie sich vielleicht an diesen Code:

    // get_string und printf mit %s
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        string answer = get_string("Wie ist Ihr Name?");
        printf("Hallo, %s\n", answer);
    }
  • Dieser Code sieht in Python so aus:

    # get_string und print, mit Format-Strings (f-Strings)
    
    from cs50 import get_string
    
    answer = get_string("Wie ist Ihr Name?")
    print(f"hallo, {answer}")

    Beachten Sie, wie die geschweiften Klammern es der Funktion print ermöglichen, dass answer darin erscheint. Das f ist erforderlich, um answer im String richtig zu formatieren (sog. f-String). Das funktioniert nicht nur in print, sondern überall, wo Strings verwendet werden.

  • Ohne CS50-Stützräder:

    answer = input("Wie ist Ihr Name? ")
    print(f"hallo, {answer}")

Variablen

  • Auch die Variablendeklaration wird vereinfacht. In C könnten Sie int counter = 0; angeben. In Python würde dieselbe Zeile lauten: counter = 0. Sie müssen den Typ der Variablen nicht angeben, weil Python eine dynamisch typisierte Sprache ist (C ist statisch typisiert).
  • Python nutzt counter += 1, um um eins zu erhöhen. Die aus C bekannte Möglichkeit mit counter++ gibt es in Python nicht.

PEP 8 – Python Style Guide

  • PEP 8 ist der offizielle Style Guide für Python-Code. Er sorgt dafür, dass Python-Code einheitlich und gut lesbar ist.
  • Die wichtigsten Regeln:
    • Einrückungen mit 4 Leerzeichen (keine Tabs)
    • Leerzeichen vor und nach Operatoren: a = b + c statt a=b+c
    • Aussagekräftige Variablennamen: anzahl_studenten statt x
    • Maximale Zeilenlänge: 79 Zeichen
  • Tools wie autopep8 können Code automatisch formatieren:
    autopep8 --in-place meine_datei.py
  • Mehr dazu in der offiziellen PEP 8 Dokumentation.

Typen

  • Datentypen müssen in Python nicht explizit deklariert werden. Sie haben zum Beispiel oben gesehen, dass Antwort eine Zeichenkette ist, aber wir mussten dem Interpreter nicht sagen, dass dies der Fall ist: Er wusste es von selbst; den Datentyp hat er sich selbst erschlossen.

  • In Python werden häufig folgende Typen verwendet:

      bool
      float
      int
      str

    Beachten Sie, dass long und double fehlen. Python nutzt für größere Zahlen automatisch Repräsentationen mit ausreichender Genauigkeit.

  • Sie können das selbst ausprobieren, indem Sie den Python-Interpreter in einem Terminal durch Eingabe von python (oder python3) im interaktiven Modus starten und folgendes eingeben: 10**100 – das Ergebnis dieser Potenzierung von 10 mit der Zahl 100 ist eine Zahl mit 101 Stellen – und wäre in C nicht ohne weiteres in einer Variablen speicherbar.

  • Einige andere Datentypen in Python sind:

      range (Sequenz von Zahlen)
      list (Listen, deren Größe automatisch angepasst wird)
      tuple (Tupel, also nicht veränderbare Listen)
      dict (Dictionaries mit Key-Value-Paaren)
      set (Mengen ohne Duplikate)
  • Jeder dieser Datentypen kann auch in C implementiert werden, aber in Python können sie einfacher implementiert werden.

Rechner

  • Vielleicht erinnern Sie sich an calculator.c von früher im Kurs:

    // Addition mit int
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Aufforderung an den Benutzer zur Eingabe von x
        int x = get_int("x: ");
    
        // Aufforderung an den Benutzer zur Eingabe von y
        int y = get_int("y: ");
    
        // Addition durchführen
        printf("%i\n", x + y);
    }
  • Wir können einen einfachen Taschenrechner genauso implementieren, wie wir es in C getan haben. Geben Sie code calculator.py in das Terminalfenster ein und schreiben Sie folgenden Code:

    # Addition mit int [mittels get_int]
    
    from cs50 import get_int
    
    # Benutzer zur Eingabe von x auffordern
    x = get_int("x: ")
    
    # Benutzer zur Eingabe von y auffordern
    y = get_int("y: ")
    
    # Addition durchführen
    print(x + y)

    Beachten Sie, wie die CS50-Bibliothek importiert wird. Dann werden “x” und “y” vom Benutzer abgefragt. Schließlich wird das Ergebnis ausgegeben. Beachten Sie, dass die Funktion “main”, die in einem C-Programm zu sehen wäre, völlig fehlt! Obwohl man eine “main”-Funktion verwenden könnte, ist sie nicht erforderlich.

  • Wir versuchen auch hier wieder, die Stützräder der CS50-Bibliothek zu entfernen. Ändern Sie Ihren Code wie folgt:

    # Addition mit int [mit input]
    
    # Benutzer zur Eingabe von x auffordern
    x = input("x: ")
    
    # Benutzer zur Eingabe von y auffordern
    y = input("y: ")
    
    # Addition durchführen
    print(x + y)

    Beachten Sie, dass die Ausführung des obigen Codes zu einem sehr seltsamen Programmverhalten führt. Woran mag das liegen?

  • Wenn Sie an dieser Stelle vermutet haben, dass der Python-Interpreter x und y als Strings betrachtet und daher die Konkatenation von x und y ausgibt, dann liegen Sie damit richtig. Sie können den Code korrigieren, indem Sie die Funktion int zur Typumwandlung wie folgt verwenden:

    # Addition mit int [mit Eingabe]
    
    # Benutzer zur Eingabe von x auffordern
    x = int(input("x: "))
    
    # Benutzer zur Eingabe von y auffordern
    y = int(input("y: "))
    
    # Addition durchführen
    print(x + y)

    Beachten Sie, wie die Eingabe für x und y an die Funktion int übergeben wird, die sie in eine Ganzzahl umwandelt. Ohne die Umwandlung von “x” und “y” in Ganzzahlen werden die Zeichen aneinandergereiht (konkateniert).

Bedingte Anweisungen

  • Aus unseren C-Vorlesungen kommt Ihnen vielleicht ein Programm wie dieses vertraut vor

    // Bedingte Anweisungen, boolesche Ausdrücke, relationale Operatoren
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Aufforderung zur Eingabe von Ganzzahlen
        int x = get_int("Wie groß ist x? ");
        int y = get_int("Wie lautet y? ");
    
        // Ganzzahlen vergleichen
        if (x < y)
        {
            printf("x ist kleiner als y\n");
        }
        else if (x > y)
        {
            printf("x ist größer als y\n");
        }
        else
        {
            printf("x ist gleich y\n");
        }
    }
  • In Python würde das wie folgt aussehen:

    # Bedingte Anweisungen, boolesche Ausdrücke, relationale Operatoren
    
    from cs50 import get_int
    
    # Benutzer zur Eingabe von Ganzzahlen auffordern
    x = get_int("Wie groß ist x? ")
    y = get_int("Wie lautet y? ")
    
    # Ganzzahlen vergleichen
    if x < y:
        print("x ist kleiner als y")
    elif x > y:
        print("x ist größer als y")
    else:
        print("x ist gleich y")

    Beachten Sie, dass es, erstens, keine geschweiften Klammern mehr gibt. Stattdessen werden Einrückungen verwendet. Zweitens wird ein Doppelpunkt in der “if”-Anweisung verwendet. Außerdem wird else if durch elif ersetzt. Auch Klammern sind in den Anweisungen if und elif nicht mehr erforderlich.

Truthiness: Was ist wahr und was ist falsch?

  • In Python gelten viele Werte als “falsch” (falsy), nicht nur False und 0:

    # Diese Werte sind alle "falsy":
    x = 0          # False
    x = ""         # False (leerer String)
    x = []         # False (leere Liste)
    x = None       # False (Pythons NULL)
    
    # Diese Werte sind "truthy":
    name = "Max"   # True (nicht-leerer String)
    zahl = 42      # True (nicht-null)
  • Das ermöglicht elegante Prüfungen:

    name = input("Name: ")
    if name:  # statt: if name != ""
        print(f"Hallo, {name}")
    else:
        print("Kein Name eingegeben")

Verkettete Vergleiche

  • Python erlaubt elegante verkettete Vergleiche:
    # Statt: if alter >= 13 and alter <= 19
    if 13 <= alter <= 19:
        print("Teenager")

Ternärer Operator

  • Python hat einen kompakten bedingten Ausdruck:

    # Statt if/else über mehrere Zeilen:
    status = "erwachsen" if alter >= 18 else "minderjährig"
  • Anders als in C lassen sich Strings genauso einfach vergleichen:

    # Benutzer zur Eingabe von Strings auffordern
    s = input("s? ")
    t = input("t? ")
    
    if s == t:
        print("same")
    else:
        print("different")

Gleichheit vs. Identität: == und is

  • == prüft, ob zwei Werte gleich sind (Inhalt).

  • is prüft, ob zwei Variablen auf dasselbe Objekt zeigen (Identität).

    x = [1, 2, 3]
    y = [1, 2, 3]
    print(x == y)   # True - gleicher Inhalt
    print(x is y)   # False - verschiedene Objekte
  • Für None sollte man immer is verwenden:

    x = None
    if x is None:      # Richtig!
        print("Ist None")
    # if x == None:    # Funktioniert, aber nicht idiomatisch
  • Betrachten Sie den bereits bekannten C-Code, um weitere Vergleiche anzustellen:

    // Logische Operatoren
    
    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Aufforderung an den Benutzer, zuzustimmen
        char c = get_char("Sind Sie einverstanden?");
    
        // Prüfen, ob einverstanden
        if (c == 'Y' || c == 'y')
        {
            printf("Einverstanden.\n");
        }
        else if (c == 'N' || c == 'n')
        {
            printf("Nicht einverstanden.\n");
        }
    }
  • Dies kann in Python wie folgt umgesetzt werden:

    # Logische Operatoren
    
    from cs50 import get_string
    
    # Aufforderung an den Benutzer zuzustimmen
    s = get_string("Sind Sie einverstanden?")
    
    # Prüfen, ob einverstanden
    if s == "Y" or s == "y":
        print("Einverstanden.")
    elif s == "N" or s == "n":
        print("Nicht einverstanden.")

    Beachten Sie, dass die beiden Senkrechtstriche, die in C verwendet werden, durch “or” ersetzt werden. In der Tat bevorzugen viele Menschen Python auch deswegen, weil es für Menschen besser lesbar ist. Beachten Sie auch, dass es in Python kein char gibt. Stattdessen wird auch für einzelne Zeichen der Datentyp “str” verwendet; die in C erforderliche Unterscheidung (einfache oder doppelte Anführungszeichen) gibt es hier nicht.

  • Eine “pythonischere” (more pythonic) Herangehensweise an das Problem würde wie folgt aussehen.

    Was bedeutet “pythonic”? In der Python-Community gibt es oft mehrere Wege, ein Problem zu lösen. Als “pythonic” bezeichnet man Code, der die Stärken und Idiome der Sprache nutzt – also lesbarer, eleganter und dem “Python-Geist” entsprechender Code. Es gibt sogar einen eingebauten Leitfaden: Tippen Sie import this in den Python-Interpreter!

    Hier wird der Datentyp Liste verwendet (Notation in eckigen Klammern):

    # Logische Operatoren, die Listen verwenden
    
    from cs50 import get_string
    
    # Aufforderung an den Benutzer zuzustimmen
    s = get_string("Sind Sie einverstanden?")
    
    # Prüfen, ob einverstanden
    if s in ["y", "yes"]:
        print("Einverstanden.")
    elif s in ["n", "nein"]:
        print("Nicht einverstanden.")

    Beachten Sie, dass wir mehrere Schlüsselwörter wie “y” und “ja” in einer “Liste” ausdrücken können.

  • Etwas unelegant ist, dass wir für alle Varianten von Groß- und Kleinschreibung nun entsprechende Elemente in den Listen benötigen. In C würden wir die Zeichen in s mit tolower(…) aus der Bibliothek ctype.h in Kleinbuchstaben umwandeln (Zeichen für Zeichen in einer Schleife).

  • Schauen Sie sich das Short-Video zu Kontrollfluss und Strings für weitere Details an.

Objektorientierte Programmierung

  • Unter Designgesichtspunkten ist das Anbieten einer Funktion wie tolower(…) nicht besonders elegant. Nach dem Include steht sie im Programm einfach so zur Verfügung wie jede andere Funktion. Bei vielen Includes wird es schnell unübersichtlich und es könnte sogar zu Namenskonflikten kommen, wenn z.B. mehrere Bibliotheken gleichzeitig included werden, die die gleichen Funktionen anbieten. Bisher haben wir das so hingenommen.

  • Wäre es nicht schöner, wenn die Funktionen, die bestimmte Daten verarbeiten, näher bei den Daten definiert wären, auf die sie anwendbar sind?

  • In der Realität sind wir es gewohnt, dass Objekte zum einen Eigenschaften mit sich bringen und zum anderen bestimmtes Verhalten haben. Ein Auto hat vier Räder und eine bestimmte Farbe. Es kann beschleunigen und bremsen.

  • Es bietet sich an, dieses Konzept auch bei der Programmierung zu verwenden und dadurch den Code besser zu organisieren.

  • Das ist die Idee der objektorientierten Programmierung, in der wir nicht mit unstrukturierten Daten arbeiten, sondern mit Objekten.

  • Mit C können wir das nicht umsetzen. Wir können lediglich eine struct erstellen, um mehrere Variablen innerhalb eines einzigen, selbst erstellten Datentyps miteinander zu verbinden. In Python können wir hingegen objektorientiert programmieren – und haben das von Anfang an getan, weil in Python alles ein Objekt ist!

  • Beim objektorientierten Programmieren können wir nicht nur mehrere Werte zu einem Objekt zusammenfassen, sondern auch Funktionen auf den Objekten aufrufen, die zu Objekten von einem bestimmten Typ (einer Klasse) gehören. Solche Funktionen nennen wir Methoden. Methoden werden durch den Punktoperator auf ein Objekt angewendet.

  • Zum Beispiel haben str-Objekte in Python eine eingebaute Methode, mit denen wir alle Großbuchstaben in Kleinbuchstaben umwandeln können Daher könnten Sie Ihren Code wie folgt ändern:

    # Objektorientiertes Programmieren
    
    from cs50 import get_string
    
    # Aufforderung an den Benutzer zuzustimmen
    s = get_string("Sind Sie einverstanden?").lower()
    
    # Prüfen, ob zugestimmt wurde
    if s.lower() in ["y", "yes"]:
        print("Einverstanden.")
    elif s.lower() in ["n", "nein"]:
        print("Nicht einverstanden.")

    Man beachte, wie dadurch anstelle des Werts von s das Ergebnis von s.lower(), einer eingebauten Methode von str, verwendet wird.

Wichtige String-Methoden

MethodeBeschreibungBeispiel
upper()Alles in Großbuchstaben"hallo".upper()"HALLO"
lower()Alles in Kleinbuchstaben"HALLO".lower()"hallo"
strip()Leerzeichen vorn/hinten entfernen" hi ".strip()"hi"
split()String in Liste aufteilen"a,b,c".split(",")["a","b","c"]
find()Position eines Teilstrings"hallo".find("ll")2
replace()Ersetzen"hallo".replace("l","x")"haxxo"
startswith()Beginnt mit…?"hallo".startswith("ha")True
isdigit()Nur Ziffern?"123".isdigit()True

Alles ist ein Objekt

  • In Python ist alles ein Objekt – auch Dinge, bei denen man es nicht erwarten würde:
    text = "Hello"
    print(type(text))       # <class 'str'>
    print(text.upper())     # Methodenaufruf auf String-Objekt
    
    numbers = [1, 2, 3]
    print(type(numbers))    # <class 'list'>
    numbers.append(4)       # Methodenaufruf auf Listen-Objekt
    
    # Sogar Funktionen sind Objekte!
    def greet():
        print("Hi")
    print(type(greet))      # <class 'function'>
    x = greet               # x zeigt auf das Funktions-Objekt
    x()                     # Ruft greet() auf

Eigene Klassen definieren

  • Eine Klasse ist ein Bauplan für Objekte. Sie definiert Attribute (Daten) und Methoden (Verhalten):

    class Rectangle:
        def __init__(self, width, height):
            # Initialisierung der Attribute
            self.width = width
            self.height = height
    
        def area(self):
            return self.width * self.height
    
        def perimeter(self):
            return 2 * (self.width + self.height)
    
    # Objekte (Instanzen) erstellen
    small_rect = Rectangle(3, 4)
    big_rect = Rectangle(10, 20)
    
    print(small_rect.area())       # 12
    print(big_rect.perimeter())    # 60
  • Wichtige Konzepte:

    • class definiert eine neue Klasse (Namenskonvention: PascalCase)
    • __init__ ist der Initialisierer – wird automatisch beim Erstellen aufgerufen
    • self ist eine Referenz auf das aktuelle Objekt (erster Parameter jeder Methode)
    • Jedes Objekt hat seinen eigenen Zustand (hier: verschiedene Breiten/Höhen)

self verstehen

  • Wenn wir eine Methode aufrufen, übergibt Python automatisch das Objekt als ersten Parameter:
    class Student:
        def __init__(self, name):
            self.name = name
            self.courses = []
    
        def enroll(self, course):
            self.courses.append(course)
    
    alice = Student("Alice")
    alice.enroll("Python")
    
    # Python macht intern daraus:
    # Student.enroll(alice, "Python")

Instanz- vs. Klassenattribute

  • Instanzattribute gehören zu einem einzelnen Objekt, Klassenattribute werden von allen geteilt:
    class Student:
        # Klassenattribut - von allen Instanzen geteilt
        school_name = "Python Academy"
    
        def __init__(self, name):
            # Instanzattribute - pro Objekt unterschiedlich
            self.name = name
    
    alice = Student("Alice")
    bob = Student("Bob")
    
    # Klassenattribut über die Klasse ändern
    Student.school_name = "Code School"
    print(alice.school_name)  # "Code School"
    print(bob.school_name)    # "Code School"

Kapselung: Private Attribute

  • Per Konvention markiert ein Unterstrich _ ein Attribut als “privat” (nur intern verwenden).
  • Wichtig: Anders als in Java (wo private vom Compiler erzwungen wird) ist der Unterstrich in Python nur eine Konvention und ein visueller Hinweis. Python wirft keinen Fehler, wenn Sie von außen auf _balance zugreifen – es funktioniert einfach! Die Verantwortung liegt beim Programmierer.
    class BankAccount:
        def __init__(self, initial_balance):
            self._balance = initial_balance  # Privat!
    
        def deposit(self, amount):
            if amount > 0:
                self._balance += amount
    
        def get_balance(self):
            return self._balance
    
    konto = BankAccount(1000)
    konto.deposit(500)
    print(konto.get_balance())  # 1500
    print(konto._balance)       # 1500 – funktioniert! Aber schlechter Stil.
    konto._balance = -100       # Auch das geht – Python vertraut dem Programmierer

Properties: Elegante Zugriffskontrolle

  • Properties ermöglichen Validierung bei scheinbar direktem Attributzugriff:
    class BankAccount:
        def __init__(self, initial_balance):
            self._balance = 0
            self.balance = initial_balance  # Nutzt Property!
    
        @property
        def balance(self):
            """Getter: Wird bei Lesezugriff aufgerufen"""
            return self._balance
    
        @balance.setter
        def balance(self, value):
            """Setter: Wird bei Schreibzugriff aufgerufen"""
            if value < 0:
                print("Fehler: Negativer Kontostand!")
                return
            self._balance = value
    
    konto = BankAccount(1000)
    print(konto.balance)      # 1000 - nutzt Getter
    konto.balance += 500      # nutzt Setter mit Validierung
    konto.balance = -100      # "Fehler: Negativer Kontostand!"

Spezielle Methoden (Dunder Methods)

  • Wenn Sie ein eigenes Objekt mit print() ausgeben, erhalten Sie eine wenig hilfreiche Ausgabe wie <__main__.GradeBook object at 0x7f...>. Aber bei einer Liste wie [1, 2, 3] wird diese schön formatiert ausgegeben. Warum? Listen haben die Methode __str__ implementiert, die Python aufruft, wenn ein Objekt als String dargestellt werden soll.
  • Dunder Methods (double underscore) ermöglichen es, dass Objekte mit Standard-Operationen funktionieren:
MethodeWird aufgerufen beiBeispiel
__str__print(obj)String-Darstellung
__len__len(obj)Länge des Objekts
__eq__obj1 == obj2Gleichheitsvergleich
__getitem__obj[i]Index-Zugriff
  • Beispiel:

    class GradeBook:
        def __init__(self):
            self._grades = []
    
        def add(self, grade):
            self._grades.append(grade)
    
        def __str__(self):
            return f"Notenbuch mit {len(self._grades)} Noten"
    
        def __len__(self):
            return len(self._grades)
    
        def __getitem__(self, index):
            return self._grades[index]
    
        def __eq__(self, other):
            if not isinstance(other, GradeBook):
                return False
            return self._grades == other._grades
    
    buch = GradeBook()
    buch.add(85)
    buch.add(92)
    print(buch)          # "Notenbuch mit 2 Noten" (nutzt __str__)
    print(len(buch))     # 2 (nutzt __len__)
    print(buch[0])       # 85 (nutzt __getitem__)
  • Mehr zu Dunder Methods erfahren Sie in der Python-Dokumentation.

  • Schauen Sie sich das Short-Video zu OOP an, das Klassen und Objekte ausführlicher erklärt.

Schleifen

  • while-Schleifen in Python funktionieren auf den ersten Blick recht ähnlich wie Schleifen in C. Sie erinnern sich vielleicht an den folgenden Code in C:

    // Demonstriert die while-Schleife
    
    #include <stdio.h>
    
    int main(void)
    {
        int i = 0;
        while (i < 3)
        {
            printf("miau\n");
            i++;
        }
    }
  • In Python sieht dieser Code wie folgt aus:

    # Demonstriert die while-Schleife
    
    i = 0
    while i < 3:
        print("miau")
        i += 1
  • Die for-Schleife in Python ist anders als in C. In Python iteriert for direkt über eine Sammlung von Werten:

    # for iteriert über eine Liste
    
    for i in [1, 2, 3]:
        print("miau")

    Hier durchläuft i nacheinander die Werte 1, 2 und 3. Das ist eleganter als die C-Variante mit Zählervariable.

  • Wenn wir nur eine bestimmte Anzahl von Durchläufen brauchen, verwenden wir range():

    # range(3) erzeugt die Sequenz 0, 1, 2
    
    for i in range(3):
        print("miau")

    Beachten Sie, dass i gar nicht benötigt wird. Python erhöht aber natürlich trotzdem in jedem Durchlauf (unnötigerweise) den Wert von i. Wenn man ausdrücken will, dass man den Wert gar nicht benötigt, kann man einen Unterstrich _ statt des i schreiben. Das macht den Code besser lesbar, weil es nicht zu Missverständnissen kommen kann (andernfalls könnte man sich irgendwann darüber wundern, dass i nirgends verwendet wird)

  • In ähnlicher Weise könnte man den obigen Code wie folgt ausdrücken:

    # Abstraktion mit Parametrisierung
    
    # Miau eine bestimmte Anzahl von Malen
    def miau(n):
        for _ in range(n):
            print("miau")
    
    
    miau(3)

    Beachten Sie, dass nun eine Funktion verwendet wird, um das Miauen auszulagern.

  • Um unser Verständnis von Schleifen und Iteration in Python zu vertiefen, erstellen wir eine neue Datei mit dem Namen uppercase.py, die sich an C-Code anlehnt, den wir früher schon einmal geschrieben haben:

    # Zeichenkette mit Großbuchstaben, ein Zeichen nach dem anderen
    
    before = input("Vorher: ")
    print("Nachher: ", end="")
    for c in before:
        print(c.upper(), end="")
    print()

    Beachten Sie, wie elegant wir mit for c in before über jeden Buchstaben im String iterieren können! In C müssten wir schreiben: for (int i = 0; i < strlen(before); i++) und dann mit before[i] auf jeden Buchstaben zugreifen. In Python geht das auch (mit for i in range(len(before)): print(before[i].upper())), aber die direkte Iteration über den String ist viel lesbarer.

    Beachten Sie auch, wie der benannte Parameter end= verwendet wird, um einen Parameter an die Funktion print zu übergeben. Wir brauchen hier einen benannten Parameter, weil print(…) für mehr Komfort so konstruiert wurde, dass es alle Positions-basierten Parameter mit Leerzeichen getrennt ausgibt. Dadurch gibt es keinen natürlichen Ort, wo wir einen Parameter zur Veränderung des Verhaltens platzieren können – und eine separate Funktion print_without_newline wäre nicht besonders elegant. Mit benannten Parametern können wir einer Methode unabhängig von der Position Parameter übergeben, auf die in der Methode zugegriffen werden kann. Bei print können wir dadurch das Standardverhalten verändern. Es gibt dann keinen Zeilenumbruch aus.

  • Bei der Lektüre der Dokumentation stellen wir fest, dass die upper()-Methode von str immer die gesamte Zeichenkette verarbeitet und wir daher weniger Code benötigen:

    # Zeichenkette auf einmal großschreiben
    
    before = input("Vorher: ")
    after = before.upper()
    print(f"Nachher: {after}")

    Beachten Sie, dass “upper” auf die gesamte Zeichenkette angewendet wird.

  • Oder noch kürzer

    # Zeichenkette auf einmal großschreiben
    
    before = input("Vorher: ")
    print(f"Nachher: {before.upper()}")

Vorschau: Was macht etwas “iterierbar”?

  • Sie haben gesehen, dass hinter in bei einer for-Schleife sowohl eine Liste ([1, 2, 3]) als auch ein String ("Hallo") stehen kann. Aber was passiert, wenn Sie for i in 42 schreiben? Python wirft einen Fehler: TypeError: 'int' object is not iterable.
  • Es scheint also so zu sein, dass das, was hinter in steht, etwas “Besonderes” sein muss. Dieses “Besondere” ist nicht fest in der Sprache kodiert, sondern es ist eine bestimmte Methode (__iter__), die implementiert sein muss, damit etwas “iterierbar” ist.
  • Mehr dazu erfahren Sie in der nächsten Vorlesung!

Abstraktion mit Funktionen

  • Wie wir heute schon angedeutet haben, können wir unseren Code verbessern, indem wir Funktionen verwenden und die Implementierungsdetails in Funktionen abstrahieren. Ändern Sie Ihren zuvor erstellten miau.py Code wie folgt:

    # Abstraktion
    
    def main():
        for i in range(3):
            miau()
    
    # Einmal miauen
    def miau():
        print("miau")
    
    
    main()

    Beachten Sie, dass die Funktion miau die Anweisung print abstrahiert. Beachten Sie auch, dass die Funktion main am Anfang der Datei definiert ist und am Ende der Datei aufgerufen wird. Dies ist eine gängiges Programmiermuster in Python. Funktionen können erst aufgerufen werden nachdem sie definiert wurden. Weil es das Konzept der Funktionsprototypen nicht gibt, rufen wir einfach main() ganz am Ende der Datei auf – zu diesem Zeitpunkt hat der Python-Interpreter die ganze Datei eingelesen und kennt alle Funktionen, die main() kann.

  • Wie aus C bekannt können wir Variablen zwischen Funktionen übergeben:

    # Abstraktion mit Parametrisierung
    
    def main():
        miau(3)
    
    
    # Miau eine bestimmte Anzahl von Malen
    def miau(n):
        for i in range(n):
            print("miau")
    
    
    main()

    Beachten Sie, dass miau jetzt eine Variable n benötigt. In der Funktion main können Sie miau aufrufen und ihm einen Wert wie 3 übergeben. Dann verwendet “miau” den Wert von “n” in der “for”-Schleife.

  • Wenn Sie den obigen Code lesen, werden Sie feststellen, dass Sie als C-Programmierer in der Lage sind, den obigen Code ganz leicht zu verstehen. Zwar sind einige Konventionen anders, aber die Bausteine, die Sie zuvor gelernt haben, sind für Sie in dieser neuen Programmiersprache nun schneller nachvollziehbar.

Standardwerte für Parameter

  • Funktionen können Standardwerte für Parameter haben:
    def begrüsse(name="Welt"):
        print(f"Hallo {name}")
    
    begrüsse()          # "Hallo Welt"
    begrüsse("Alice")   # "Hallo Alice"

Mehrere Rückgabewerte

  • Python-Funktionen können mehrere Werte zurückgeben:
    def get_kreis_info(radius):
        umfang = 2 * 3.14 * radius
        flaeche = 3.14 * radius * radius
        return umfang, flaeche
    
    # Aufruf mit Entpacken
    u, f = get_kreis_info(5)
    print(f"Umfang: {u}, Fläche: {f}")

Docstrings

  • Funktionen sollten mit einem Docstring dokumentiert werden:

    def quadrat(x):
        """Berechnet das Quadrat einer Zahl.
    
        Args:
            x: Die zu quadrierende Zahl
        Returns:
            Das Quadrat von x
        """
        return x * x

    Der Docstring steht direkt nach der Funktionsdefinition in dreifachen Anführungszeichen.

Keine Trunkierung bei Division und Fließkomma-Ungenauigkeit

  • Erinnern Sie sich, dass wir in C die Erfahrung gemacht haben, dass die Division einer ganzen Zahl durch eine andere zu einem ungenauen, trunkierten Ergebnis führen kann.

  • Schauen wir uns an, wie Python eine solche Division handhabt, indem wir den Code für calculator.py ändern:

    # Division mit ganzen Zahlen, Nachweis der fehlenden Trunkierung
    
    x = int(input("x: "))
    
    y = int(input("y: "))
    
    z = x / y
    print(z)

    Beachten Sie, dass automatisch ein Fließkomma-Wert ausgegeben wird – in C wäre das Ergebnis der Division hingegen auf die nächste Ganzzahl abgerundet (trunkiert) worden. Wenn Sie sich mehr Ziffern nach .333333 ausgeben lassen würden, würden Sie sehen, dass wir auch in Python mit Gleitkomma-Ungenauigkeit leben müssen.

  • Wir können diese Ungenauigkeit aufdecken, indem wir unsere Codes leicht abändern:

    # Fließkomma-Ungenauigkeit
    
    # Benutzer zur Eingabe von x auffordern
    x = int(input("x: "))
    
    # Benutzer zur Eingabe von y auffordern
    y = int(input("y: "))
    
    # x durch y dividieren
    z = x / y
    print(f"{z:.50f}")

    Hier sehen Sie ein Beispiel für eine weiterführende Syntax von Format-Strings, die so ähnlich funktioniert wie bei printf in C. Wir geben die Zahl mit 50 Nachkommastellen aus. Dadurch sehen wir die Ungenauigkeit von Fließkommazahlen.

Exceptions

  • Sehen wir uns noch einmal folgenden Code an:

    from cs50 import get_int
    
    def main():
    
        # Benutzer zur Eingabe von x auffordern
        x = get_int("x: ")
    
        # Benutzer zur Eingabe von y auffordern
        y = get_int("y: ")
    
        # Addition durchführen
        print(x + y)
    
    
    main()

    Wenn Sie bei diesem Programm statt einer Zahl einen String eingeben (z.B. cat), dann werden Sie erneut aufgefordert eine Zahl einzugeben. Die CS50-Bibliothek kümmert sich intern um die Fehlerbehandlung. Wie macht sie das?

  • Das beobachtete Verhalten ist nicht das Standardverhalten von Python. Das sehen wir, wenn wir versuchen, get_int nachzubauen.

  • Ändern Sie calculator.py wie folgt:

    # Implementiert get_int
    
    def get_int(prompt):
        return int(input(prompt))
    
    
    def main():
    
        # Benutzer zur Eingabe von x auffordern
        x = get_int("x: ")
    
        # Benutzer zur Eingabe von y auffordern
        y = get_int("y: ")
    
        # Addition durchführen
        print(x + y)
    
    
    main()

    Beachten Sie, dass die Eingabe von falschen Daten zu einem Fehler führt (ValueError) und das Programm abgebrochen wird.

  • In Python und anderen modernen Programmiersprachen gibt es ein leistungsfähiges Konstrukt zur Fehlerbehandlung, sogenannte Exceptions.

  • Die Idee ist folgende: Anstatt für alle möglichen Fehlerfälle explizit mit bedingten Anweisungen Prüfschritte einzubauen, „versuchen“ (engl. to try), wir den Code auszuführen und behandeln (man sagt: “fangen”, engl. to catch) dabei möglicherweise auftretende Ausnahmezustände. Dazu ändern wir unseren Code wie wie folgt:

    # Implementiert get_int mit einer Schleife
    
    def get_int(prompt):
        while True:
            try:
                return int(input(prompt))
            except ValueError:
                print("Not an integer")
    
    
    def main():
    
        # Benutzer zur Eingabe von x auffordern
        x = get_int("x: ")
    
        # Benutzer zur Eingabe von y auffordern
        y = get_int("y: ")
    
        # Addition durchführen
        print(x + y)
    
    
    main()

    Beachten Sie, dass der obige Code wiederholt versucht, den richtigen Datentyp abzurufen, und bei Bedarf zusätzliche Eingabeaufforderungen liefert. So ähnlich arbeitet auch get_int im cs50-Modul.

Testen mit pytest

  • pytest macht das systematische Testen einfach. Testfunktionen beginnen mit test_:

    # test_calculator.py
    import pytest
    
    def test_addition():
        assert 2 + 2 == 4
    
    def test_division_by_zero():
        with pytest.raises(ZeroDivisionError):
            result = 1 / 0
  • Tests ausführen:

    pytest test_calculator.py     # Einzelne Datei
    pytest -v                     # Alle Tests, ausführliche Ausgabe
  • Schauen Sie sich das Short-Video zu Fehlerbehandlung an, das Exceptions und pytest ausführlicher erklärt.

Vertiefung: Weitere Exception-Konzepte (nicht in der Vorlesung behandelt)

Die folgenden Konzepte sind im Short-Video erklärt.

Eigene Exceptions auslösen (raise)

  • Mit raise können Sie selbst Exceptions auslösen:
    def withdraw_money(balance, amount):
        if amount > balance:
            raise ValueError("Not enough money in account")
        return balance - amount
    
    try:
        new_balance = withdraw_money(100, 200)
    except ValueError as e:
        print(f"Error: {e}")  # "Error: Not enough money in account"

Mehrere Exception-Typen behandeln

  • Verschiedene Fehler können unterschiedlich behandelt werden:

    try:
        number = int(input("Zahl eingeben: "))
        result = 100 / number
    except ValueError:
        print("Bitte eine gültige Zahl eingeben")
    except ZeroDivisionError:
        print("Division durch Null nicht erlaubt")
  • Oder mehrere auf einmal behandeln:

    try:
        result = risky_operation()
    except (ValueError, ZeroDivisionError) as error:
        print(f"Fehler: {error}")

try – except – else

  • Der else-Block wird nur ausgeführt, wenn kein Fehler auftritt:
    try:
        result = int(input("Zahl: "))
    except ValueError:
        print("Ungültige Eingabe")
    else:
        print(f"Quadrat: {result ** 2}")  # Nur wenn Konvertierung erfolgreich

Listen und Dictionaries (Vorschau)

  • Python bietet mächtige eingebaute Datenstrukturen: Listen (list) und Wörterbücher (dict).

  • Eine Liste ist eine geordnete Sammlung von Werten:

    scores = [72, 73, 33]
    print(sum(scores) / len(scores))  # Durchschnitt: 59.33...
  • Ein Dictionary speichert Schlüssel-Wert-Paare und ermöglicht schnellen Zugriff:

    telefonbuch = {
        "Alice": "+49-123-456",
        "Bob": "+49-789-012"
    }
    print(telefonbuch["Alice"])  # +49-123-456
  • In der nächsten Vorlesung werden wir Listen und Dictionaries ausführlich behandeln und sehen, wie man sie geschickt für effiziente Datenverarbeitung einsetzt.

  • Schauen Sie sich schon mal das Short-Video zu Collections an, um einen Vorgeschmack zu bekommen.

Zusammenfassend

In dieser Vorlesung haben Sie gelernt, wie die Bausteine der Programmierung aus früheren Vorlesungen in Python genutzt werden können. Außerdem haben Sie gelernt, wie mit Pythons Syntax einfacher Code geschrieben werden kann und wie Sie gängige Python-Funktion nutzen können.

Vermutlich haben Sie in dieser Vorlesung einige der Konzepte auf Anhieb verstanden. Das liegt nicht nur daran, dass die Syntax von Python leichter zugänglich ist als die Syntax von C. Es liegt auch daran, dass die Fähigkeiten und das Wissen aus den letzten Wochen Ihnen den Einstieg in eine neue Programmiersprache erleichtern. Sie verfügen nun über eine solide Basis, mit der Sie sich selbst neue Programmiersprachen aneignen können!

Vielleicht können Sie jetzt erahnen, wie Sie durch die Herangehensweise, die wir in diesem Kurs verfolgen, eine neue Art des Lernens entdecken – neugierig zu sein (Was passiert eigentlich, wenn ich hier … eingebe?) und nachzuhaken, um den Dingen auf den Grund zu gehen (Wieso ist das eigentlich so?). Diese Art des Lernens wird Ihnen hoffentlich beim Erlernen weiterer Programmiersprachen - und vielleicht auch beim Lernen in anderen Bereichen - von Nutzen sein.

Konkret haben wir besprochen…

  • Python
  • Variablen und PEP 8
  • Typen
  • Bedingte Anweisungen (inkl. Truthiness, ternärer Operator, is vs ==)
  • Schleifen
  • Objektorientiertes Programmieren (Klassen, init, self, Properties, Dunder Methods)
  • Funktionen (Standardwerte, Multiple Returns, Docstrings)
  • Trunkierung und Fließkomma-Ungenauigkeit
  • Ausnahmen (try/except) und Testen mit pytest
  • Listen und Dictionaries (Vorschau)

Die Fortsetzung folgt in der nächsten Vorlesung!