3.9 LISTEN

 

 

EINFÜHRUNG

 

Manchmal musst du Werte speichern, die zusammen gehören, aber deren genaue Anzahl du beim Erstellen des Programms nicht kennst. Du brauchst also eine Datenstruktur, in der du mehrere Werte speichern kannst. Die Struktur sollte so flexibel sein, dass sie auch die Reihenfolge der hinzugefügten Werte berücksichtigt. Es ist naheliegend, eine Aneinanderreihung von einfachen Behältern dafür einzusetzen, von der du bereits gehört hast, nämlich eine Liste. Hier erfährst du nun ausführlich, wie man mit Listen umgeht.

Eine Liste mit 5 Elementen

Eine Liste besteht aus einzelnen Elementen, die nacheinander angeordnet sind. Im Gegensatz zu einer unstrukturierten Menge von Elementen gibt es also ein erstes und ein letztes Element und alle anderen, ausser dem letzten, haben einen Nachfolger.

Listen (und ähnliche Container) sind für die Programmierung enorm wichtig. Die möglichen Operationen mit Listen sind sehr anschaulich. Die wichtigsten sind:

Elemente hinzufügen (am Ende, am Anfang, irgendwo dazwischen)

Element lesen

Elemente verändern

Elemente entfernen
Alle Elemente durchlaufen

Elemente sortieren

Nach Elementen suchen

In Python kannst du in Listen beliebige Daten speichern, also nicht nur Zahlen. Die einzelnen Elemente können sogar einen unterschiedlichen Typ haben, du kannst also beispielsweise Zahlen und Buchstaben in der gleichen Liste speichern.

PROGRAMMIERKONZEPTE: Container, Liste, Vorgänger, Nachfolger, Referenzvariable

 

 

NOTENLISTE

 

Eine Liste kannst du wie eine Variable auffassen. Sie hat also auch einen Namen und einen Wert, nämlich ihre Elemente. Du erzeugst sie mit einem eckigen Klammerpaar, z.B. erzeugt li = [1, 2, 3] eine Liste mit den Elementen 1, 2 und 3. Eine Liste kann auch leer sein. Du definierst eine leere Liste mit li = [].

Eine typische Verwendung von Listen ist ein Notenbüchlein, wo du die Noten in einem bestimmten Schulfach eingetragen hast, sagen wir einmal Biologienoten. Zu Beginn des Semesters hast du eine leere Liste, in Python ausgedrückt bioNoten = [].  Das Hineinschreiben von Noten entspricht dem Hinzufügen von Listenelementen, was du in Python mit dem Befehl append() machst, für eine Note 5 also: bioNoten.append(5). Du kannst die Liste jederzeit mit einem print-Befehl ansehen, du schreibst einfach print bioNoten.

Willst du den Notendurchschnitt berechnen, so musst du die Liste durchlaufen. Das kannst du sehr einfach und elegant mit einer for-Schleife machen, denn

for note in bionoten:

kopiert der Reihe nach jeden Listenwert in die Variable note und du kannst diese im Schleifenkörper verwenden.

bioGrades = []
bioGrades.append(5.5)
print(bioGrades)
bioGrades.append(5)
print(bioGrades)
bioGrades.append(5.5)
print(bioGrades)
bioGrades.append(6)
print(bioGrades)
count = 0
for note in bioGrades:
    count += note
print("Average: " + str(count / len(bioGrades)))
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Mit der Methode append() fügst du neue Elemente am Ende der Liste an.

Die eingebaute Funktion len() gibt dir die aktuelle Länge der Liste zurück. Beachte den interessanten Trick mit der Variable count, wie man die Summe bildet, um dann den Durchschnitt zu berechnen. Du kannst aber auch mit der eingebauten Funktion sum(bioNoten) die Summe direkt erhalten.

 

 

LISTE MIT FESTER ANZAHL VON ELEMENTEN

 

Oft ist dir bereits bei der Erstellung des Programms bekannt, wie lang ein Listenbehälter sein muss und dass alle Elemente denselben Datentyp haben. In vielen Programmiersprachen nennt man eine solche Datenstruktur einen Array. Auf die einzelnen Elemente greift man üblicherweise über ihren Index zu. In Python gibt es diesen Datentyp mit festerer Länge nicht und man verwendet dazu eine Liste.

Das Programm definiert ein Viereck als eine Liste mit 4 Eckpunkten (diese werden ebenfalls als eine Liste mit 2 Koordinaten definiert). Damit du von Anfang an mit Indizes auf die Viereckliste zugreifen kannst, erzeugst du eine Liste mit 4 Nullen viereck = [0, 0, 0, 0]. Man kann dazu die Kurzschreibweise viereck = [0] * 4 verwenden.

Nachher kopierst du die 4 Eckpunkte hinein. DAbei werden die Nullen durch Punktlisten ersetzt. Mit einer for-Schleife stellst du das Viereck dar.

 
from gpanel import *

pA = [0, -3]
pB = [5, -3]
pC = [0, 7]
pD = [-5, 7]

makeGPanel(-10, 10, -10, 10)
line(-10, 0, 10, 0)
line(0, -10, 0, 10)

polygon = [0] * 4  # list with 4 elements, initialized with 0
polygon[0] = pA
polygon[1] = pB
polygon[2] = pC
polygon[3] = pD

for i in range(4):
    k = i + 1
    if k == 4:
        k = 0
    line(polygon[i], polygon[k])
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Wenn du bei der Erstellung des Programms die Listenlänge bereits kennst, erzeugst du eine Liste mit den Initialisierungswerten 0 und greifst dann über den Index auf die Elemente zu.

Merke dir, dass der Index des ersten Elements 0 und der Index des letzten Elments len(list) - 1 ist.

 

 

ELEMENTE EINFÜGEN UND LÖSCHEN

 

Das Programm zeigt, wie  eine Textverarbeitung funktioniert. Die eingegebenen Buchstaben werden in eine Buchstabenliste eingefügt. Es ist klar, dass man zu Beginn nicht weiss, wieviele Buchstaben du eingeben wirst, also ist eine Liste die ideale Datenstruktur. Zudem siehst du einen Textcursor, der mit einem Mausklick an irgend eine Stelle des Text gesetzt werden kann.

Tippst du eine Buchstabentaste, so wird der Buchstaben rechts vom Cursors eingefügt und die Liste wächst, tippst die die Backspace-Taste, so wird der links neben dem Cursor stehende Buchstaben gelöscht und die Liste schrumpft.

Um das Ganze anschaulich zu gestalten, schreibst du die Buchstaben als Text mit einer Text- und Hintergrundfarbe in ein GPanel. Dabei durchläufst du die Liste mit einem Listenindex i.

 
from gpanel import *

BS = 8
SPACE = 32
DEL = 127

def showInfo(key):
    text = "List length = " + str(len(letterList))
    if key != "":
        text += ". Last key code = " + str(ord(key))
    setStatusText(text)  
        
def updateGraphics():
    clear()
    for i in range(len(letterList)):
        text(i, 2, letterList[i], Font("Courier", Font.PLAIN, 24), 
              "blue", "light gray")
    line(cursorPos - 0.2, 1.7, cursorPos - 0.2, 2.7) 

def onMousePressed(x, y):
    setCursor(x)
    updateGraphics()

def setCursor(x):
    global cursorPos
    pos = int(x + 0.7)
    if pos <= len(letterList):    
       cursorPos = pos

makeGPanel(-1, 30, 0, 12, mousePressed = onMousePressed)

letterList = []
cursorPos = 0
addStatusBar(30)
setStatusText("Enter Text. Backspace to delete. Mouse to set cursor.")
lineWidth(3)

while True:
    delay(10)
    key = getKey()
    if key == "":
        continue
    keyCode = ord(key)
    if keyCode == BS:  # backspace
        if cursorPos > 0:
            cursorPos -= 1
            letterList.pop(cursorPos)
    elif keyCode >= SPACE and keyCode != DEL:      
        letterList.insert(cursorPos, key)
        cursorPos += 1
    updateGraphics()
    showInfo(key)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Du hast bereits früher gelernt, dass du auf einzelne Elemente einer Liste über den Listenindex zugreifen kannst, der bei Null beginnt. Du verwendest dazu die eckigen Klammern, also letterList[i].  Der Index muss immer im Bereich 0 und Listenlänge - 1 liegen. Verwendest du eine for in range()-Struktur, so ist der Stoppwert gerade die Länge der Liste.

Mit dem Index darfst du aber nie auf ein Element zugreifen, dass es gar nicht gibt. Fehler mit ungültigen Listenindizes gehören zu den häufigsten Programmierfehlern. Passt du nicht auf, so kriegst du Programme, die unter Umständen manchmal funktionieren und manchmal absterben.

Um zu testen, welche Taste gedrückt wurde, kannst du getKey() verwenden. Diese Funktion kehrt nach dem Aufruf sofort zurück und liefert den Buchstaben der zuletzt gedrückten Taste als String oder einen leeren String, falls keine Taste gedrückt wurde.

 

 

BEREITS EIN PROFIPROGRAMM

 

Mit deinem Wissen bist nun bereits in der Lage, einen Grafen zu visualisieren [mehr... Graph Drawing ist ein ganzes Themengebiet der Informatik]

Die Aufgabenstellung (auch "Pflichtenheft" genannt) ist die Folgende:

In einem Grafikfenster kannst du mit rechtem Mausklick gefüllte Kreise erzeugen, die als Knoten eines Grafen aufgefasst werden, wo die Knoten untereinander mit Strecken, Kanten genannt, verbunden sind. Fährst du mit der Maus auf einen Knoten, so kannst du diesen mit einem linken Maus-Press und nachfolgendem Drag herumziehen und der Graf wird ständig aktualisiert. Klickst du mit mit einem rechten Mausklick auf einen bereits bestehenden Knoten, so wird dieser entfernt.

Es ist gescheit, dass du komplexere Aufgaben so löst, indem du zuerst eine Teilaufgabe erledigst, die noch nicht das ganze Pflichtenheft erfüllt. Du schreibst beispielsweise zuerst ein Programm, mit dem du per Mausklick Knoten erzeugen kannst. Sie sollen zwar mit allen bereits vorhandenen Knoten verbunden werden aber du kannst sie noch nicht verschieben.

Es liegt auf der Hand, den Graf mit einer Liste graph zu modellieren, in der du die Knotenpunkte speicherst.

 

Die Knoten selbst sind Punkte mit zwei Koordinaten P(x, y), die du - wie bereits früher - mit einer Punktliste [x, y] modellierst. Es liegt also eine Liste vor, die als Elemente wiederum Listen (allerdings mit fester Länge 2) enthält. Das Verbinden der Knoten erreichst du über eine doppelte for-Schleife, wobei du acht geben musst, dass die Knoten nur einmal verbunden werden.

from gpanel import *

def drawGraph():
    clear()
    for pt in graph:
        move(pt) 
        fillCircle(2)
    
    for i in range(len(graph)):
        for k in range(i, len(graph)):
            line(graph[i], graph[k])

def onMousePressed(x, y):
    pt = [x, y]
    graph.append(pt)
    drawGraph()

graph = []
makeGPanel(0, 100, 0, 100, mousePressed = onMousePressed)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

Als nächstes baust du nun das Ziehen und Löschen der Knoten ein. Dazu brauchst du auch die rechte Maustaste. Beim Ziehen ist es wichtig zu wissen, welcher Knoten gerade gezogen wird. Diesen kannst du dir über seinen Index iNode in der Liste graph merken. Wird kein Knoten gezogen, ist iNode = -1. Du berechnest mit dem Satz von Pythagoras in der Funktion near(x, y) den Abstand des Punktes P(x,y) zu allen Knoten. Sobald eines der Abstandsquadrate kleiner als 10 ist, brichst du die Berechnung ab und gibst den Index des Knotens zurück. Dabei siehst du, dass man eine Funktion mit return auch mitten im Ablauf verlassen kann [ mehr... Das vorzeitige Herausspringen aus einer Funktion kann zu unstrukturierten Programmen führen und ist deshalb bei vielen Programmierern verpönt].

Alles andere ist schöne Programmierarbeit, die du mit deinem bisherigen Wissen auch allein zu Stande bringen würdest.

 

 
from gpanel import *

def drawGraph():
    clear()
    for i in range(len(graph)):
        move(graph[i]) 
        if i == iNode:
            setColor("red")
        else:
            setColor("green")              
        fillCircle(2)
  
    setColor("blue")
    for i in range(len(graph)):
        for k in range(i, len(graph)):
            line(graph[i], graph[k])

def onMousePressed(x, y):
    global iNode   
    if isLeftMouseButton():
        iNode = near(x, y)
    if isRightMouseButton():
        index = near(x, y)
        if index != -1:
            graph.pop(index)
            iNode = -1
        else:
            pt = [x, y]
            graph.append(pt)
    drawGraph()

def onMouseDragged(x, y):
    if isLeftMouseButton():
        if iNode == -1:
            return        
        graph[iNode] = [x, y]
        drawGraph()

def onMouseReleased(x, y):
    global iNode
    if isLeftMouseButton():
        iNode = -1
        drawGraph()

def near(x, y):
    for i in range(len(graph)):
        p = graph[i]
        d = (p[0] - x) * (p[0] - x) + (p[1] - y) * (p[1] - y)
        if  d < 10:
            return i
    return -1

graph = []
iNode = -1
makeGPanel(0, 100, 0, 100, 
              mousePressed = onMousePressed, 
              mouseDragged = onMouseDragged,
              mouseReleased = onMouseReleased)
addStatusBar(20) 
setStatusText("Right mouse button to set nodes, left button to drag")             
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Das Programm ist voll eventgesteuert. Der Hauptblock definiert lediglich zwei globale Variablen und initialisiert das Grafikfenster. Bei jeder Aktion wird das ganze Grafikfenster gelöscht und mit der aktuellen Situation des Grafen neu aufgebaut.

Die wichtigsten Operationen mit Listen

 
 

li = [1, 2, 3, 4]

li = [1, "a", [7 , 5]]

li[i]

li[start:end]

li[start:end:step]

li[start:]

li[:end]

li.append(element)

li.insert(i, element)

li.extend(li2)

li.index(element)

li.pop(i)

pop()

li1 + li2

li1 += li2

li * 4

[0] * 4

len(li)

del li[i]

del li[start:end]

del li[:]

li.reverse() 

li.sort() 

x in li

x not in li

Liste mit den Zahlen 1, 2, 3, 4 definieren

Liste mit unterschiedlichen Datentypen definieren

Auf Listenelement mit Index i zugreifen

Neue Teilliste mit Elementen start bis end,  aber ohne end

Neue Teilliste mit Elementen start bis end mit Schritt step

Neue Teilliste mit allen Elementen von start an

Neue Teilliste von ersten Element bis end, aber ohne end

Anfügen ans Ende

Einfügen an Stelle i (Element i rutscht nach rechts)

Elemente aus li2 anfügen (Konkatenation)

Sucht das erste Vorkommen und gibt dessen Index zurück

Entfernt das Element mit Index i und gibt es zuück

Entfernt das letzte Element und gibt es zurück

Gibt die Konkatenation von li1 und li2 in neuer Liste zurück

Ersetzt li1 durch die Konkatenation von li1 und li2

Neue Liste mit Elementen von li viermal wiederholt

Neue Liste mit der Länge 4 (jedes Element Zahl 0)

Gibt die Anzahl Listenelemente zurück

Entfernt das Element mit Index i

Entfernt alle Elemente von start bis end, aber ohne end

Entfernt alle Elemente, es bleibt eine leere Liste

Kehrt die Liste um (letztes Element wird das erste)

Sortiert die Liste (Vergleich mit Standardverfahren)

Gibt True zurück, falls x in der Liste enthalten ist

Gibt True zurück, falls x nicht in der Liste enthalten ist

 

Die Notation mit den eckigen Klammern nennt man Slice-Operation. start und end sind Indizes der Liste. Die Slice-Operation funktioniert ähnlich wie die Parameter von range().

 

 

AUFGABEN

 

1.


Lese mit inputFloat("prompt", False) eine beliebige Anzahl von Noten ein. Drückst du den Abbrechen-Knopf, so wird der Durchschnitt in der Konsole ausgeschrieben. Beachte, dass du den Parameterwert False verwenden musst, damit das Programm nicht beendet wird, wenn du Abbrechen klickst. Beim Abbruch  wird der spezielle Wert None zurückgegeben, auf den du wie üblich mit if testen kannst.


2.


Erweitere das oben stehende Editor-Programm unter Verwendung der Slice-Notation so, dass bei jedem rechten Mausklick der Beginn des Satzes bis und mit dem ersten Leerschlag weggeschnitten wird.


3.


Bei jedem Mausklick soll ein neues Bild eines Fussballs (football.gif) an der Stelle des Mauscursors erscheinen. Alle Fussbälle bewegen sich ständig auf dem Bildschirm hin und her. Orientiere dich am Fussball-Beispiel im Kapitel Animationen. Du kannst das Programm etwas optimieren, indem du das Fussbaldbild nur einmal mit img = getImage("sprites/football.gif") lädst und zum Zeichnen img der Funktion image() übergibst.


 

4.


Mit der linken Maustaste werden Eckpunkte eines Polygons gewählt und in einer Liste gespeichert. Mit dem rechten Mausklick soll ein rot gefülltes Polygon gezeichnet werden.

Verwende für das Zeichnen des Polygons die Funktion fillPolygon(liste).


 




 

 

ZUSATZSTOFF:


 

VERÄNDERLICHE UND UNVERÄNDERLICHE DATENTYPEN

 

In Python werden alle Datentypen als Objekte gespeichert, also auch die numerischen Typen (integer, float, long, complex). Wie du weisst, greifst du auf ein Objekt über seinen Namen zu. Man sagt auch, dass der Name auf das Objekt verweist oder an das Objekt gebunden wird. Eine solche Variable heisst darum auch Referenzvariable.

Auf ein bestimmtes Objekt kann mehr als ein Name verweisen. Man nennt einen weiteren Namen auch ein Alias. Wie du damit umgehst, zeigt das folgende Beispiel.

Ein Dreieck wird durch drei Eckpunktlisten a, b, c festgelegt. Mit der Anweisung a_alias = a erzeugst du einen Alias von a, so dass a und a_alias auf die gleiche Liste verweisen. Veränderst du die Eckpunktliste mit dem Namen a, so wird logischerweise beim Zugriff mit dem Namen a_alias die Änderung auch sichtbar, da a und a_alias auf dieselbe Liste verweisen.

 


from gpanel import *

makeGPanel(-10, 10, -10, 10)

a = [0, 0]  
a_alias = a
b = [0, 5]
c = [5, 0]

fillTriangle(a, b, c)
a[0] = 1
setColor("green")
fillTriangle(a_alias, b, c)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

Da Zahlen auch Objekte sind, erwartest du dasselbe Verhalten, falls du für Eckpunktskoordinaten Zahlen verwendest. Das folgende Beispiel zeigt aber ein anderes Verhalten. Änderst du xA, so ändert sich der Wert von xA_alias nicht.

 


from gpanel import *

makeGPanel(-10, 10, -10, 10)

xA = 0
yA = 0
xA_alias = xA
yA_alias = yA
xB = 0
yB = 5
xC = 5
yC = 0

fillTriangle(xA, yA, xB, yB, xC, yC)
xA = 1
setColor("green")
fillTriangle(xA_alias, yA_alias, xB, yB, xC, yC)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

Was ist die Erklärung dafür? Der Grund liegt darin, dass Zahlen unveränderliche Objekte sind und die Anweisung xA = 1 eine neue Zahl erzeugt. xA_alias ist also immer noch 0.

Der Unterschied zwischen unveränderlichen und veränderlichen Datentypen zeigt sich auch bei der Parameterübergabe an Funktionen. Wird ein unveränderliches Objekt übergeben, so kann im Innern der Funktion das übergebene Objekt nicht verändert werden. Wird hingegeben ein veränderliches Objekt übergeben, so kann die Funktion das Objekt verändern. Eine solche Veränderung nennt man einen Neben- oder Seiteneffekt. Es gehört zum guten Programmierstil, Seiteneffekte nur sparsam einzusetzen, weil sie zu schwer auffindbaren Fehlverhalten führen können.

Im folgenden Beispiel verändert die Funktion translate() die übergebenen Eckpunktlisten.

 


from gpanel import *

def translate(pA, pB, pC):
  pA[0] = pA[0] + 5
  pA[1] = pA[1] + 2
  pB[0] = pB[0] + 5
  pB[1] = pB[1] + 2
  pC[0] = pC[0] + 5
  pC[1] = pC[1] + 2

makeGPanel(-10, 10, -10, 10)

a = [0, 0]  
b = [0, 5]
c = [5, 0]
fillTriangle(a, b, c)
translate(a, b, c)
setColor("green")
fillTriangle(a, b, c)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

In Python werden alle Daten als Objekte gespeichert, aber gewisse Objekte gelten als unveränderlich (immutable). Es sind dies: numerische Datentypen, string, byte, tuple.

Alle anderen Datentypen sind veränderlich (mutable). Weist man einer Variablen eines unveränderlichen Datentyp einen neuen Wert zu, so wird ein neues Objekt erzeugt.

Werden einer Funktion veränderliche Objekte übergeben, so kann die Funktion die  Objekte verändern, unveränderliche Objekte sind aber vor solchen Veränderungen geschützt.