zurück rauf weiter Englisch Index

Kapitel 9

Rohübersetzung -- bitte um Rückmeldungen über Fehler und Unklarheiten an glingl@aon.at

Tupel

9.1 Veränderbarkeit und Tupel

Bis jetzt haben wir zwei zusammengesetzte Datentypen kennengelernt: Strings, die aus Zeichen zusammengesetzt sind; und Listen, die aus Elementen von beliebigem Typ aufgebaut sind. Einer der Unterschiede zwischen diesen beiden war der, dass die Elemente einer Liste geändert werden können, die Zeichen eines Strings aber nicht. Mit anderen Worten: Strings sind unveränderbar und Listen sind veränderbar.

Es gibt in Python noch einen weiteren Datentyp, Tupel genannt, der dem Typ Liste ähnlich ist, aber unveränderbar. Syntaktisch ist ein Tupel eine Komma-separierte Liste von Werten:

>>> tuple = 'a', 'b', 'c', 'd', 'e'

Obwohl es nicht unbedingt notwendig ist, ist es üblich Tupel in runde Klammern einzuschließen:

>>> tuple = ('a', 'b', 'c', 'd', 'e')

Um ein Tupel mit nur einem Element zu erzeugen, muss man das letzte Komma anfügen:

>>> t1 = ('a',)
>>> type(t1)
<type 'tuple'>

Ohne das Komma behandelt Python ('a') als einen String in runden Klammern.

>>> t2 = ('a')
>>> type(t2)
<type 'string'>

Abgesehen von Syntax-Fragen sind die Operationen mit Tupeln dieselben wie die Operationen mit Listen. Der Index-Operator wählt ein Element aus einem Tupel aus.

>>> tuple = ('a', 'b', 'c', 'd', 'e')
>>> tuple[0]
'a'

Und der Abschnitt-Operator wählt einen Bereich von Elementen aus:

>>> tuple[1:3]
('b', 'c')

Wenn man aber versucht, ein Element des Tupels zu ändern, erhält man eine Fehlermeldung:

>>> tuple[0] = 'A'
TypeError: object doesn't support item assignment

Auch wenn man die Elemente eines Tupels nicht ändern kann, kann man es doch durch ein anderes Tupel ersetzen:

>>> tuple = ('A',) + tuple[1:]
>>> tuple
('A', 'b', 'c', 'd', 'e')

9.2 Tupel-Wertzuweisung

Gelegentlich ist es nützlich, die Werte von zwei Variablen zu vertauschen. Mit konventionellen Wertzuweisungen muß man dazu eine temporäre Variable verwenden. Um zum Beispiel a und b zu vertauschen:

>>> temp = a
>>> a = b
>>> b = temp

Wenn man das oft tun muß, ist es doch ein bißchen mühselig. Python stellt eine Art der Tupel-Wertzuweisung bereit, die dieses Problem elegant löst:

>>> a, b = b, a

Die linke Seite ist ein Tupel von Variablen, die rechte Seite ein Tupel von Werten. Jeder Wert wird der entsprechenden Variablen zugewiesen. Alle Ausdrücke auf der rechten Seite werden ausgewertet bevor eine Wertzuweisung ausgeführt wird. Dies macht Tupel-Wertzuweisung recht vielseitig.

Natürlich müssen die Anzahl der Variablen auf der linken Seite und die Anzahl der Werte auf der rechten Seite übereinstimmen:

>>> a, b, c, d = 1, 2, 3
ValueError: unpack tuple of wrong size

9.3 Tupel als Rückgabewerte

Funktionen können Tupel als Werte zurückgeben. Wir könnten zum Beispiel eine Funktion schreiben, die zwei Parameter vertauscht:

def swap(x, y):
  return y, x

Dann können wir den Rückgabewert einem Tupel aus zwei Variablen zuweisen.

a, b = swap(a, b)

In diesem Fall hat es keinen großen Vorteil, eine swap Funktion zu definieren. Es liegt sogar eine Gefahr in dem Versuch, swap in einer Funktion zu kapseln, wenn man es nämlich (naheliegenderweise) so versuchen würde:

def swap(x, y):      # fehlerhafte Version!
  x, y = y, x

Wenn wir diese Funktion folgendermaßen aufrufen:

swap(a, b)

dann sind a und x Aliasnamen für denselben Wert. Wenn man x innerhalb von swap ändert, wird x auf einen anderen Wert verweisen, das hat aber keine Auswirkung auf a in __main__. Ebenso hat die Änderung von y keine Auswirkung auf b.

Diese Funktion wird ausgeführt, ohne eine Fehlermeldung zu produzieren, aber sie tut nicht das, was beabsichtigt war. Dies ist ein Beispiel für einen logischen, oder semantischen, Fehler.

Übung: Zeichne ein Zustandsdiagramm für diese Funktion. um besser einzusehen, warum sie nicht richtig funktioniert.

9.4 Zufallszahlen

Die meisten Computerprogramme tun jedes Mal dasselbe, wenn sie ausgeführt werden. Man sagt, sie sind deterministisch. Determinismus ist normalerweise eine gute Sache, da wir doch erwarten, dass dieselbe Berechnung auch stets dasselbe Ergebnis liefert. Für manche Anwendungen möchten wir allerdings, dass der Computer unvorhesehbar agiert. Spiele sind ein offensichtliches Beispiel dafür, aber es gibt auch noch andere.

Ein Programm wirklich nichtdeterministisch zu machen stellt sich als ziemlich schwierig heraus. Es gibt aber Möglichkeiten zu erreichen, dass sie wenigstens nichtdeterministisch aussehen. Eine davon ist die Erzeugung von Zufallszahlen und von ihnen das Ergebnis eines Programms abhängig zu machen. Python hat eine eingebaute Funktion, die Pseudozufallszahlen erzeugt, die zwar nicht wirkliche Zufallszahlen im mathematischen Sinn sind, aber für unsere Zwecke ausreichen.

Der random Modul enthält eine Funktion namens random, die eine Gleitkommazahl zwischen 0.0 und 1.0 zurückgibt. Jedesmal, wenn man random aufruft, erhält man die nächste Zahl einer langen Folge. Um ein Beispiel zu sehen, kann man folgende Schleife ausführen:

import random

for i in range(10):
  x = random.random()
  print x

Um Zufallszahlen zwischen 0.0 und einer oberen Grenze, etwa high zu erzeugen, multipliziert man x mit high.

Übung: Erzeuge Zufallszahlen zwischen low und high.
Übung: Erzeuge ganze Zufallszahlen zwischen low and high, einschließlich der beiden Endpunkte.

9.5 Listen von Zufallszahlen

Der erste Schritt ist die Erzeugung einer Liste von Zufallswerten. randomList übernimmt eine ganze Zahl als Parameter, die die Länge der Liste angibt. Sie gibt eine Liste von Zufallszahlen mit dieser Länge zurück. Sie beginnt mit einer Liste von n Nullen. Bei jedem Schleifendurchlauf wird eines ihrer Elemente durch eine Zufallszahl ersetzt. Der Rückgabewert ist ein Verweis auf die vollständige Liste.

def randomList(n):
  s = [0] * n
  for i in range(n):
    s[i] = random.random()
  return s

Wir testen jetzt diese Funktion mit einer Liste von acht Elementen. Für den Zweck der Fehlersuche ist es besser, klein anzufangen-

>>> randomList(8)
0.15156642489
0.498048560109
0.810894847068
0.360371157682
0.275119183077
0.328578797631
0.759199803101
0.800367163582

Es wird angenommen, dass die von random erzeugten Zufallszahlen gleichmäßig verteilt sind. Das heißt, das jeder Wert gleich wahrscheinlich ist.

Wenn wir den Bereich der möglichen Werte in gleich große "Schachteln" aufteilen und abzählen, wie oft eine Zufallszahl in jede Schachtel fällt, dann sollten wir etwa die gleichen Anzahlen für jede erhalten.

Wir können diese Theorie überprüfen, indem wir ein Programm schreiben, das den Bereich in solche "Schachteln" aufteilt und die Anzahl der Werte in jeder abzählt.

9.6 Abzählen

Ein guter Zugang zu Problemen dieser Art ist, sie in Teilprobleme aufzuspalten und dabei insbesonderer nach solchen Ausschau zu halten, die zu einem Berechnungsmuster (Code-Muster) passen, das man schon kennt.

In diesem Fall wollen wir ein Liste von Zahlen durchlaufen und abzählen, wie oft ein Wert in einen gegebenen Bereich fällt. Das klingt bekannt. In Abschnitt haben wir ein Programm geschrieben, das einen String durchlaufen hat und dabei abgezählt hat, wie oft ein gegebener Buchstabe vorkam.

Wir werden also einmal das alte Programm kopieren und es für unser neues Problem anpassen. Das ursprüngliche Programm war:

count = 0
for char in fruit:
  if char == 'a':
    count = count + 1
print count

Im ersten Schritt ersetzen wir fruit durch list und char durch num. Das ändert das Programm nicht, sondern macht es nur lesbarer.

Im zweiten Schritt ändern wir die Überprüfung. Wir wollen nicht Buchstaben finden. Wir wollen wissen, ob num zwischen den gegebenen Werten low und high liegt.

count = 0
for num in list
  if low < num < high:
    count = count + 1
print count

Im letzten Schritt kapseln wir diesen Code in einer Funktion namens inBox. Parameter sind die Liste und die Werte von low und high.

def inBox(list, low, high):
  count = 0
  for num in list:
    if low < num < high:
      count = count + 1
  return count

Indem wir ein existierende Programm kopieren und dann abändern, können wir diese Funktion schnell schreiben und sparen eine Menge Zeit fürs debuggen. Man nennt dieses Verfahren der Programmentwicklung pattern matching, also: Verwenden passender Muster. Wenn man merkt, dass man ein Problem, das vorliegt, schon einmal gelöst hat, dann verwende man diese Lösung wieder.

9.7 Viele Schachteln

Wenn die Anzahl der Schachteln anwächst, wird inBox ein bisschen umständlich. Mit zwei Schachteln geht es ja noch:

low = inBox(a, 0.0, 0.5)
high = inBox(a, 0.5, 1)

Aber mit vier Schachteln ist es schon etwas mühselig:

box1 = inBox(a, 0.0, 0.25)
box2 = inBox(a, 0.25, 0.5)
box3 = inBox(a, 0.5, 0.75)
box4 = inBox(a, 0.75, 1.0)

Es liegen da zwei Probleme vor: das erste besteht darin, dass wir für jede Schachtel einen neuen Variablennamen erfinden müssen. Das andere darin, dass wir den Bereich für jede Schachtel berechnen müssen.

Wir werden das zweite Problem zuerst lösen. Wenn die Anzahl der Schachteln gleich numBoxes ist, dann ist die "Breite" jeder Schachtel gleich 1.0 / numBoxes.

Wir werden eine Schleife verwenden um den Bereich jeder Schachtel zu berechnen. Die Schleifenvariable i zählt von 0 bis numBoxes-1:

boxWidth = 1.0 / numBoxes
for i in range(numBoxes):
  low = i * boxWidth
  high = low + boxWidth
  print low, "to", high

Die untere Grenze für jede Box berechnen wir, indem wir die Schleifenvariable mit der Breite der Schachtel multiplizieren. Die obere Grenze is nun gerade eine boxWidth entfernt.

Mit numBoxes = 8, erhalten wir folgende Ausgabe:

0.0 to 0.125
0.125 to 0.25
0.25 to 0.375
0.375 to 0.5
0.5 to 0.625
0.625 to 0.75
0.75 to 0.875
0.875 to 1.0

Man überzeugt sich, dass jede Schachtel gleich breit ist, dass sie sich nicht überlappen und dass sie den ganzen Bereich von 0.0 bis 1.0 überdecken.

Kommen wir nun zu unserem ersten Problem zurück. Wir müssen acht ganze Zahlen speichern, wobei wir die Schleifenvariable abenützen, um jeweils eine zu bezeichnen. Wahrscheinlich fällt einem jetzt ein: Liste!

Wir müssen unserer Liste von Schachteln außerhalb der Schleife erstellen, weil wir das ja nur einmal zu tun brauchen. Innerhalb der Schleife werden wir inBox mehrmals aufrufen und das i.te Element der Liste auf neuesten Stand bringen:

numBoxes = 8
boxes = [0] * numBoxes
boxWidth = 1.0 / numBoxes
for i in range(numBoxes):
  low = i * boxWidth
  high = low + boxWidth
  boxes[i] = inBox(list, low, high)
print boxes

Mit einer Liste von 1000 Werten produziert dieser Code beispielsweise folgende "Schachtel"-Liste:

[138, 124, 128, 118, 130, 117, 114, 131]

Diese Zahlen sind alle ziemlich nahe an 125, was wir gar nicht anders erwartet haben. Zumindest sind sie nahe genug, dass wir daran glauben können, dass der Zufallsgenerator funktioniert.

Übung: Teste diese Funktion mit einigen längeren Listen und prüfe ob sich für die Anzahl der Werte in jeder "Schachtel" Abweichungen ergeben.

9.8 Eine Lösung in einem Durchlauf

Obwohl dieses Programm funktioniert, ist es nicht so effizient, wie es sein könnte. Bei jedem Aufruf von inBox durchläuft es die ganze Liste. Wenn man die Anzahl der Schachteln vergrößert, ergibt das eine ganze Menge Durchläufe.

Es wäre besser nur einen Durchlauf durch die Liste zu machen und dabei für jeden Wert den Index der Schachtel zu berechnen, in die er hinein gehört. Dann kann man den entsprechenden Zähler um eins erhöhen.

Im vorigen Abschnitt nahmen wir einen Index i und multiplizierten ihn mir der boxWidth um die untere Grenze für eine gegebene Schachtel zu finden. Nun wollen wir einen Wert zwischen 0.0 und 1.0 nehmen und den Index der Schachtel finden, in die er hineingehört.

Da dieses Problem die Umkehrung des vorhergehenden Problems ist, werden wir wohl durch boxWidth dividieren müssen, anstatt damit zu multiplizieren.

Und weil boxWidth = 1.0 / numBoxes ist, ist die Division durch boxWidth gleichwertig mit der Multiplikation mit numBoxes. Wenn wir eine Zahl im Bereich von 0.0 bis 1.0 mit numBoxes multiplizieren, erhalten wir eine Zahl im Bereich von 0.0 bis numBoxes. Wenn wir das auf die nächstkleinere ganze Zahl abrunden, bekommen wir genau das, was wir suchen     einen Box-Index:

numBoxes = 8
boxes = [0] * numBoxes
for i in list:
  index = int(i * numBoxes)
  boxes[index] = boxes[index] + 1

Dabei haben wir die int - Funktion benützt, um eine Gleitkommazahl in eine Ganzzahl umzuwandeln.

Wäre es möglich, dass diese Berechnung einen Index erzeugt, der außerhalb des gültigen Bereichs liegt (also entweder negativ ist, oder größer als len(boxes)-1)?

Eine solche Liste von "Schachteln", die die Anzahlen einer Menge von Werten den entsprechenden Bereichen enthalten, wird ein Histogramm genannt.

Übung: Schreibe eine Funktion namens histogramm, die eine Liste von Werten und eine Anzahl von "Schachteln" als Paramter hat und ein Histogramm mit der gegebenen Anzahl von "Schachteln" zurückgibt.

9.9 Glossar

unveränderbarer Typ
Ein Typ, dessen Elemente nicht verändert werden können. Wertzuweisungen zu Elementen oder Abschnitten verursachen für unveränderbare Typen einen Fehler.
immutable type
A type in which the elements cannot be modified. Assignments to elements or slices of immutable types cause an error.
veränderbarer Typ
Ein Datentyp, dessen Elemente verändert werden können. Alle veränderbaren Typen sind zusammengesetzte Typen. Listen und auch der Typ Dictionary sind veränderbare Datentypen, Strings und Tupel sind nicht veränderbar.
mutable type
A data type in which the elements can be modified. All mutable types are compound types. Lists and dictionaries are mutable data types; strings and tuples are not.
Tupel
Ein Sequenz-Typ, der dem Typ der Liste ähnlich ist, außer dass er unveränderbar ist. Tupel können überall dort verwendet werden, wo ein unveränderbarer Typ gebraucht wird, zum Beispiel als Schlüssel in einem Dictionary.
tuple
A sequence type that is similar to a list except that it is immutable. Tuples can be used wherever an immutable type is required, such as a key in a dictionary.
Tupel-Zuweisung
Eine Zuweisung zu allen Elementen eines Tupels in einer einzigen Wertzuweisung. Tupel-Zuweisung geschieht parallel und nicht in Folge nacheinander. Sie ist daher z. B. nützlich für das Vertauschen von Werten.
tuple assignment
An assignment to all of the elements in a tuple using a single assignment statement. Tuple assignment occurs in parallel rather than in sequence, making it useful for swapping values.
deterministisch
Ein Programm, das bei jedem Aufruf dasselbe macht.
deterministic
A program that does the same thing each time it is called.
Pseudozufallszahlen
Ein Folge von Zahlen, die wie zufällig aussieht, obwohl sie in Wahrheit das Ergebnis einer deterministischen Berechnung ist.
pseudorandom
A sequence of numbers that appear to be random but that are actually the result of a deterministic computation.
Histogramm
Eine Liste von ganzen Zahlen, in der jedes Element angibt, wie oft igendetwas vorgekommen ist.
histogram
A list of integers in which each element counts the number of times something happens.
pattern matching
Ein Programmentwicklungsverfahren, bei dem bereits bekannt Berechnungsmuster indentifiziert und für die Lösung eines ähnlichen Problems angepasst werden.
pattern matching
A program development plan that involves identifying a familiar computational pattern and copying the solution to a similar problem.


zurück rauf weiter Englisch Index