Algorithmische Abstraktion mit Python

Werbung
Kapitel 3
Algorithmische Abstraktion
mit Python
3.1
Allgemeines zu Python
Python ist eine imperative und objekt-orientierte Programmiersprache mit dynamischer Typisierung. Sie ist gut geeignet zum schnellen Ausprobieren von
Algorithmen (rapid prototyping) und auch als Script-Sprache.
Sie hat als Basiswerte ganze Zahlen, Gleitkommazahlen (Double), und Strings.
Sie hat eine umfangreiche Fehlerbehandlung zur Laufzeit (Exception-Handling).
Es gibt bereits vordefinierte Datentypen wie Tupel und Listen und auch vordefinierte Listenfunktionen. Auf weitere Möglichkeiten von Python wie eigene
Datentypen, Klassen, Methoden, Aus- und Eingabe werden wir hier nicht explizit eingehen.
3.2
Programmierung in Python
Wir verwenden den Interpreter in Python zur Auswertung von Definitionen und
Ausdrücken.
Ziel ist nicht die volle Programmiersprache Python zu lernen, sondern die typischen Programmiermethoden und das Speicher- und Auswertungsmodell einer
imperativen Programmiersprache zu verstehen.
Zunächst führen wir nur die Programmierung ohne Listen und Listenfunktionen
ein.
3.2.1
Python: Struktur eines imperativen Programms
Ein Python-Programm-File besteht, ähnlich wie ein Haskell-Programm,
aus Definitionen von Funktionen. Es können Deklarationsteile wie ImportDeklarationen enthalten sein.
1
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
2
Die wesentlichen Bausteine einer Funktionsdefinition sind Befehle (Anweisungen, Kommandos), wie Zuweisungen Sprungbefehle, Ein/Ausgabebefehle,
usw.. Die meisten Befehle haben das Ziel, irgendeinen Seiteneffekt zu bewirken
wie Änderung des Speichers, Ausgabe auf einen Drucker, u.ä. oder den aktuellen
Zustand zu ermitteln.
Innerhalb der Anweisungen gibt es auch die Möglichkeit Ausdrücke zu schreiben, deren Auswertung ähnlich zu Haskell erfolgt, allerdings ist es immer die
strikte Auswertungsreihenfolge. In diesen Ausdrücken ist die Verwendung selbstdefinierter Funktionen möglich, wobei auch die Ergebnisse der Funktionsaufrufe
(Returnwert) bei der Berechnung verwendet wird.
3.2.2
Zuweisung und Kontrollstrukturen in Python
Python erlaubt die Eingabe von arithmetischen Ausdrücken. Man kann den Interpreter wie einen Taschenrechner, aber mit änderbarem Speicher, verwenden:
>>>
>>>
>>>
>>>
25
>>>
5.0
>>>
a = 3
b = 4
c = a**2 + b**2
c
print math.sqrt(c)
Wir werden die Syntax von wesentlichen Konstrukten in Python nun kurz
erklären, für weitere Konstrukte sowie Details zur Syntax von Python sei auf
Tutorials und Handbücher verwiesen.
Die Syntax der Zuweisung (Assignment) ist:
variable = ausdruck
Zunächst wird das Ergebnis des Ausdruckes berechnet, wobei die Werte von
Variablen im Speicher nachgeschaut werden. Das Resultat ist ein im Speicher
liegendes “Objekt”. Objekte können hierbei einfache Zahlen, Strings aber auch
komplexe Datenstrukturen sein. Anschließend weist die Zuweisung der Variablen als Wert das Objekt zu. Alternative Sichtweise ist, dass das Objekt als
Name den Namen der Variablen erhält. Ein Objekt kann durchaus mehrere Namen besitzen. Die Deklaration der Variablen(namen) geschieht in Python mit
der ersten Zuweisung, Variablen müssen somit nicht explizit vorher deklariert
werden.
Man kann auch arithmetische Operatoren mit der Zuweisung verknüpfen:
x op = Ausdruck
ist möglich:
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
>>>
>>>
>>>
20
>>>
>>>
100
3
a= 100
a /= 5
a
a *= 5
a
Es ist auch eine (scheinbar parallele) Mehrfach-Zuweisung möglich:
x1 , . . . xn = e1 , . . . , en
wobei ei Ausdrücke sind.
Die Auswertung des Ausdrucks
x1 , . . . xn = s1 , . . . , sn
ist folgendermaßen: Der Wert aller Ausdrücke si wird zuerst ermittelt, und
danach werden den Variablennamen xi die Werte (d.h. Objekte ) si zugewiesen.
Das if-then-else-Konstrukt hat in Python die Syntax:
if Bedingung:
Anweisungen wenn Bedingung wahr
else:
Anweisungen, wenn Bedingung falsch
Eine Besonderheit von Python ist, dass es das Schlüsselwort then nicht
gibt, dafür Doppelpunkte zur Trennung einzelner Teilkonstrukte verwendet werden. Zusätzlich bestimmt die Einrückung die Blockstruktur, und sorgt dafür,
dass man Klammern weglassen kann. Es gibt noch weitere Varianten von
if-then-else in Python, ohne else Zweig oder mit zusätzlichen Abfragen
(elif-Zweigen).
Die while-Schleife dient zur Iteration, die Syntax ist:
while Bedingung:
Schleifenkörper
Der Anweisungsblock des Schleifenkörpers wird solange ausgeführt bis die Bedingung nicht (mehr) erfüllt ist.
Im Schleifenrumpf können die Statements break und continue verwendet
werden.
Beispiel 3.2.1 >>> a=10
>>> while a > 0:
...
print a
...
a = a-1
...
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
4
10
9
8
7
6
5
4
3
2
1
Funktions- bzw Prozedurdefinitionen, werden mit dem Schlüsselwort
def eingeleitet:
def Funktioname(parameter1, parameter2, . . .) :
Anweisungen
return Wert
Der Rückgabewert der Funktion ist der mit dem Schlüsselwort return zurück
gereichte Wert. Die return-Anweisung darf auch weggelassen werden, dann liefert die Funktion nichts zurück (bzw. None). Prozeduraufrufe finden immer mit
voller Argumentanzahl und mit eingeklammerten Argumenten statt: f (a, b, c).
Komplexe Zahlen sind verfügbar. Sie werden als Paar von zwei DoubleZahlen repräsentiert, der Real- und der Imaginär-Teil. Will man diese aus der
komplexen Zahl z extrahieren, dann benutzt man z.real und z.imag.
>>> a=1.5+0.5j
>>> a.real
1.5
>>> a.imag
0.5
>>> abs(a)
1.5811388300841898
>>> a*a
(2+1.5j)
>>>
Neben einfachen Strings kann man Unicode Strings in Python auf folgende
Art erzeugen:
>>> u’Hello World !’
u’Hello World !’
Es gibt eine Menge von Modulen, die zur Verfügung stehen. Man kann sie
im Interpreter verwenden durch entsprechende import-Anweisungen und qualifizierte Aufrufe:
5
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
>>> import cmath
>>> a=1.5+0.5j
>>> cmath.sin(a)
(1.1248012470579227+0.036860823712804462j)
>>>
Eine genauere Beschreibung folgt noch.
3.3
3.3.1
Beispiele und Besonderheiten in Python
Einige einfache Funktionen in Python
In Python kann man wie in Haskell Funktionen definieren. Die Syntax ist etwas
anders: Z.B. die Wurzelberechnung mittels Iteration kann man so programmieren – wenn man sie selbst programmieren will, und nicht math.sqrt verwenden
will.
def wurzel(x):
return wurzeliter(1.0,x)
def wurzeliter(schaetzwert,x):
if
gutgenug(schaetzwert,x):
return schaetzwert
else: return wurzeliter(verbessern(schaetzwert, x),
x)
def quadrat(x): return x*x
def gutgenug(schaetzwert,x):
return (abs ((quadrat(schaetzwert) - x)
def verbessern(schaetzwert,x):
return mittelwert(schaetzwert,
def mittelwert(x,y):
/ x) < 0.00001)
(x / schaetzwert))
return (x + y) / 2.0
>>> wurzel(2.0)
1.4142156862745097
>>>
3.3.2
Verletzung der referentiellen Transparenz
Die referentielle Transparenz, die in Haskell galt, wird in Python, wie in allen
imperativen Programmiersprachen, verletzt:
count = 0
def f(x):
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
6
global count
count = count + x
return count
Der Test f (1) == f (1) ergibt 0, d.h. falsch “ bei diesem Beispiel, da die
”
globale Variable count durch Seiteneffekte verändert wird.
3.3.3
Flussdiagramme
Der größte gemeinsame Teiler kann durch die Prozedur ggtpy berechnet werden:
def ggtpy(x,y):
if x <= 0 or y <= 0:
print ’Eingabe in GGT muss positiv sein’
else:
while x != y:
if x > y:
x = x - y
else:
y = y - x
return x
Die schnellere Version von ggt, hier rekursiv:
def ggt(x,y):
if y == 0:
return x
else: return ggt(y, (x % y))
Einige Beispielauswertungen:
>>> ggtpy(4,6)
2
>>> ggtpy(100,85)
5
>>> ggtpy(3,3)
3
>>> ggtpy(0,0)
Eingabe in GGT muss positiv sein
0
>>> ggtpy(1234,4321)
1
>>> ggtpy(1243,3421)
11
Man kann ein sogenanntes Flussdiagramm verwenden, um die Wirkungsweise dieses Programms zu veranschaulichen (siehe Figur 3.1). Allerdings sind
Flussdiagramme als Entwurfsmethode oft von begrenztem Nutzen, da zu große
Diagramme entstehen.
7
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
x := a; y := b
drucke x
Ja
nein
x=y
Stop
Ja
x>y
x := x-y
nein
y := y-x
Abbildung 3.1: Flußdiagramm für ggt
3.3.4
Vertauschung ohne Hilfsvariable
Mit der Mehrfach-Zuweisung kann man die Vertauschung von Variablenwerten
ohne Hilfsvariable durchführen:
>>>
>>>
>>>
>>>
2
>>>
1
>>>
a = 1
b = 2
a,b = b,a
a
b
In anderen imperativen Programmiersprachen braucht man i.a. die Sequenz:
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
c = a
a = b
b = c
#######
>>> a =
>>> b =
>>> c =
>>> a =
>>> b =
>>> a
2
>>> b
1
>>>
8
Im Python Interpreter:
1
2
a
b
c
Die Implementierung der Mehrfach-Zuweisung ist doch nicht ganz so parallel
wie vermutet
>>> a,b,a = 1,2,3
>>> a
3
>>>
3.3.5
Berechnung der Fibonacci-Zahlen
Die Berechnung der Fibonacci-Zahlen kleiner als n kann man in Python (optimiert) so hinschreiben:
def fib(n):
a,b = 0,1
while b < n:
print b,
a,b = b,a+b
Die formale Definition ist f ibn = f ibn−1 + f ibn−2 für n > 1 und f ib0 = 0,
f ib1 = 1.
>>> fib(1000)
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
3.3.6
Implementierung der 3 ∗ n + 1-Funktion
Der Fairness halber die Python-Implementierung von drein in Python:
def drein(x):
while x != 1:
print(x)
if x % 2 == 0:
x = x/2
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
else: x =
print(1)
3.3.7
9
3*x+1
Boolesche Funktionen
Beispiel 3.3.1 Zur Demonstration erläutern wir kurz die Wirkungsweise der
Booleschen Funktionen:
>>> a = 1
>>> not a
False
>>> a = 0
>>> b = 1
>>> a or b
1
>>> a and b
0
>>> del b
>>> b
Traceback (most recent call last):
File "<input>", line 1, in ?
NameError: name ’b’ is not defined
>>> a
1
>>> a or b
1
D.h. not, or, and wirken (fast) wie die Booleschen Operatoren. Es gibt eine
kleine Besonderheit: or und and werten von links nach rechts aus und geben ihr
Ergebnis sofort zurück, wenn der Wert aufgrund des ersten Arguments schon
bekannt ist, auch wenn das zweite undefiniert ist.
Sie wirken wie folgende Funktionen:
def or(x,y):
if x:
return x
else:
return y
def and(x,y):
if x:
return y
else:
return x
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
3.3.8
10
Iterative Prozesse
Funktionen, die iterative Prozesse erzeugen, lassen sich leicht mittels einer
while-Schleife (oder einer for-Schleife) programmieren: Hier die den HaskellFunktionen entsprechenden Funktionen zur Fakultät in Python. Beachte, dass
man die Anweisung zur strikten Auswertung in Python nicht braucht.
def fakultaetlin(n):
a = faktiter(1,1,n)
return a
def faktiter(produkt,zaehler,max):
if zaehler > max:
return produkt
else: return faktiter(zaehler * produkt,zaehler + 1,max)
def faktwhile(n):
produkt = 1
zaehler = 1
max = n
while zaehler <= max:
produkt = (zaehler * produkt)
zaehler = zaehler + 1
return produkt
3.3.9
Module in Python
Module dienen zur
Strukturierung / Hierarchisierung: Einzelne Programmteile können innerhalb verschiedener Module definiert werden; eine (z. B. inhaltliche) Unterteilung des gesamten Programms ist somit möglich. Hierarchisierung ist
möglich, indem kleinere Programmteile mittels Modulimport zu größeren
Programmen zusammen gesetzt werden.
Kapselung: Nur über Schnittstellen kann auf bestimmte Funktionalitäten zugegriffen werden, die Implementierung bleibt verdeckt. Sie kann somit unabhängig von anderen Programmteilen geändert werden, solange die Funktionalität (bzgl. einer vorher festgelegten Spezifikation) erhalten bleibt.
Wiederverwendbarkeit: Ein Modul kann für verschiedene Programme benutzt (d.h. importiert) werden.
In Python gibt es ebenfalls Module, hierbei gibt es jedoch keinen Modulkopf
wie in Haskell, der eine Datei als Modul auszeichnet. Vielmehr ist jede Datei, die
Python-Code enthält, ein Modul und kann von anderen Modulen aus importiert
werden.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
11
Modulimport mittels import
In einfachster Weise kann ein Modul, durch die Anweisung
import Modulname
importiert werden, wobei der Modulname dem Dateinamen ohne Dateiendung
entspricht. Bei erstmaliger Ausführung der import-Anweisung werden die auf
oberster Ebene des Moduls definierten Befehle (d.h. solche die nicht innerhalb
einer Funktionsdefinition stehen) ausgeführt. Bei nochmaligem Ausführen der
import-Anweisung werden diese Anweisungen nicht erneut ausgeführt. Zur Reinitialisierung eines geladenen Moduls steht jedoch die Funktion reload zur
Verfügung, die als Argument das Modulobjekt erwartet, d.h.
reload(Modulname)
kompiliert den Quellcode erneut (in interpretierbaren Byte-Code) und führt anschließend die Anweisungen auf oberster Ebene des importierten Moduls erneut
aus.
Beispiel 3.3.2 Wir betrachten die Datei Fib.py, d.h. das Modul Fib:
def fib(n):
a,b = 0,1
while b < n:
print b,
a,b = b,a+b
X=10
fib(X)
# Diese Zeilen werden beim Import
# ausgefuehrt
Beim Import des Moduls werden die beiden letzten Befehle (im Namensraum
des Modules Fib) ausgeführt:
>>> import Fib
1 1 2 3 5 8
Auf die Funktionsdefinitionen und globalen Variablen des mittels import
eingebundenen Moduls kann nur qualifiziert, d.h. in der Form Modulname.Funktionsname bzw. Modulname.Variablenname, zugegriffen werden.
Beispiel 3.3.3 Nach Laden des Moduls Fib aus Beispiel 3.3.2 ist die (globale) Variable X unbekannt, sie existiert jedoch qualifiziert, gleiches gilt für die
Funktion fib:
>>> X
Traceback (most recent call last):
File "<stdin>", line 1, in ?
NameError: name ’X’ is not defined
>>> Fib.X
10
>>> Fib.fib(5)
1 1 2 3
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
12
Lokale Aliase für Modulnamen
Importierte Module können analog wie in Haskell unter einem Alias-Namen
importiert werden, hierfür gibt es in Python das Schlüsselwort as, z.B. kann man
das Modul Fib aus Beispiel 3.3.2 unter dem Namen Fibonacci importieren:
>>>
1 1
>>>
1 1
import Fib as Fibonacci
2 3 5 8
Fibonacci.fib(5)
2 3
Einbinden mittels from
Mit der from-Anweisung können zum einen die zu importierenden Definitionen
selektiert werden, zum anderen sind sie direkt im Namensraum des importierenden Moduls, d.h. unqualifiziert, verfügbar, die Syntax hierbei ist
from Modulname import Definitionsliste
Z.B. kann man ausschließlich die Funktion fib importieren:
>>>
1 1
>>>
1 1
from Fib import fib
2 3 5 8
fib(5)
2 3
Es gibt noch die Variante
from Modulname import *
die sämtliche Namen eines Moduls in den Namensraum des importierenden Moduls kopiert. Hiervon sei aber abgeraten, da unerwartete Seiteneffekte auftreten
können:
Beispiel 3.3.4 Wir betrachten das Modul Printer:
X = 10;
def printer():
global X:
print X
Wird dieses Modul mit der from *-Anweisung importiert und die importierte
globale Variable X verändert, so gilt diese Änderung nur im Namensraum des
importierenden Moduls, nicht jedoch im Namensraum der Funktion printer:
>>> from Printer import *
>>> X
10
>>> printer()
10
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
13
>>> X = 20
>>> printer()
10
>>> X
20
Keine Datenkapselung in Python
In Python gibt es keine (echte) Möglichkeit bestimmte Funktionsdefinitionen,
o.ä. vom Export auszuschließen, d.h. das importierende Modul kann auf sämtliche definierten Namen des importierten Moduls zugreifen. Eine Datenkapselung
ist somit in Python nicht möglich. Es gibt lediglich die Konvention, dass man
Definitionen deren Namen mit einem Unterstrich _ beginnen, als versteckt ansehen sollte, und im importierenden Modul nicht benutzen soll. Der Interpreter
verbietet den Zugriff jedoch nicht.
3.4
Auswertung von Python-Programmen
Der von-Neumann-Rechner ist ein von John von Neumann (1903-1957) vorgeschlagenes Modell eines universellen Rechners, der zum Konzept der imperativen Programmiersprachen passt, genauer: zu einem primitiven AssemblerProgramm.
Das Konzept des von-Neumann Rechners ist hilfreich beim Verständnis
der Auswertung von imperativen Programmen. Um die volle Auswirkung der
Zuweisung zu verstehen, insbesondere die Gültigkeitsbereiche von Variablen,
ist es hilfreich, ein solches Modell des Rechners und Speichers zu betrachten.
14
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
Haupt-Speicher
Daten
Programm
Bus
Ein/
Ausgabe
Steuerwerk
Register
Programmzähler
Datenzeiger
Rechenwerk
Akkumulator
Der Speicher ist in gleichlange Einheiten, die Speicherzellen zu je einem
Byte = 8 Bits, eingeteilt, die fortlaufend nummeriert sind und unter dieser
Nummer adressierbar sind, und direkt, unter Angabe der Adresse, gelesen und
geschrieben werden können. Als weitere größere Einheit hat man manchmal
auch (Speicher-)Worte (normalerweise 1 Wort = 4 Byte = 32 Bit).
Eine zentrale Idee ist die Speicherung von Daten und Programmen im gleichen Speicher in Binärcode, wobei die Programme in kodierter Form vorliegen.
Selbst die von-Neumann Maschine in der einfachsten Form benötigt zumindest: ein Adressregister für das Programm (den Programmzähler), ein Register
für Datenadressierung, und ein Register zum Rechnen (Akkumulator). Heutzutage sind die Register 32 oder 64 Bit lang. Mit 32 Bit ist der adressierbare
Speicher auf 232 Bytes beschränkt (ca. 4 Gigabyte). Im Steuerwerk werden die
Programmbefehle interpretiert, und im Rechenwerk die arithmetischen Operationen ausgeführt.
Ein Programm für eine von-Neumann-Maschine besteht aus Befehlen, die
man sich durchnummeriert vorstellt: 1.,2.,3. . . . , und die hintereinander im Speicher abgelegt sind.
Als Befehle sind möglich:
• arithmetische: Addiere Konstante zum Akkumulator. Auch indirekt adressierte: mulitpliziere Zahl an Adresse xxx mit dem Akkumulator und speichere das Ergebnis im Akkumulator. Auch arithmetische Vergleiche sind
möglich.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
15
• bedingte und unbedingte Sprungbefehle. Z.B. springe zu Programmbefehl
108. Oder: Wenn Akkumulator 6= 0, dann springe zu Befehl 101, sonst zu
201.
• Ein-Ausgabe-Befehle: Drucke den Inhalt des Akkumulators. Oder: Speichere den Inhalt des Akkumulators an der Adresse, die im Datenregister
steht.
Die zweite zentrale Idee des von-Neumann Rechner-Modells ist die Methode
der Abarbeitung des Programms:
Es gibt einen Befehlszyklus, der in etwa so abläuft.
Start des Programms ist mit dem Programmbefehl 1.
Der Befehlszyklus:
1. Lade den Programmbefehl mit der Nummer, die im Programmzähler angegeben ist, in das Steuerwerk. Das Steuerwerk steuert dann, abhängig
vom Inhalt des Programmbefehle, die weitere Abarbeitung.
2.
• Wenn es ein Sprungbefehl ist, setze den Programmzähler neu. Ende
des Zyklus.
• Wenn es ein arithmetischer Befehl ist, dann entsprechend abarbeiten. Diese Abarbeitung wird vom Rechenwerk erledigt. Der
Programmzähler wird danach um 1 erhöht. Ende des Zyklus.
• Wenn es ein Ein-Ausgabebefehle ist, dann entsprechend abarbeiten.
Der Programmzähler wird danach um 1 erhöht. Ende des Zyklus.
3. Bei dem speziellen Befehl Stop wird das Programm beendet.
Natürlich ist ein richtiger Computer (bzw. Prozessor) komplizierter. Zum
Beispiel hat die Recheneinheit noch weitere universelle Register, die meist so
groß sind, dass eine Adresse gespeichert werden kann.
3.4.1
Auswertung von Pythonprogrammen
In diesem Abschnitt werden wir einen Ausschnitt aus der operationalen Semantik von Python angeben. Das soll das Verständnis der Vorgänge vertiefen. Auch
wenn es zunächst wie eine umständliche Erklärung von Zusammenhängen wirkt,
die man nach dem Erlernen der Programmiersprache einfach weiß, braucht
man eine solche formale Begriffsbildung und Regeln, um die Auswertung von
Programmen und Ausdrücken rechnerunabhängig mitteilen und beschreiben zu
können. Dieser Formalismus ist es auch notwendig, um zu spezifizieren bzw. zu
verstehen, wie man geschachtelte Ausdrücke mit Seiteneffekten und Funktionsaufrufen auswertet. Zudem versteht man danach auch, welche Implementierungen der Modellvorstellung nicht entsprechen.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 9. Dezember 2004
16
Wir nehmen an, dass nur sehr einfach strukturierte Programme vorliegen.
D.h. als Python-Programm betrachten wir eine Menge von Funktionsdefinitionen und Variablendefinitionen, wobei nur Zuweisung, if-then-else, while, arithmetische und logische Operationen benutzt werden. Funktionsaufrufe werden
erst mal ausgeschlossen, da sie Seiteneffekte auf globalen Variablen bewirken
können und auch etwas komplizierter sind. Diese werden später behandelt. An
Basiswerten betrachten wir im Moment nur Zahlen, wobei auch Boolesche Werte
als Zahlen gespeichert werden 1 bedeutet ’wahr’ und 0 bedeutet ’falsch’.
3.4.2
Die formale Modellierung
Wir betrachten auch die globale Umgebung, damit wir Seiteneffekte auf globale
Variablen angeben und modellieren können, da dies in den meisten imperativen
Programmiersprachen vorkommt. Insbesondere muss die operationale Semantik dann die Reihenfolge der Operationen beschreiben, denn die Seiteneffekte
ändern sich, wenn die Reihenfolge der Auswertungen verändert wird.
Unsere Modellierung in diesem Abschnitt ist vereinfacht und ist nicht einfach erweiterbar auf geschachtelte Funktionsdefinitionen, da man dazu weitere
Konzepte benötigt.
Definition 3.4.1 Eine Bindung ist ein Paar (x, v), wobei x ein Variablenname
und v ein Basiswert ist oder ⊥. Das schreiben wir etwas deutlicher als x 7→ v.
Ein Umgebungsrahmen (Frame) ist eine endliche Menge von Bindungen.
Pro Variablennamen x kann nur eine Bindung x 7→ v in einem Umgebungsrahmen enthalten sein.
Eine Umgebung (Variablenumgebung) ist ein Paar (R1 , R2 ) von zwei Umgebungsrahmen, des lokalen Umgebungsrahmens R1 und des globalen Umgebungsrahmens R2 .
Um das Modell zu vereinfachen, nehmen wir an, dass in unserer Modellierung lokale Variablennamen niemals gleich globalen Variablennamen sind. Das
kann man erreichen, indem lokale Variablen in Funktionsrümpfen so umbenannt
werden, dass diese disjunkt zu allen anderen Variablen sind.
Der Wert wert(x, Z) einer Variablen x in einer Umgebung Z = (R1 , R2 ) ist
definiert als der Wert v der Bindung x 7→ v in der Umgebung R1 ∪ R2 . Kommt
kein solches Paar in R1 ∪ R2 vor, oder ergibt sich ⊥, so ist der Wert undefiniert.
Normalerweise wird bei einer solchen Anfrage das Programm abgebrochen.
Die Funktion update(x, v, Z) hat als Ergebnis die Umgebung Zneu , die aus
Z = (R1 , R2 ) folgendermaßen entsteht:
• Wenn x lokale Variable ist, und es eine Bindung x 7→ v 0 in R1 gibt, wird
diese durch x 7→ v ersetzt. Das ergibt einen veränderten Umgebungsrahmen R10 , so dass Zneu := (R10 , R2 ).
• Wenn x lokale Variable ist und wenn es keine Bindung x 7→ v 0 in R1 gibt,
dann ergibt sich ein Fehler: Wert wird referenziert ohne dass er angelegt
wurde.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 15.12.2004
17
• Wenn x globale Variable ist, und es eine Bindung x 7→ v 0 in R2 gibt, wird
diese durch x 7→ v ersetzt. Das ergibt einen veränderten Umgebungsrahmen R20 , so dass Zneu := (R1 , R20 ).
• Wenn x globale Variable ist und wenn es keine Bindung x 7→ v 0 in R2 gibt,
dann ergibt sich ein Fehler: Wert wird referenziert ohne dass er angelegt
wurde.
Beispiel 3.4.2 Sei die Umgebung definiert als
Z = ({x 7→ 1, y 7→ 2, z 7→ 3}, {gx 7→ 2, gu 7→ 100})
Dann ist wert(x, Z) = 1,
wert(gu, Z) = 100,
und update(x, 30, Z) = ({x 7→ 30, y 7→ 2, z 7→ 3}, {gx 7→ 2, gu 7→ 100}).
Definition 3.4.3 Der Wert eines Ausdrucks s in der Umgebung Z ist der
Wert, der bei Berechnung des Ausdrucks entsteht, wobei der Wert der Variablen
in der Umgebung Z ermittelt wurde. Ist eine (für die Berechnung benötigte) Variable x in s undefiniert, dann ist der Wert von s ebenfalls undefiniert. D.h. wir
nehmen bei der Auswertung von Ausdrücken in einfachem Python im Moment
an, dass keine Seiteneffekte (d.h. keine Zustandsänderungen) durch Auswertungen von Ausdrücken entstehen.
Hier einige formale Definitionen für die Auswertung von arithmetischen und
Booleschen Ausdrücken, die seiteneffektfrei sind, d.h. ohne weitere Funktionsaufrufe, wobei der Operator rechts in der Tabelle jeweils der arithmetische
Operator ist. Funktionsanwendungen und Kombinationen mit arithmetischen
Ausdrücken werden wir weiter unten behandeln.
wert(s1 + s2 , Z)
wert(s1 ∗ s2 , Z)
:=
:=
wert(s1 , Z) + wert(s2 , Z)
wert(s1 , Z) ∗ wert(s2 , Z)
1 wenn wert(s1 , Z) ≤ wert(s2 , Z)
wert(s1 ≤ s2 , Z)
:=
0 sonst
1 wenn wert(s, Z) = 0
wert(not s, Z)
:=
0 wenn wert(s, Z) 6= 0
0
wenn wert(s1 , Z) = 0
wert(s1 and s2 , Z) :=
wert(s2 , Z) wenn wert(s1 , Z) 6= 0
...
... ...
...
... ...
Beispiel 3.4.4 Wert eines seiteneffektfreien Ausdrucks in einer Umgebung.
Sei Z = ({x 7→ 1, y 7→ 2, z 7→ 3}, {u 7→ 100}). Dann ergibt sich mit obiger
Definition als Wert von x+(y*z):
18
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 15.12.2004
wert(x + (y ∗ z), Z)
=
wert(x, Z) + wert(y ∗ z, Z)
=
1 + (wert(y, Z) ∗ wert(z, Z))
=
1 + (2 ∗ 3)
=
7
Als Wert von x+u ergibt sich, wobei u global definiert ist:
wert(x + u, Z)
=
wert(x, Z) + wert(u, Z)
=
1 + 100
=
101
Auswertung von Zuweisungen
Definition 3.4.5 Wir werden im folgenden die Schreibweise der logischen Regeln verwenden:
V oraussetzungen
Konsequenz
Oberhalb des Striches werden evtl. mehrere Voraussetzungen notiert und unterhalb die Konsequenz.
Wenn es keine Voraussetzung gibt, wird nur die Konsequenz notiert (ohne
Bruchstrich)
Effekt der Ausführung eines Python-Befehls
Wir schreiben den Effekt einer Befehlsausführung folgendermaßen hin:
(Z; B) → Z 0 , wenn, nach Ausführung des Befehls (des Programmstücks) B
im Zustand Z der Zustand Z 0 danach hinterlassen wird.
Zuweisung:
(Z; x = s) → update(x, wert(s, Z), Z)
Auch hier ist zu beachten, dass wir erst mal annehmen, dass s keine Seiteneffekte
bewirkt.
Mehrfachzuweisung:
(Z; x1 , . . . , xn = s1 , . . . , sn )
→
update(xn , wert(sn , Z), update(xn−1 , wert(sn−1 , Z), . . . update(x1 , wert(s1 , Z), Z) . . .))
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 15.12.2004
19
Man erkennt, dass diese Zuweisung ein Potential zur Parallelisierbarkeit hat,
aber nur unter der Annahme, dass die Auswertung der Ausdrücke keine Seiteneffekte bewirkt, und dass die Variablennamen der Zuweisung alle verschieden
sind.
Beispiel 3.4.6 Sei Z = ({x 7→ 3, y 7→ 4, z 7→ 5}, ∅).
Die Auswertung von x,y = y,x im Zustand Z ergibt:
Z 0 = update(y, wert(x, Z), update(x, wert(y, Z), Z))
= update(y, wert(x, Z), update(x, 4, Z))
= update(y, wert(x, ({x 7→ 3, y 7→ 4, z 7→ 5}, ∅)), ({x 7→ 4, y 7→ 4, z 7→ 5}, ∅))
= update(y, 3, ({x 7→ 4, y 7→ 4, z 7→ 5}, ∅))
= ({x 7→ 4, y 7→ 3, z 7→ 5}, ∅)
Hier muss man beachten, dass Z in den Berechnungen immer die gleiche
Umgebung referenziert.
Beispiel 3.4.7 Sei Z = ({x 7→ 3, y 7→ 4, z 7→ 5}, ∅).
Die Auswertung von x,x = y,0 im Zustand Z ergibt:
Z 0 = update(x, wert(0, Z), update(x, wert(y, Z), Z))
= update(x, 0, update(x, 4, Z))
= update(x, 0, update(x, 4, ({x 7→ 3, y 7→ 4, z 7→ 5}, ∅))),
= update(x, 0, ({x 7→ 4, y 7→ 4, z 7→ 5}, ∅),
= ({x 7→ 0, y 7→ 4, z 7→ 5}, ∅),
Auswertung der Sequenz
Dies ist ein Konzept, das in Haskell unnötig ist, aber in imperativen Programmiersprachen wie Python notwendig ist.
{c1 ; c2 } ist die Hintereinanderausführung der beiden Kommandos c1 , c2 . Die
Regel zur operationalen Semantik ist:
(Z; c1 ) → Z1 (Z1 ; c2 ) → Z2
(Z; {c1 ; c2 }) → Z2
Beispiel 3.4.8 Sei Z = ({x 7→ 3, y 7→ 4, z 7→ 5}, ∅).
Die Auswertung von x = y; y = x im Zustand Z ergibt:
1. (Z, x = y)
→ update(x, wert(y, Z), Z)
= update(x, 4, Z)
= ({x 7→ 4, y 7→ 4, z 7→ 5}, ∅)
=: Z1
2. (Z1 , y = x) → update(y, wert(x, Z1 ), Z1 )
= update(y, 4, Z1 )
= ({x 7→ 4, y 7→ 4, z 7→ 5}, ∅)
= Z1
Fallunterscheidung
Auswertungsregeln:
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 15.12.2004
20
wert(b, Z) 6= 0 (Z, c1 ) → Z1
(Z; if b : c1 else : c2 ) → Z1
wert(b, Z) = 0 (Z, c2 ) → Z2
(Z; if b : c1 else : c2 ) → Z2
Die Auswertung einer Fallunterscheidung ohne else-Fall oder mit weiteren
elseif-Fällen ist jetzt eine einfache Verallgemeinerung.
Bei der Abarbeitung muss man sich merken, dass zunächst nur die Bedingung
ausgewertet wird, und abhängig von ihrem Wert entweder die Befehle des jaoder des nein-Falles.
While-Schleife
Bei der formalen Definition der Auswertung der while-Schleife muss man beachten, dass diese evtl. nicht stoppt. Die folgende Regel berücksichtigt das. Ebenso
beachte man, dass die Regel rekursiv ist.
wert(b, Z) = 0
(Z; while b : c) → Z
wert(b, Z) 6= 0
(Z, c) → Z1 (Z1 ; while b : c) → Z2
(Z; while b : c) → Z2
Die erste Regel behandelt den Fall, dass die Bedingung falsch ist, und die
while-Schleife nicht durchlaufen wird. In dem Fall bleibt die Umgebung erhalten.
Die zweite Regel behandelt den Fall, dass Die Bedingung wahr ist, und die
Schleife durchlaufen wird. In der Bedingung steht: wenn der Rumpf einmal
durchlaufen wird, danach die While-Schleife und sich dann die Umgebung Z2
ergibt, dann auch nach der While-Abarbeitung alleine.
Die for-Schleife werden wir noch behandeln.
Beispiel 3.4.9 Sei die Umgebung vor der Auswertung der while-Schleife: Z =
({x 7→ 0, n 7→ 5}, ∅).
Der Befehl sei
while (n > 0): x = x+n; n = n-1
Wir gehen die Auswertung durch, entsprechend den Regeln:
1. wert(n > 0, Z) ergibt 1.
2. Wir wenden die zweite Regel an. Die zweite Voraussetzung ist (Z, c) → Z1 .
c ist hierbei der Rumpf des while-Ausdrucks und Z1 die Umgebung nach
einmaliger Ausführung. Es ergibt sich Z1 = ({x 7→ 5, n 7→ 4}, ∅).
Um die dritte Voraussetzung zu erfüllen, muss man jetzt wieder einen
while-Ausdruck, jetzt im Zustand Z1 auswerten. D.h. man muss wieder
die volle Regel für while verwenden. Das kommt mehrfach vor und ergibt
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 15.12.2004
21
eine Folge von Umgebungen:
({x 7→ 9, n 7→ 3}, ∅).
({x 7→ 12, n 7→ 2}, ∅).
({x 7→ 14, n 7→ 1}, ∅).
({x 7→ 15, n 7→ 0}, ∅).
3. Danach muss man, um die letzte Voraussetzung zu erfüllen, die erste Regel
anwenden, es ergibt sich, dass die Umgebung erhalten bleibt.
4. Jetzt kann man alle Voraussetzungen nach und nach erfüllen, die letzte
Umgebung wird jeweils weitergegeben. Es ergibt sich am Ende
Z2 = ({x 7→ 15, n 7→ 0}, ∅).
3.4.3
Anzahl der Schritte bei Auswertung
Die Regeln der operationalen Semantik sind zwar nicht so formuliert, dass sie die
Reihenfolge der Operationen genau festlegen, aber man kann an der Weitergabe
der Umgebungen die notwendige Reihenfolge der Schritte ablesen.
Aufbauend auf der operationalen Semantik können wir jetzt die Anzahl der
Auswertungsschritte definieren und auch zählen.
Wir vereinbaren: Bei Auswertung eines Ausdrucks werden folgende Auswertungsschritte gezählt:
• Einzelauswertung einer Operation in einem Ausdruck
• Auswertung eines Befehls
• Zuweisung
• Wertermittlung bei Variablen
Nicht gezählt wird die implizite Initialisierung von Variablen.
Bei Operationen wir += nehmen wir an, dass diese zunächst in eine normale
Zuweisung transformiert wurden.
Beispiel 3.4.10 Die Auswertung der Sequenz x = y; y = x ergibt:
Auswertung von y
Zuweisung auf x
Auswertung von x
Zuweisung auf y
Das sind insgesamt 4 Auswertungsschritte.
Beispiel 3.4.11 Auswertung einer while-Schleife, die fib (10) berechnet.
Wir zählen die Schritte.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 15.12.2004
22
n = 10;
a,b = 0,1;
while n > 0:
print b;
a,b = b,a+b;
n = n-1;
Die Anzahl der Auswertungsschritte ergibt:
1 Zuweisung zu n
2 Mehrfachzuweisung
1 Aufruf von while
2 Bedingung
2 print
2 Mehrfachzuweisung a = b
4 Mehrfachzuweisung b = a+b; (a;b;+;=)
3 Zuweisung n=n-1
Ergebnis: 3
für Initialisierung
1
für while-Aufruf
14 pro while-Durchlauf (10 mal)
2
Bedingung zur While-Beendigung
In der Summe ergibt das: 6 + 14 ∗ n Auswertungsschritte, d.h. O(n).
3.4.4
Auswertung unter Benutzung von Funktionsaufrufen und Seiteneffekten
Hier erweitern wir die Auswertung um die Behandlung von Funktionsaufrufen
und auch Seiteneffekten. Die (echten) Seiteneffekte können in unserer Modellierung nur von Funktionsanwendungen herrühren, in denen globale Variablen
verändert werden.
Bevor wir die Formalisierung durchführen, hier noch zwei Beispiele.
Beispiel 3.4.12 Ein Beispiel einer Booleschen Funktion mit Seiteneffekt. Die
Funktion schonmalda testet, ob der Wert 12345 schon einmal eingegeben wurde.
schonmaldaKenn = False
def schonmalda(x):
global schonmaldaKenn;
if (x == 12345):
if schonmaldaKenn:
return True
else: schonmaldaKenn = True
def InitKenn():
global schonmaldaKenn;
schonmaldaKenn = False
>>> kap3.schonmalda(3)
False
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 15.12.2004
23
>>> kap3.schonmalda(12345)
False
>>> kap3.schonmalda(12345)
True
>>> kap3.InitKenn()
>>> kap3.schonmalda(12345)
False
>>> kap3.schonmalda(12345)
True
>>>
Beispiel 3.4.13 Die Funktion addToAkku addiert das Argument jeweils zu einem Akkumulator, der als globale Variable vereinbart ist.
Akkumulator = 0
def addToAkku(x):
global Akkumulator;
Akkumulator = Akkumulator + x
return Akkumulator
def ShowAkku():
return Akkumulator
>>> kap3.addToAkku(2)
2
>>> kap3.ShowAkku()
2
>>> kap3.addToAkku(2) * kap3.addToAkku(2)
24
>>>
Die letzte Rechnung kommt von 4 ∗ 6 = 24, da der Akkumulator jeweils um 2
erhöht wird.
Da Python keine explizite Variablendeklaration für lokale Variablen in Funktionen hat, aber doch eine implizite Variablendeklaration macht, müssen wir
diese Variablen hier behandeln. Mit LV(f ) bezeichnen wir die lokalen Variablen
von f : Das sind solche, die nicht als formale Parameter von f vereinbart sind, die
aber auf der linken Seite von Zuweisungen innerhalb des Rumpfs vorkommen.
Ein Funktionsaufruf in Python ist von der Form f (s1 , . . . , sn ). Um die Auswertung zu verstehen, muss man sich klarmachen, dass die si wieder Funktionsaufrufe enthalten können, und dass Argumente von links nach rechts ausgewertet
werden.
Da als Ergebnis einer Auswertung der Wert eines Funktionsaufrufs und der
Zustand benötigt wird, schreiben wir
(Z; s) →e (Z 0 ; v)
24
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
falls es sich um die Auswertung eines Ausdrucks (expressions) s im Zustand Z
handelt, Z 0 der Zustand danach ist und v der return-Wert von s.
Aufgrund der Regeln wird bei der Auswertung →e nur der globale Anteil in
Z und Z 0 unterschiedlich sein, im Gegensatz zu Auswertung von Befehlen.
Eine (kleine) Schwierigkeit bereitet die spezielle Methode der Rückgabe mittels return, die nicht nur einen Wert zurückgibt, sondern auch die Ausführung
der Funktion beendet, und damit einen Rücksprung bewirkt. Das kann in unserer Methode nicht genau modelliert werden, statt eines Rücksprungs sind nach
einem return innerhalb der Funktion alle Befehle ohne Wirkung.
Wir erweitern deshalb den bisher benutzten Formalismus, indem wir im Umgebungsrahmen zwei spezielle Variablennamen zur Steuerung der Ausführung
hinzufügen:
rj enthält true bzw. false, wenn ein Rücksprung aktiv ist.
rw enthält den Rückgabewert, wenn ein Rücksprung aktiv ist.
Wir geben nacheinander die operationale Semantik der Befehle an, wenn
Seiteneffekte erlaubt sind.
Zunächst die einfachen Auswertungen:
(Z, x) →e (Z, wert(x, Z))
(Z, v) →e (Z, v)
wenn x Variable ist
wenn v Basiswert ist
Wenn ein Rücksprung aktiv ist, sollen Anweisungen c nichts tun:
v 6= 0
(({. . . , rj 7→ v, . . .}, R), c) → ({. . . , rj 7→ v, . . .}, R)
return
(Z; s) →e (Z 0 ; w)
(Z, return s) → update( rj, 1, update( rw, w, Z 0 ))
Die operationale Semantik der Konstrukte muss jetzt so erweitert werden,
dass diese nur unter der Bedingung ausgeführt werden, dass rj den Wert 0
hat. Es reicht aus, das jeweils in den Bedingungen aller Regeln mit aufzuführen.
Wir lassen diese Vorbedingung bei der Notation unten weg, damit die Notation
Regeln nicht zu kompliziert wird.
Zuweisung
(Z, s) →e (Z1 , v)
(Z; x = s) → update(x, v, Z1 )
Mehrfachzuweisung:
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
25
(Z, s1 ) →e (Z1 , v1 ), (Z1 , s2 ) →e (Z2 , v2 ), . . . , (Zn−1 , sn ) →e (Zn , vn )
(Z; x1 , . . . , xn = s1 , . . . , sn )
→
update(xn , vn , update(xn−1 , vn−1 , . . . update(x1 , v1 , Zn ) . . .))
Man erkennt, dass die Mehrfach-Zuweisung jetzt nicht mehr parallelisierbar ist,
da die Umgebungen jeweils übergeben werden müssen.
Fallunterscheidung:
(Z, b) →e (Z1 , v) v 6= 0 (Z1 , c1 ) → Z2
(Z; if b : c1 else : c2 ) → Z2
(Z, b) →e (Z1 , v) v = 0 (Z1 , c2 ) → Z2
(Z; if b : c1 else : c2 ) → Z2
While-Schleife
(Z, b) →e (Z1 , v) v = 0
(Z; while b : c) → Z1
(Z, b) →e (Z1 , v) v 6= 0 (Z1 , c) → Z2 (Z2 ; while b : c) → Z3
(Z; while b : c) → Z3
Auswertung eines Ausdrucks
Wir müssen auch den Fall betrachten, dass eine Funktion ohne ein return beendet wird. Dann wird auch kein Wert zurückgegeben. Diesen “Nicht-Wert“ kann
man durch einen eigenen Wert beschreiben, in Python ist es None. Er verhält
sich ähnlich wie das Objekt () des unit-types “ in Haskell.
”
Damit kann man die Auswertung eines Ausdrucks beschreiben. Hierbei gehen
wir davon aus, dass f den Rumpf rumpf f hat, die formalen Parameter x1 , . . . , xn
und die lokalen Variablen LV (f ) = {y1 , . . . , ym }.
(Z, s1 ) →e (Z1 , v1 ), . . . , (Zn−1 , sn ) →e (Zn , vn ),
R = {x1 7→ v1 , . . . , xn 7→ vn , y1 7→ ⊥, . . . , ym 7→ ⊥, rj 7→ 0, rw 7→ None}
Zn = (R1 , R2 )
((R, R2 ), rumpf f ) → ({. . . , rw 7→ w}, R20 )
(Z, f (s1 , . . . , sn )) →e ((R1 , R20 ), w)
Das kann man so beschreiben:
Zeile 1: Zuerst werden die Argumente von f von links nach rechts ausgewertet.
Da man damit rechnen muss, dass die Auswertung Seiteneffekte in der lokalen
Umgebung und in der globalen hat, darf man das nur sequentiell machen, und
der veränderte Zustand muss jeweils mitübergeben werden. Das kann einen rekursiven Abstieg in die Auswertung von Unterargumenten bedeuten.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
26
Zeile 2: Wenn die Auswertung der Argumente beendet ist, kann man die lokale Umgebung R definieren, die die LV-Variablen initialisiert und den formalen
Variablen die entsprechenden Werte zuweist.
Zeile 3: aus Zn wird die globale Umgebung extrahiert.
Zeile 4: Jetzt kann man den Rumpf der Funktion auswerten. Am Ende des Aufrufs ist der Rückgabe-Wert bekannt, falls alles terminiert. Wenn kein return
gemacht wurde, wird als Wert das Objekt None zurückgegeben, das als Initialisierung verwendet wurde.
Da in den Argumenten eines Ausdrucks ein return unzulässig ist 1 , braucht
man nicht damit zu rechnen, dass die Zustände Zi einen Sprung erzwingen.
Ergebnis am Ende ist der evtl. veränderte Zustand, und der berechnete Wert.
Zu beachten ist, dass die Berechnung wert(s, Z) jetzt nur noch für Variablen s stimmt. Die Berechnung des Wertes für arithmetische (auch Boolesche)
Ausdrücke muss angepasst werden und sieht jetzt so aus:
(Z, s1 ) →e (Z1 , v1 ), (Z1 , s2 ) →e (Z2 , v2 ), v1 op v2 = v3
(Z, s1 op s2 ) →e (Z2 , v3 )
wobei op ein arithmetischer (bzw. Boolescher) Operator ist.
Beispiel 3.4.14 Als Abkürzung verwenden wir in den Beispielen rj statt rj
und rw statt rw
def fakultaet(x):
if x <= 1:
return 1
else: return x*fakultaet(x-1)
Die jeweiligen lokalen Umgebungen bei Auswertung von fakultaet(3) ohne
die globale Umgebung sind in zeitlicher Reihenfolge wie folgt:
Aufruf von fakultaet(3) im Zustand Z
{x 7→ 3, rj 7→ 0, rw 7→ None}
rekursives Auswerten von fakultaet (2)
x 7→ 2, rj 7→ 0, rw 7→ None}
rekursives Auswerten von fakultaet (1)
{x 7→ 1, rj 7→ 0, rw 7→ None}
Auswerten von return 1
{x 7→ 1, rj 7→ 1, rw 7→ 1}
Auswerten von return 2
{x 7→ 2, rj 7→ 1, rw 7→ 2}
Auswerten von return 6
{x 7→ 3, rj 7→ 1, rw 7→ 6}
Resultat am Ende ist 6.
Beispiel 3.4.15 Veränderung einer globalen Variablen durch eine Zuweisung.
Hier kann man die dauerhafte Veränderung der Umgebung nachvollziehen.
1 Hier
gibt es doch eine Typprüfung in Python
27
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
count = 0
def nreft(x):
global count
count = count + x
return count
Die Ausführung von nreft(1) ergibt:
Aufruf von nreft(1)
({x 7→ 1, rj 7→ 0, rw 7→ None}, {count 7→ 0})
nach count = count + x ({x 7→ 1, rj 7→ 0, rw 7→ None}, {count 7→ 1})
nach return count
({x 7→ 1, rj 7→ 1, rw 7→ 1}, {count 7→ 1})
Die globale Umgebung nach dem nreft(1)-Aufruf ist {count 7→ 1} und der
Rückgabewert ist = 1.
Die Bedingung nreft(1) == nreft(1) kann jetzt nicht zu True auswerten,
denn nach Auswertung des linken Ausdrucks hat count den Wert 1, nach Aufruf
des zweiten hat count den Wert 2, das sind auch die jeweiligen Rückgabewerte,
somit ergibt sich False.
Beispiel 3.4.16 Wir
zeigen
die
Zustände
eulerzahl(0.0001) in zeitlicher Reihenfolge.
bei
Auswertung
def eulerzahl(x):
s = 1
e = 0
n = 1
while x < s:
e = e + s
s = 1.0 / (fakultaet(n))
n = n+1
return e
Abfolge der Zustände, wobei wir die globale Umgebung weglassen.
von
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
28
Aufruf von eulerzahl(0.0001)
{x 7→ 0.0001, rj 7→ 0, rw 7→ None}
Aufruf von eulerzahl
{s 7→ ⊥, e 7→ ⊥, n 7→ ⊥, x 7→ 0.0001, rj 7→ 0, rw 7→ None}
3 Zuweisungen
{s 7→ 1, e 7→ 0, n 7→ 1, x 7→ 0.0001, rj 7→ 0, rw 7→ None}
1. Zuweisungen im while
{s 7→ 1, e 7→ 1, n 7→ 1, x 7→ 0.0001, rj 7→ 0, rw 7→ None}
fakultaet im while, nur dessen lokale Umgebung
{x 7→ 1, rj 7→ 0, rw 7→ None}
nach Aufruf fakultaet und Zuweisung:
{s 7→ 1, e 7→ 1, n 7→ 1, x 7→ 0.0001, rj 7→ 0, rw 7→ None)]
nach n = n+1
{s 7→ 1, e 7→ 1, n 7→ 2, x 7→ 0.0001, rj 7→ 0, rw 7→ None)]
nach e = e+s
{s 7→ 1, e 7→ 2, n 7→ 2, x 7→ 0.0001, rj 7→ 0, rw 7→ None)]
...
Bemerkung 3.4.17 Man sieht, dass Zuweisungen bzw. allein die Vermutung,
dass welche erfolgen könnten, die Parallelisierbarkeit von Programmen verhindert.
Die Reihenfolge der Auswertung in Python spielt eine große Rolle und kann
nicht verändert werden, ohne die Bedeutung der Programme zu ändern.
In Python wird das Ausmaß der Seiteneffekte etwas zurückgedrängt, auch
wenn es noch ein normales Mittel der Programmierung ist.
3.4.5
Anzahl Auswertungsschritte mit Funktionsanwendungen und return
Die Anzahl der Auswertungsschritte einer Auswertung kann man auf Programme mit Funktionsanwendung erweitern, wenn man vereinbart, dass return
selbst als eine Auswertung zählt, während das Überspringen der nachfolgenden Anweisungen, bis die Funktion verlassen wird, nicht mitzählt. Ebenso zählt
die Initialisierung von internen Variablen nicht mit.
Beispiel 3.4.18 Anzahl der Schritte am Beispiel der rekursiven FibonacciFunktion.
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else: return (fib(n-2) + fib(n-1))
Anzahl Schritte für fib(3):
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
Anzahl Schritte
1
1
2
1
2
1
1
2
1
1
2
1
2
1
2
1
...
3.4.6
29
welche Auswertung
fib(3)-Aufruf
if- Aufruf
n == 0
if- Aufruf
n == 1
return- Aufruf
+- Aufruf
n-2-Auswertung
fib-Aufruf (fib(1))
if- Aufruf (rekursiv in fib(1))
n == 0
if- Aufruf
n == 1
return- Aufruf: fib(1) zu Ende
n-2-Auswertung
fib-Aufruf (fib(2))
...
Argumentieren mittels operationeller Semantik
Man kann die Definition der operationellen Semantik verwenden, um Beweise
über Python-Programme zu führen. Als Beispiel nehmen wir die Funktion, die
die Fibonacci-Zahlen berechnet.
def fibw (n):
a,b = 0,1
while n > 0:
print b,
a,b = b,a+b
n = n-1
Wir zeigen: fibw(n) druckt F ibn als letzten Wert, unter der Voraussetzung,
dass n > 0 und kein Überlauf eintritt.
Man braucht noch eine Klarstellung, die nicht ganz offensichtlich ist: Die
Operationen, die das Programm ausführt, sind konzeptuell etwas anderes als
die mathematischen Operationen. Wir können aber annehmen, dass nach einer
Übersetzung der internen Datenstrukturen in mathematische Objekte sich die
Python-Operatoren +, −, ∗, /, > wie die mathematischen Operationen verhalten, insbesondere da wir angenommen haben, dass kein Überlauf eintritt.
Der Nachweis geschieht mit vollständiger Induktion nach Anzahl der
Durchläufe des while-Rumpfs. Eigentlich muss man zunächst beweisen, dass
die Schleife terminiert. Das ist aber einfach, da der Wert n immer kleiner wird.
Man braucht eine Idee, was man beweisen will. I.a. braucht man dafür eine
sogenannte Schleifeninvariante. Hier kann man diese leicht raten und erhält:
wenn man den Zeitpunkt vor Durchlauf der while-Schleife nimmt, und mit m
30
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
den Wert der Eingabe bezeichnet:
a = F ibm−n , b = F ibm−n+1
Diese Schleifeninvariante wird jetzt mit vollständiger Induktion nachgewiesen:
Die Induktion nach Anzahl der Schleifendurchläufe ergibt jetzt:
Basisfall m = n : a = F ibn−n = F ib0 = 0 stimmt. Und b = F ib1 = 1 stimmt.
Induktionsfall: Wenn n > 0: Die Schleife wird durchlaufen.
(Wir schreiben a, b, n für die alten Werte.)
print b ändert den Zustand nicht.
a,b = b,a+b ergibt: a = b, b = a + b
n = n-1 ergibt n = n − 1.
Vor der Schleife galt:
a = F ibm−n , b = F ibm−n+1
Wir berechnen a = b = F ibm−n+1 = F ibm−n . Das ist der erste Teil der Schleifeninvariante.
Die Berechnung für b ergibt: b = a + b = F ibm−n + F ibm−n+1
Einsetzen des veränderten Index n ergibt: b = F ibm−n−1 + F ibm−n . Nach der
Definition der Fibonacci-Zahlen ist das gerade b = F ibm−n−1 + F ibm−n =
F ibm−n+1 .
Damit ist die Behauptung mit Induktion bewiesen. Wir müssen noch argumentieren, was das letzte gedruckte b ist. Offenbar ist dann n = 1. Es ergibt
sich, dass b = F ibm−1+1 = F ibm gedruckt wurde.
Beispiel 3.4.19 Dieses Beispiel soll zeigen, dass man auch in Python mit einem let doppelte Auswertungen vermeiden kann.
Die Funktion f (x, y) := x(1 + xy)2 + y(1 − y) + (1 + xy)(1 − y) läßt sich
effizienter berechnen, wenn man definiert:
a
b
f (x, y)
:=
:=
:=
1 + xy
1−y
xa2 + yb + ab
Die entsprechende Definition sieht so aus:
def polyf(x,y):
a = 1 + x *y
b = 1 - y
return (x * a**2 +
3.4.7
y* b + a*b)
Lokale Funktionen und geschachtelten Gültigkeitsbereiche
Python erlaubt in der Version 2.3 geschachtelte Funktionsdefinitionen, d.h. solche, die innerhalb einer Funktionsdefinition gemacht werden und auf die lokalen
Variablen der Ober-Funktion zugreifen dürfen. Hier gibt es Beschränkungen bei
Zuweisungen: Externe Variablen (außer globale) dürfen nicht verändert werden,
aber sie dürfen referenziert werden.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
31
Beispiel 3.4.20
def f(x):
def g(y):
return x+y;
return g(11);
Der Aufruf f(4)ergibt 15. Hier ist die textuelle Sichtbarkeit (lexikalischer
Gültigkeits-Bereich) entscheidend. Der Aufruf f(11) hat als Rückgabewert den
Wert von g(11). Im Rumpf von g wird x+y ausgewertet: Da g die Variable x
von f sieht, ergibt sich 4+11, d.h. 15.
Bei folgender unabhängiger Definition von f und g ergibt sich ein Fehler.
def f(x):
return g(11);
def g(y):
return x+y;
Der Grund ist, dass g die lokale Variable x von f nicht sieht. Das Prinzip,
das dahintersteht ist der lexikalische Gültigkeits-Bereich von Variablen.
Die Semantik von Funktionen soll erhalten bleiben, wenn man die formalen und
lokalen Parameter einer Funktion nur in dieser Funktion umbenennt. Würde der
externe Zugriff auf x oben noch funktionieren, so wäre das nach Umbenennung
nicht mehr möglich:
def f(z):
return g(11);
def g(y):
return x+y;
Die allgemein verwendete Variante der Variablensichtbarkeit ist der lexikalische Gültigkeitsbereich, d.h., man darf nur Variablennamen verwenden, die im
Programmtext auf derselben oder einer höheren Stufe verwendet werden.
Die andere Methode, die oben im Beispiel nicht funktionierte, und die man
in Ausnahmefällen (mit global-Definition) verwendet, allerdings so, dass man
sie wieder als Variante der lexikalische Sichtbarkeit sehen kann, nennt man dynamischen Gültigkeitsbereich der Variablen (hier x). Die Funktion g sieht x in
der Ablaufumgebung und versucht diese zu ändern.
In Python wird das Prinzip für globale Variablen verwendet, nachdem sie
als solche deklariert wurden. Diese Methode hat ohne extra Deklaration schwerwiegende Nachteile, da sie die theoretische Behandlung der Bedeutung von Programmen sehr schwierig macht und zudem Namensabhängigkeiten einführt.
Der Effekt der Auswertung in Python bei geschachtelten Funktionsdefinitionen kann so beschrieben werden:
• Wird ein undeklarierter Name referenziert, d.h. der Wert abgefragt, dann
wird im nächsthöheren Gültigkeitsbereich gesucht.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
32
• Wird im innersten Gültigkeitsbereich ein Variablenname definiert, kann
man nicht mehr auf den gleichen Variablennamen im höheren Gültigkeitsbereich zugreifen, auch wenn man den ganz unten definierten Variablennamen wieder löscht (mit del)
• Variablennamen aus höheren Gültigkeitsbereichen können nicht gelöscht
werden (mit del) und sie können auch nicht neu gebunden werden. (mit
x = ...), ein solches Assignment führt dazu, dass eine neue lokale Deklaration angelegt, und gleichzeitig auch dazu, dass sämtliche Anweisungen im
selben Gültigkeitsbereich, die x referenzieren, sich nur auf diesen Gültigkeitsbereich beziehen
Wir betrachten noch ein Beispiel:
Beispiel 3.4.21
def fff():
v = 2;
def ggg():
v = 3;
return hhh ();
def hhh():
return v
return ggg();
Hier ist die Frage, welchen Wert fff()zurückgibt: 2 oder 3? Analyse ergibt,
dass der lexikalische Gültigkeitsbereich der lokalen Variablen v der Rumpf von
fff ist außer dem Rumpf von ggg, ebenso der Rumpf von hhh. Damit ergibt
sich 2 als Wert.
Betrachten wir das Prinzip der Umbenennung und das Python-Prinzip der
Lokalisierung aller veränderbaren Variablen, dann ist das v in ggg lokal in ggg,
also kann man folgendermaßen umbenennen:
def fff():
v = 2;
def ggg():
w = 3;
return hhh ();
def hhh():
return v
return ggg();
Dann ergibt fff() immer noch 2.
3.5
Listenverarbeitung in Python
Es gibt Datentypen, die Sequenzen von Objekten implementieren: Tupel, Listen
und Strings. Tupel und Listen werden analog zu Haskell dargestellt. Z.B. (1, 2, 3)
33
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
ist das 3-Tupel aus den Zahlen 1,2,3, und [1, 2, 3] die Liste dazu. Tupel sind in
der Länge festgelegt, während Listen ihre Länge ändern können.
Es gibt in Python die Listenfunktionen map, +, filter, und reduce, die
vordefiniert sind. Sie haben analoge Ergebnisse und Funktionalitt wie die Funktionen map, ++, filter, und foldl in Haskell.
Beachte: Die Listenfunktionen in Prfixschreibweise verndern die Liste nicht,
whrend die Listenfunktionen in Attributschreibweise die Liste verndern knnen.
Hier ein Auszug aus einer Dokumentation zu Python.
Operationen auf allen Arten von Folgen (Listen, Tupel, Strings) 2
Operation Resultat
x in s
True wenn ein Eintrag von s = x, sonst False
x not in s False wenn ein Eintrag von s gleich x, sonst True
s+t
Konkatenation von s und t
s∗n
n Kopien von s aneinander gehängt
s[i]
i-es Element von s, Start mit 0
s[i : j]
Teilstück s ab i (incl.) bis j (excl.)
len(s)
Länge von s
min(s)
Kleinstes Element von s
max(s)
Größtes Element von s
Bemerkungen
Bem.
(1)
(1), (2)
(1) Wenn i oder j negativ ist, dann ist der Index relativ zum Ende des String,
d.h. len(s)+i oder len(s)+j wird eingesetzt. Beachte, dass -0 = 0.
(2) Das Teilstück von s von i bis j wird definiert als die Folge von Elementen
mit Index k mit i <= k < j. Wenn i oder j größer als len(s) sind, nehme
len(s). Wenn i weggelassen wurde, nehme len(s). Wenn i ≥ j, dann ist das
Teilstück leer.
Operation auf Listen (mutable sequences)3
Operation
s[i] = x
s[i : j] = t
del s[i : j]
s.append(x)
s.extend(x)
s.count(x)
s.index(x)
s.insert(i, x)
s.remove(x)
s.pop([i])
s.reverse()
s.sort([cmpF ct])
2 Aus
3 Aus
Resultat
item i von s ersetzt durch x
Unterliste s von i bis j ersetzt durch t
dasselbe wie s[i : j] = []
dasselbe wie s[len(s) : len(s)] = [x]
dasselbe wie s[len(s) : len(s)] = x
Return: Anzahl der i mit s[i] == x
Return: kleinstes i mit s[i] == x
dasselbe wie s[i : i] = [x] wenn i >= 0
dasselbe wie del s[s.index(x)]
dasselbe wie x = s[i]; del s[i]; return x
Umdrehen von s in place
Sortiere s in place
Quick-Reference http://rgruet.free.fr/#QuickRef
Quick-Reference http://rgruet.free.fr/#QuickRef
Bemerkungen
(5)
(1)
(1)
(4)
(3)
(2), (3)
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
34
Kommentare:
(1) Ergibt eine ValueError-Exception, wenn x nicht in s, d.h. wenn der Index
außerhalb des gültigen Bereichs ist.
(2) Die sort()-Methode nimmt ein optionales Argument, das eine Vergleichsfunktion für 2 Argumente ist, die -1, 0, or 1 als Wert ergeben muss,
abhängig davon, ob das erste Elemente kleiner als, gleich oder größer als
das zweite Argument sein soll. Beachte dass dadurch das Sortieren sehr
viel langsamer wird.
(3) Die sort() und reverse()-Methoden ändern die Liste in-place. Es gibt keinen
Rückgabewert zur Warnung vor diesem Seiteneffekt.
(4) Die pop()-Methode wird nur für Listen unterstützt. Das optionale Argument i wird automatisch als -1 definiert, so dass normalerweise das letzte
Element entfernt und zurückgegeben wird.
(5) Ergibt eine TypeError-Exception, wenn x kein Listenobjekt ist.
Weitere Funktionen, die nicht in Attributschreibweise angewendet werden:
Operation
Resultat
map(f,s)
Liste [f s[0], f s[1], . . . ]
filter(p,s)
Liste der Elemente mit p(s[i]) = True
reduce(op,s)
s[0] op s[1] op . . .
reduce(op,s,init) Dasselbe wie init op (reduce(op,s))
zip(s,t)
Liste der Paare der Elemente von s,t.
Der Rückgabewert (return . . . ) der Funktionen entspricht im allgemeinen
dem der Haskellfunktionen. Bei Seiteneffekten haben die Listen-Funktionen von
Python ein ganz anderes Verhalten als in Haskell. Haskell ist referentiell transparent, was bewirkt, dass die Listenargumente nach einer Funktionsanwendung
unverändert sind, während eine Funktionsanwendung bzw. Operatoranwendung
in Python oft eine Veränderung der Listenstruktur, und damit der Werte von
Argumentvariablen nach sich zieht. Einige Funktionen sind nur dazu gemacht,
genau dies zu bewirken. Z.B. reverse dreht die Python-Liste um.
Wir geben zur Listenverarbeitung in Python einige Beispiele an:
>>> range(20)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> range(5,10)
[5, 6, 7, 8, 9]
>>> len(range(10))
10
>>> len(range(1000000))
Traceback (most recent call last):
File "<input>", line 1, in ?
MemoryError
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
35
>>>len(xrange(1000000))
1000000
>>> a = [’a’, ’b’,’c’]
>>> a
[’a’, ’b’, ’c’]
>>> b = [3,4,5]
>>> a.extend(b)
>>> a
[’a’, ’b’, ’c’, 3, 4, 5]
## a ist modifiziert
>>> b
[3, 4, 5]
>>> a.append(b)
>>> a
[’a’, ’b’, ’c’, 3, 4, 5, [3, 4, 5]]
>>> a.reverse()
>>> a
[[3, 4, 5], 5, 4, 3, ’c’, ’b’, ’a’]
>>> b
[3, 4, 5]
## b bleibt
>>> b.reverse()
>>> a
[[5, 4, 3], 5, 4, 3, ’c’, ’b’, ’a’]
## a ist modifiziert!
Variablen, die in einem Aufruf f (t1 , . . . , tn ) nicht erwähnt werden, können
trotzdem nach der Auswertung dieses Aufrufs andere Werte haben. Das passiert
durch sogenanntes aliasing: der gleiche Speicherbereich hat verschiedene Namen.
Schleifen in Python
Schleifen kann man in Python programmieren mittels while oder for: Hier ein
Beispiel für for:
>>> for i in range(1,11):
...
print(i)
...
1
2
3
4
5
6
7
8
9
10
>>>
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
36
Beispiel zu vordefinierten Listenfunktionen
Beispiele zu den Listenfunktionen map, +, filter, und reduce, die analog zu
map, ++, filter, und foldl in Haskell sind.
Folgende Funktion listcopy erzeugt eine Kopie einer Liste:
def id(x): return x
def listcopy(x):
return map(id, x)
def geradeq(n):
return n%2 == 0
>>> filter(geradeq,range(20))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> map(quadrat,range(1,10))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
>>>[1,2,3]+[11,12,13]
>>>[1,2,3,11,12,13]
Die Summe aller Elemente einer Liste kann man ebenfalls analog zu den
Haskell-Methoden berechnen:
def add(x,y): return x+y
>>> reduce(add,range(100))
4950
def mal(x,y): return x*y
def fak(n): return reduce(mal,range(1,n+1))
Alias-Namens-Problematik
Alias-Namens-Problematik (Aliasing): Der gleiche Speicherbereich wird von mehreren Variablen (-Namen) referenziert.
Effekte: Der Wert (bzw. das Datenobjekt) zu einer Variablen x kann
sich im Laufe der Programmbearbeitung verändern, ohne dass diese
Variable im Programm in einem Befehl, der den Speicher verändert,
vorkommt.
Allgemeiner kann auch ein Objekt unter dem Namen A erreichbar
sein, und Teile des Objekts unter anderen Namen.
Das bedeutet, dass der einfache Schluss:
B wurde im Aufruf nicht erwähnt, also ist es nach der Auswertung des Aufrufs
nicht verändert! falsch sein kann.
Ein weiteres Beispiel für die Effekte der Alias-Namens-Problematik:
37
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
>>>
>>>
>>>
[3,
>>>
[3,
>>>
>>>
[1,
>>>
[1,
a = [3,2,1]
b = a
a
2, 1]
b
2, 1]
a.sort()
a
2, 3]
b
2, 3]
Um sich ein Bild von verketteten Listen zu machen, ist folgendes Darstellungsweise von Listen in einem Diagramm hilfreich, das die Situation nach der
Zuweisung a = [1,2,3]; b = a repräsentiert.
• Pfeile ausgehend von Namen bedeuten Bindungen: a 7→ . . .. Andere Pfeile
kann man ebenfalls als Bindung ansehen, nur sind die Namen implizit.
repräsentiert ein Paar von zwei impliziten Namen. In einer
• Eine Box
Implementierung wird man das als Adressen (Zeiger) darstellen.
Der erste Pfeil von der linken Zelle einer Box ausgehend ist die Bindung
des Listenelementes.
Der zweite Pfeil ist die Bindung an die Restliste (Liste ohne das erste
Element).
• Die zweite Zelle der letzten Box symbolisiert einen Zeiger auf die leere
Liste (Nil).
b
a
Nil
1
2
3
Es ist zu beachten, dass bei a.extend(b) eine Kopie der Liste b an a angehängt wird. Bei append wird nur der Verweis genommen:
>>>
>>>
>>>
>>>
[1,
>>>
a = [1,2,3]
b = [4,5,6]
a.append(b)
a
2, 3, [4, 5, 6]]
b.reverse()
38
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
>>> a
[1, 2, 3, [6, 5, 4]]
>>>
Bild des Speichers nach den obigen Befehlen:
a
Nil
2
1
3
b
Nil
6
5
4
Python nutzt die Flexibilität der Listendarstellung nicht aus, sondern es
wird intern optimiert. D.h. Man hat Listenobjekte, die keine Teillisten haben,
so dass man eigentlich ein anderes Diagramm verwenden könnte:
>>> a = [1,2,3]
a
Nil
1
>>>
>>>
>>>
[1,
>>>
>>>
2
b = [4,5,6]
a.append(b)
a
2, 3, [4, 5, 6]]
b.reverse()
a
3
39
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
[1, 2, 3, [6, 5, 4]]
>>>
a
Nil
1
2
3
b
Nil
6
5
4
Stacks und Queues
Ein Stack (bzw. Keller, Stapel) ist eine oft verwendete Datenstruktur. In Python kann man einen Stack leicht implementieren, man kann sogar eine Liste
von beiden Seiten als Stack verwenden:
push e auf Stack a: a.insert(0,e)
Benutzung der Listen von links:
a.insert(0,b)
a.pop(0)
##push(b)
Benutzung der Listen von rechts:
a.append(b)
a.pop()
Es tritt ein Fehler auf, wenn man vom leeren Stapel etwas herunternehmen
will.
Beispiel 3.5.1 Ein Beispiel, das sich eines Stacks bedient, ist die Verwendung
eines Umgebungsstacks zur Auswertung von (Python-)Ausdrücken. Das Umdrehen einer Liste wurde schon im Haskell-Kapitel besprochen.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 07.01.2005
40
Eine Schlange, Queue ist eine Datenstruktur, die sich wie eine Warteschlange verhält: Vorne wird bedient (abgearbeitet), und hinten stellen sich Leute an
(first-in first out, fifo).
In Python kann man das effizient implementieren mit einer der folgenden
Kombination von Operationen.
a.insert(0,b), a.pop() oder a.append(b), a.pop(0).
3.5.1
Auswertungsregeln für Listenfunktionen in Python:
operationale Semantik
Dieser Paragraph soll verdeutlichen, dass man die operationale Semantik der Listenverarbeitung in einer imperativen Programmiersprache wie Python formal
darstellen kann, wobei man den Speicher nur als abstrakte Schnittstelle benötigt,
Natürlich wird dies dann letztlich doch auf einen adressierbaren Speicher abgebildet, aber man kann danach konzeptuell trennen zwischen den minimalen
Erfordernissen der Auswertung und den zufälligen Eigenschaften der Implementierung. Z.B. ist die Größe der Adressen oder die Lokalität eher eine Eigenschaft
der Implementierung auf einem Rechner mit linearem Speicher.
Der Heap (Halde) dient dazu, Daten mit Struktur, die vom Programm
modelliert werden, darzustellen, und deren Veränderung korrekt zu beschreiben.
Dazu werden Heap-Variablen verwendet, deren Zweck es ist, Zeiger abstrakt
zu modellieren und Objekte bereitzustellen, die vom Programm aus referenziert werden können. 4 . Um auch die Problematik der nicht mehr erreichbaren
Speicherteile korrekt zu behandeln, führen wir zusätzlich noch den Begriff der
Wurzelvariablen ein. Die Wurzelvariablen haben nur den Zweck, den garbagecollector zu unterstützen.
Definition 3.5.2 Eine Halde (Heap) besteht aus folgenden Komponenten:
• Einer Menge VH von Heap-Variablen, d.h. einer Menge von Namen.
• Einer Menge von HH-Bindungen an Heapvariablen, wobei zwei Arten von
HH-Bindungen vorkommen:
– x 7→ v: eine HH-Bindung eines Wertes v (Zahl, Character oder Nil)
an die Heap-Variable x, oder x 7→ ⊥ , x 7→ None.
– x 7→ [x1 |x2 ]: eine HH-Bindung eines Paares (einer Box) aus zwei
Heap-Variablen [x1 |x2 ] an die Heap-Variable x.
Folgende Zusatzbedingungen müssen erfüllt sein:
Es darf pro Heap-Variable nur eine HH-Bindung im Heap sein.
Jede Heap-Variable, die in einer Box vorkommt, muss auch eine HH-Bindung
im Heap haben.
Meist werden einige Heapvariablen als Wurzel-Variablen markiert. Die Menge der Wurzelvariablen nennen wir VW ; wobei VW ⊆ VH gelten muss.
4 Wie
bei anderen Namen ist die Implementierung sinnvoll als Zeiger.
41
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
Die beiden Formen der HH-Bindung kann man folgendermaßen darstellen:
x
x
v
x1
x2
Jede Heapvariable repräsentiert ein komplexes Objekt, das man als gerichteten Graphen sehen kann, wobei nur binäre Verzweigungen vorkommen und
die Daten an den Blättern sind. Eine Programmvariable z wird dann durch die
(PH-) Bindung {z 7→ x}, an ein Objekt gebunden, wobei x die Heapvariable ist,
die das Objekt repräsentiert.
Wir wiederholen die Definition der Bindungen und Umgebung zu Programmen (siehe Def. 3.4.1), wobei statt Zahlen und Zeichen auch Objekte aus einer
Halde verwendet werden.
Definition 3.5.3 Gegeben eine Halde H. Eine (Programmvariablen- bzw. PH) Bindung (bzgl. der Halde H) ist ein Paar x 7→ y, wobei x eine ProgrammVariable und y eine Heap-Variable ist.
Ein Umgebungsrahmen (Frame) zu einer Halde H ist eine endliche Menge
R von PH-Bindungen zur Halde H. Pro Programm-Variablennamen kann nur
eine PH-Bindung in R enthalten sein.
Eine Umgebung (Variablenumgebung, Zustand) ist ein Tripel (R1 , R2 , H)
von zwei Umgebungsrahmen, des lokalen Umgebungsrahmens R1 bezüglich der
Halde H, und des globalen Umgebungsrahmens R2 bezüglich der Halde H; und
der Halde H.
Wir nehmen an, dass die Wurzel-Variablen von H mindestens die HeapVariablen sind, die in den Variablenumgebungen R1 , R2 als Ziele vorkommen.
Oft ist die Umgebung als Stack organisiert, der die Auswertung von Ausdrücken unterstützt. Unsere Modellierung erlaubt keine Werte direkt auf dem
Stack. In realen Implementierung werden zur Optimierung oft Werte direkt auf
dem Auswertungsstack abgelegt und verarbeitet, da dies effizienter ist.
Beispiel 3.5.4 Die Darstellung der Liste [100] nach dem Aufruf z1 = [100]
mit Umgebung und Halde ist folgendermaßen:
lokaler Umgebungsrahmen
{z1 7→ z2}
globaler Umgebungsrahmen ∅
Halde
{z2 7→ [z3|z4]
z3 7→ 100
z4 7→ Nil}
42
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
z2
z1
z3
z4
100
Nil
Beispiel 3.5.5 Darstellung einer zyklischen Liste, wobei man diese nur so
drucken kann: [[[[[...]]]]]]
AA
AB
Nil
Die Halde dazu sieht so aus: {AA 7→ [AA|AB], AB 7→ Nil}
H,∗
Definition 3.5.6 Die Erreichbarkeitsrelation −→ des Heaps H kann man folgendermaßen definieren:
Eine Heap-Variable y ist von der Heap-Variablen x aus erreichbar, Notation
H,∗
x −→ y, wenn x = y, oder wenn es eine endliche Folge von Variablen x1 , . . . , xn
gibt, so dass x = x1 und y = xn und für alle i = 1, . . . n − 1 entweder
xi 7→ [xi+1 |x0i+1 ] ∈ H oder xi 7→ [x0i+1 |xi+1 ] ∈ H wobei x0i+1 irgendwelche
Heap-Variablen sein dürfen.
Eine Heap-Variable x ist erreichbar (aktiv), wenn es eine Wurzelvariable x0
H,∗
gibt mit x0 −→ x, andernfalls heißt sie unerreichbar (redundant).
H,∗
Bemerkung 3.5.7 Die Erreichbarkeitsrelation −→ des Heaps H kann man
auch als die reflexiv-transitive Hülle der folgenden Relation definieren:
H,∗
H,∗
Wenn x 7→ [x1 |x2 ] ∈ H, dann gilt x −→ x1 und x −→ x2 .
Die Bindungen von unerreichbaren Variablen kann man aus dem Heap entfernen. In einer Implementierung nennt man das garbage collection. Wurzelvariablen sind i.a. die Heap-Variablen, auf die vom Stack aus referenziert wird und
die Heap-Variablen, auf die das Programm referenziert.
43
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
Jede Variable der Halde repräsentiert einen Basiswert oder eine komplexere
Datenstruktur. z.B.: In der Halde H = {x 7→ [x1 |x2 ], x1 7→ 1, x2 7→ Nil)}
repräsentiert die Halden-Variable x die Liste [1] und x2 die leere Liste.
x
x1
x2
Nil
1
Regeln der (erweiterten) operationalen Semantik
Der Aufwand dafür ist etwas höher als bei der ersten Variante der operationalen
Semantik von Python, die nur mit Zahlen umgehen konnte, da auch zyklische
Strukturen beschreibbar sein sollen.
Der Zustand besteht aus Bindungsumgebung und Heap. Die übliche Umgebung wird so verallgemeinert, dass jeder Programmvariablen eine Heapvariable
zugeordnet wird, die ihrerseits auf einen (evtl. komplexeren) Wert im Heap verweist.
Jetzt können wir die Auswertung von Listenausdrücken und Listenfunktionen in Python mittels dieser Halden-Modellierung beschreiben: Wir verwenden
die Klammer [[.]], um syntaktische Konstrukte einzuklammern, damit diese von
den Operationen deutlich getrennt werden, und verwenden die Notation
(Z, s) →e (Z 0 , x)
um eine Auswertung des Ausdrucks s im Zustand Z zu bezeichnen, wobei x die
Heapvariable ist, die den zurückgegebenen Wert repräsentiert und Z 0 der neue
Zustand ist.
Definition 3.5.8 Eine Auswertung des Listenausdrucks [a1 , . . . , an ] in einer
Halde H folgt der Regel (n ≥ 1):
(Z; [[a1 ]]) →e (Z1 ; x1 ) (Z1 ; [[[a2 , . . . , an ]]]) →e (Z2 ; x2 ) Z2 = (R2,1 , R2,2 , H2 )
(Z; [[[a1 , . . . , an ]]]) →e (R2,1 , R2,2 , H2 ∪ {x 7→ [x1 |x2 ]}); x
Am Ende der Listen verwenden wir die spezielle Konstante Nil:
((R1 , R2 , H); [[[]]]) →e ((R1 , R2 , H ∪ {x 7→ Nil}); x)
Die Auswertung des Listenausdrucks [a1 , . . . , an ] kann man so umschreiben:
Zuerst wird a1 ausgewertet, danach a2 usw., bis an . Der Zustand am Ende
ist der letzte Zustand nach dieser Auswertung. In der Halde repräsentiert die
Haldenvariable x die ausgewertete Liste.
44
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
Die Alias-Namens-Problematik tritt jetzt auch bei Zuweisung von Listenvariablen auf.
Sei die Folge der Befehle gegeben:
a = [1,2]
b = a
Dann entsteht folgende Struktur in der Umgebung und in der Halde: Nach
a = [1,2]:
Bindungen: . . . , a 7→ u1 , . . .
Heap: {u1 7→ [u2 |u3 ], u2 7→ 1, u3 7→ [u4 |u5 ], u4 7→ 2, u5 7→ Nil, . . .}
Nach der weiteren Zuweisung b = a entsteht der Zustand
Bindungen: . . . , a 7→ u1 , . . . , b 7→ u1 , . . .
Heap: {u1 7→ [u2 |u3 ], u2 7→ 1, u3 7→ [u4 |u5 ], u4 7→ 2, u5 7→ Nil, . . .}
Wenn jetzt innerhalb der Liste zu a etwas verändert wird, dann ändert sich
automatisch etwas in der Liste, auf die b zeigt.
a
u1
b
u2
1
u3
u4
u5
2
Jetzt können wir auch die Auswertung von Funktionen wie len, append,
insert, . . . angeben. Für die folgende Regel nehmen wir der Einfachheit halber
an, dass a, b Programm-Variablen sind.
((R1 , R2 , H); [[a.append(b)]]) →e ((R1 , R2 , H 0 ); None)
Wobei H = H0 ∪ {x 7→ Nil} der Heap vor der Auswertung ist. x
sei letzte Heap-Variable der Liste zu a die auf Nil zeigt, b 7→ x1 ist
Bindung in der Umgebung R1 , und
H 0 = H0 ∪ {x 7→ [x1 |x2 ], x2 7→ Nil} mit der neu erzeugten HeapVariablen x2 .
D.h. die x-Bindung {x 7→ Nil} im Heap wird ersetzt durch {x 7→ [x1 | x2 ]},
wobei x2 neu eingeführt wird.
Die Auswertung von a.append(b) kann man sich so veranschaulichen:
Nil
45
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
Vorher:
a
u1
b
x
x1
Nil
w
Nachher:
a
u1
b
x1 x2
x
x1
Nil
w
Dadurch wird (die Liste) b als letztes Element an die Liste angehängt.
Durch append-Anwendungen kann auch eine Liste entstehen, die unendlich tiefe Verschachtelung hat, indem man z.B. a.append(a) ausführt. Die zugehörige
Halde für die Liste a = [1, 2] sieht so aus:
{u1 7→ [u2 |u3 ], u2 7→ 1, u3 7→ [u4 |u5 ], u4 7→ 2, u5 7→ [u1 |u7 ], u7 7→ Nil, . . .}
Diese Halde ist ein Graph, der Zyklen hat. Man sieht auch, dass im Diagramm
u1 zweimal vorkommt, aber die zwei Pfeile der gleichen Bindung entsprechen.
a
u1
u2 u3
1
u4 u5
u1 u7
2
Als weiteres Beispiel die operationelle Semantik der Funktion insert. Für
diese Regel nehmen wir ebenfalls an, dass a, b Programm-Variablen sind.
((R1 , R2 , H); [[a.insert(i, b)]]) →e ((R1 , R2 , H 0 ); None)
wobei xi die Heap-Variable zum i-ten Listen-Tail ist. H = H0 ∪ {xi 7→
[ei+1 |xi+1 ]}, b 7→ y1 ∈ R1 , und H 0 = H0 ∪ {xi 7→ [y1 |y2 ], y2 7→ [ei+1 |xi+1 ]},
wobei y2 neue Heap-Variable ist.
Nil
46
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
vorher:
ei
xi
ei+1
...
xi+1
nach insert:
ei
xi
ei+1
y1
xi+1
y2
Der Fall, dass die Liste zu Ende ist, geht analog: H = H0 ∪ {xi 7→ Nil},
b 7→ z1 ∈ S, und H 0 = H0 ∪ {xi 7→ [z1 |z2 ], z2 7→ Nil}.
Die operationelle Semantik von map kann damit ebenfalls beschrieben werden, allerdings ist diese Funktion aus kleineren Operationen zusammengesetzt,
so dass es einfacher wäre, für die Implementierung der Funktion map die operationale Semantik der kleineren Operationen zu beschreiben.
Zum Beispiel, indem man ersatzweise die folgende Funktion betrachtet.
def map_demo(f,xs):
if len(xs) == 0:
return [];
else:
wert = map_demo(f, xs[1:len(xs)]);
wert.insert(0,f(xs[0]));
## Seiteneffekt
return wert;
Den Inhalt des Elements mit Index i der Liste a kann man mittels
a[i] = b
verändern.
Wir geben hierfür die operationelle Semantik an, wobei wir annehmen, dass b
eine Programmvariable ist, und bei der Auswertung von Anweisungen rechts
nur den veränderten Zustand angeben.
((R1 , R2 , H); [[a[i] = b]]) → (R1 , R2 , H 0 )
wobei H = H0 ∪ {xi 7→ [y1 |y2 ]}, xi ist die Heap-Variable zum i-ten Listen-Tail,
b 7→ z ∈ R1 , und H 0 = H0 ∪ {xi 7→ [z|y2 ]}.
...
47
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
Indirektionen
Diese könnte man auch in der Halde erlauben. Es sind Bindungen von der Art
x1 7→ x2 , wobei x1 , x2 beides Heap-Variablen sind. Deren Bedeutung ist, dass der
Wert von x1 erst über die Referenz x2 und evtl. weitere Indirektionen gefunden
werden kann, wobei die Indirektionen für die Programmiersprache nicht sichtbar
sind.
Das formale Modell würde dadurch etwas komplizierter, da man den Wert
nicht direkt finden kann. Man muss auch Indirektionszyklen beachten, die eine
Nichtterminierung des Wertesuchens bewirken könnten.
In Implementierungen werden Indirektionen z.T. verwendet, aus verschiedenen Gründen: manche Operationen lassen sich leichter implementieren bzw.
formulieren, und manchmal ist es effizienter, Indirektionen zu verwenden.
3.5.2
Implementierung der Halde in einem Speichermodell
Zunächst das Speichermodell in dem die Halde implementiert werden soll.
Definition 3.5.9 Einen RAM-Speicher S kann man definieren als endliche
Folge von Bytes (1 Byte = 8 Bit), (alternativ: von Zahlen zwischen 0 und
28 − 1 = 255), nummeriert mit Indices 0, . . . , L − 1. L ist die Größe des Speichers.
Der gespeicherte Wert unter der Adresse i ist das Byte S(i), d.h. das i − te
Element der Folge, wenn die Zählung mit 0 beginnt.
Die benötigte Adresslänge in Bits ist dlog2 (L)e.
Die Zugriffsfunktion get(S, i, l) hat als Ergebnis die Subfolge von S der Länge l
ab i.
00101001
0
1
2
3
4
5
Eine Implementierung von Adressen eines Speichers mit Adresslänge 32 Bit
kann man durchführen, indem man eine Binärzahl, die durch eine Folge von 4
Bytes repräsentiert wird, als Adresse im gleichen Speicher interpretiert.
Heapvariablen kann man dann implementieren als Folge von 4 Bytes (auch Wort genannt), die im Speicher direkt hintereinanderliegen, d.h.
S(i), S(i + 1), S(i + 2), S(i + 3). Diese werden als Adressen interpretiert.
Ein Paar [x1 |x2 ] von zwei Heapvariablen kann man als hintereinanderliegende Folge von 8 Bytes implementieren, d.h. S(i), . . . , S(i + 7). Eine Zahl kann
man implementieren wie eine Adresse, nur wird der Wert als die binäre Zahl
selbst interpretiert.
48
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
Beispiel 3.5.10 Hat man z.B. (mit Programmvariable b, Heapvariable x) eine
Bindung b 7→ x und x 7→ 4, so kann man x als 4-Byte Adresse implementieren.
Wenn an der Stelle mit der Adresse x die Adresse i steht, dann ist der Wert
der Heapvariablen x ist dann der Wert unter dieser Adresse, d.h. S(i).
Hat x die Adresse 100, dann erhält man durch a = get(H, 100, 4) die Adresse
i, die zu x assoziiert ist, get(H, i, 1) ergibt dann den Wert, der bei korrekter
Implementierung dann 4 ist.
200
100
4
x
b
100
200
In dieser Implementierung geht die Information verloren, welcher Typ von
Daten gemeint ist. Das sieht man daran, dass man damit Indirektionen, Paare,
Nil, und Zahlen nicht unterscheiden kann. Je nach Implementierung benötigt
man daher i.a. 5 noch eine Markierung (englisch: Tag), die diese verschiedenen
Daten im Speicher voneinander unterscheidet.
D.h. man könnte vereinbaren:
Adresse:
Paar:
Ganze Zahl:
Nil:
kein Wert:
1
1
1
1
1
Byte
Byte
Byte
Byte
Byte
Markierung
Markierung
Markierung
Markierung
Markierung
(z.B.
(z.B.
(z.B.
(z.B.
(z.B.
binär
binär
binär
binär
binär
1), und 4 Byte Adresse.
2), und 8 Byte Adressen.
3), und 4 Byte Zahl.
4).
5).
Durch diese Haldenimplementierung haben wir eine abstrakte Schnittstelle
angedeutet, die auch anders implementiert sein kann, ohne dass sich die
Funktionalität der Halde bzw. der Umgebung ändert.
Z.B. braucht folgendes nicht zu gelten:
• die Adressen eines Adresspaares (einer Box) [x1 |x2 ] liegen direkt nebeneinander.
• Adressen sind als aufsteigende Folge von Bytes repräsentiert.
• Der nächste Eintrag im Array (Feld) hat Adresse des aktuellen Feldes +4.
Auch die Länge der Adressen ist nicht festgelegt.
5 Diese
soll.
Markierung wird nicht benötigt, wenn ein Programm vorher weiß, was dort stehen
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
3.6
49
Felder, Arrays in Python
In Python sind die Listen (und Tupel) gleichzeitig eindimensionale Felder, die
auch heterogen (d.h. mit Elementen unterschiedlichen Typs) sein dürfen. Mehrdimensionale Arrays und Matrizen kann man mit einem Array von Arrays, d.h.
mit Listen von Listen modellieren.
Die interne Implementierung eines Feldes macht es möglich, eine (praktisch)
konstante Zugriffszeit zu realisieren, wenn der Index bekannt ist. Allerdings ist
die Handhabung eines Feldes im Programm etwas aufwändiger, da man bei
Zugriffen stets den Index berechnen muss und da man im allgemeinen ein Feld
nicht so ohne weiteres verkleinern oder erweitern kann.
Folgend einige Beispiele:
Beispiel 3.6.1 Transposition einer 2-dimensionalen quadratischen Matrix in
Python
def transpose(a,n):
for i in range(n):
for j in range(i+1,n):
a[i][j], a[j][i] = a[j][i] , a[i][j]
return a
def testtranspose(n):
b = range(n)
for i in range(n):
b[i] = range(n)
for i in range(n):
print b[i];
c = transpose(b,n)
print " "; print "Nach Transposition:"
for i in range(n):
print c[i]
Beispiel 3.6.2 Matrixmultiplikation für quadratische Matrizen und Determinante einer Matrix
def matrixMult(a,b):
leng = len(a);
c = range(leng);
for i in range(leng):
c[i] = range(leng);
for j in range(leng):
c[i] [j] = 0;
for i in range(leng):
for j in range(leng):
sum = 0
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
50
for k in range(leng):
sum = sum + a[i][k]*b[k][j]
c[i][j] = sum
return c
def testmatmult(leng):
a = range(leng);
b = range(leng);
print a;
print b;
for i in range(leng):
a[i] = range(leng);
b[i] = range(leng);
for j in range(leng):
a[i] [j] = i;
b[i] [j] = j;
print a;
print b;
return (matrixMult(a,b))
###Determinante einer Matrix: rekursiv, exponentiell
\begin{verbatim}
def determinante(a):
n = len(a);
if n <= 0:
return 1;
else:
if n == 1:
return a[0][0]
else:
sum = 0;
flip = -1;
for i in range(n):
flip = (-1)*flip;
b = matrixcopy(a);
for j in range(n):
b[j].pop(0);
b.pop(i);
sum = sum + flip*a[i] [0] * determinante(b);
return(sum);
Beachte, dass es unter Benutzung der Gauß-Elimination einen ALgorithmus
mit Laufzeit O(n3 ) gibt.
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
3.6.1
51
Operationale Semantik: C
C ist eine wichtige und verbreitete Programmiersprache für maschinennahes
Programmieren, insbesondere Betriebssysteme. C-Compiler ergeben effizienten
Code. Der Abstraktionsgrad von C bzw. der C-Programme ist zwar höher als der
von Assembler-Sprachen, aber nicht so hoch wie der von Python oder Haskell.
Wir untersuchen die Eigenschaften der operationalen Semantik von C anhand eines Konstruktes.
Aus Kernighan/Ritchie: Programmieren in C:
Diese Regeln sind außerordentlich kompliziert, denn sie müssen mit
”
einer Mischung von Funktionen alten und neuen Stils fertigwerden.
Wenn möglich, sollten Mischungen vermieden werden.“
Die Reihenfolge, in der Argumente bewertet werden, ist nicht fest”
gelegt; man beachte, dass sich verschiedene Übersetzer hierin unterscheiden. Die Argumente und Funktionsbezeichner werden jedoch
vollständig bewertet [d.h. ausgewertet], mit allen Nebenwirkungen,
bevor die Ausführung der Funktion beginnt...“
Ein anderes Zitat zum Versuch einer Andeutung der (funktionalen) operationellen Semantik von C:
. . . In anderen Worten, wenn das n-te Kode-Fragment durch die
”
Funktion fn repräsentiert wird, und s0 ist der Anfangszustand, dann
ist der Endzustand definiert durch
sn = fn (fn−1 (. . . f1 (f0 (s0 )) . . .))
Die Ausführung der einzelnen Funktionen erlaubt . . . Parallelismus.“
Wir wollen hier mal explizit ein Code-Fragment untersuchen, dass wir im
Prinzip ala Python analysieren können. Dazu schnell die Erklärung eines COperators:
i++ bewirkt, wenn ausgewertet, den Seiteneffekt i := i+1 wobei i Programmvariable sein muss. Die Nebenbedingung ist: Wenn es in einem Ausdruck vorkommt, dann ist der Return-Wert i, und der Wert danach ist i+1. Um die
operationale Semantik hinzuschreiben, nehmen wir mal an, dass das auch in
Python geht. Die operationale Semantik ist dann:
v = wert(i, Z) w = v + 1 Z 0 = update(i, w, Z)
(Z; [[i++]]) →e (Z 0 ; v)
Zum Vergleich hier die operationale Semantik des Ausdrucks ++i, der sich von
i++ nur dadurch unterscheidet, dass der Returnwert bereits i+1 ist:
v = wert(i, Z) w = v + 1 Z 0 = update(i, w, Z)
(Z; [[++i]]) →e (Z 0 ; w)
Als Beispiel für i++ betrachte das C-Code-Fragment
Praktische Informatik 1, WS 2004/05, Kapitel 3, vom 11.01.2005
52
i = 7;
j = i++ * i++
Informelle Begründung für den erwarteten Zustand danach:
Die Multiplikation wertet i++ zweimal aus: Der Wert des linken Argument ist
7, des rechten Argument 8, da es um 1 erhöht wurde, danach hat i den Wert 9.
Der Wert von j ist 7 ∗ 8 = 56.
Zunächst mal eine Überraschung beim Ausprobieren (aus Dissertation Ronald Moore).
Der C-Code
i = 7;
j = i++ * i++;
kompiliert in gcc hinterlässt den Zustand j = 49, i = 8 statt j = 56, i = 9!
Erklärungsversuch: der Compiler versucht, die Doppelauswertung von i++ zu
vermeiden und ändert daher das Programm ab zu:
i = 7;
j = let x = i++ in
x*x;
bzw. in let-freier Schreibweise:
i = 7;
x = i++;
j = x*x;
Die operationale Semantik ergibt jetzt:
Nach Auswertung von x: {x 7→ 7, i 7→ 8, . . .}. Danach wertet man x*x aus und
erhält 49, wie vom gcc berechnet.
Fazit: Die Verwirrung, die beim Programmierer entsteht, wird bewirkt durch
eine (optimierende) Programmtransformation, die die operationale Semantik
nur erhält, wenn die Unterausdrücke keine Seiteneffekte bewirken; im Fall, dass
die Unterausdrücke Seiteneffekte bewirken, wird die operationale Semantik nicht
erhalten.
Herunterladen