Beschränkung aufs Wesentliche

Neben Perl und Tcl hat sich in den letzten Jahren mit Python eine weitere Scriptsprache etablieren können - natürlich objektorientiert.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 16 Min.
Von
  • Rainer Fischbach
Inhaltsverzeichnis

Python hängt das Attribut ‘Scriptsprache’ an. Das ist zwar nicht falsch, doch auch nicht geeignet, die Sprache hinreichend zu charakterisieren. Scriptsprachen sind en vogue. Dass das so ist, liegt an den Programmieraufgaben, die sich stellen, wenn man Dienste über das Internet abwickeln möchte: Automatisch Inhalt zu formatieren oder Anfragen zu analysieren und zu beantworten macht einen wachsenden Teil der Leistungen aus, die Software zu erbringen hat. Der Aufstieg von Perl hat sich vor diesem Hintergrund vollzogen.

Mehr Infos

Ressourcen

Alle wichtigen Informationen zu Python finden sich auf deren Home Site, inklusive Erweiterungen, Special Interest Groups und Anwendungen. Zu den größeren gehören das Web-Publishing-Werkzeug Zope (siehe iX 8/99, S. 64) und der Web-Browser Grail, aber auch viele kleine Helfer sind dort vermerkt.

Weshalb also noch eine solche Sprache? Was bietet sie, was Perl nicht bietet? Die beste Antwort darauf lautet ganz im Sinne von Mies van der Rohes Devise ‘less is more’: weniger. Python ist eine minimalistische Sprache. Das Reference Manual (mit weiterer Dokumentation online verfügbar: www.python.org/) gleicht seinem Umfang nach eher dem von Pascal. Ihr Schöpfer Guido van Rossum akkumulierte keine Features, sondern konzentrierte sich auf wenige Prinzipien:

  • Sparsamkeit durch Orthogonalität: Es gibt nur eine begrenzte Zahl von - leistungsfähigen und kombinierbaren - Konstrukten, Grunddatentypen und Standardfunktionen.
  • Abgestufte Strukturierungsmittel: auf mehreren Ebenen strukturierbar durch Module, Klassen, Funktionen und Codeblocks.
  • Programmkomponenten: dynamische Datenobjekte. Die strukturellen Einheiten von Programmen sind selbst solche Objekte. Sie lassen sich inspizieren, manipulieren, zur Laufzeit erzeugen und generell wie andere Daten handhaben.
  • Konsistente Erweiterbarkeit: Werkzeuge für spezielle Anwendungsgebiete sind nicht fest in die Sprache integriert, sondern Erweiterungsmodule realisieren sie; wobei die vorhandenen Konstrukte und Standardunktionen auf neue Objektklassen übertragbar sind.

Die Sparsamkeit der Ausstattung und die abgestufte Struktur machen Python zu einem akzeptablen Werkzeug des Software-Engineering. Nichts an Python ist grundsätzlich neu. Das Geniale liegt in einer gelungenen Auswahl von Ideen, die aus zum Teil weit außerhalb des Mainstream verlaufenden Entwicklungen stammen. In gewisser Weise bietet Python Lisp ohne die Klammern und Smalltalk ohne die OO-Dogmatik. Auch die regulären Ausdrücke, die Perl-Anhänger so schätzen, stehen in Form des re-Moduls zur Verfügung. Weitere Module bieten unter anderem Zugang zu wichtigen Internet-Diensten beziehungsweise -Protokollen der Transport- und Anwendungsebene (siehe dazu [1, 5]).

Lisp zeichnet sich durch die flexiblen und durch Literale notierbaren Listen aus. Python setzt diese Tradition nicht nur fort, sondern erweitert sie: Es gibt eine Menge von Reihungstypen (sequences), die Merkmale wie die Indizierbarkeit, das Längenattribut, die Möglichkeit, Ausschnitte zu bilden et cetera teilen. Die Reihentypen untergliedern sich in zwei Arten: die veränderlichen (mutable) und die unveränderlichen (immutable). Zur ersten Art gehören die Listen, zur zweiten die Zeichenketten und die Tupel. Die Werte dieser Typen sind durch entsprechende Literale notierbar:

mein_text = 'ein Text'
mein_tupel = ("Konstanz", (5.76,5.80))
deine_liste = [6, 9, 13, "sam"]
meine_liste = [mein_text, 2, 3,
deine_liste, mein_tupel]

Auch einzelne Zeichen sind Zeichenketten. Die Regeln für Sonderzeichen wie ‘\012’ entsprechen den von C her bekannten. Listen und Tupel können inhomogen und beliebig ineinander verschachtelt sein. Die Ausdrücke

'dein' + meine_liste[0][3:]
mein_tupel[1][1] + deine_liste[2]

liefern die Werte dein Text und 18,8. Die Indizes einer Sequenz s laufen von 0 bis len (s) - 1. Negative Indizes adressieren (durch stillschweigende Addition von len (s)) die Folge von hinten: s[-1] ist ihr letztes Element. Ausschnitte bildet man durch die Angabe ihres ersten und des auf das letzte folgenden Elements. Links vom Doppelpunkt bedeutet ein ausgelassener Index 0, rechts davon len (s). Von den Zuweisungen

meine_liste[4] = 'lindau', (5.40, 5.43))
mein_text[3:7] = ' A'
deine_liste[-2:] = ['spam', 18, '18']

produziert die zweite einen Fehler. Dagegen ist es nicht nur zulässig, ein durch seine Position identifiziertes Listenelement durch ein neues zu ersetzen, sondern auch einen ganzen Ausschnitt durch einen anderen, dessen Länge von der des ersetzten abweichen kann. Listenobjekte lassen sich durch spezielle Methoden verändern: append fügt ein Element am Ende an, und sort sortiert sie; wobei es möglich ist, die Vergleichsfunktion als optionalen Parameter anzugeben.

Die dritte Zuweisung verändert nicht nur deine_liste sondern auch meine_liste, da das an den ersten Namen gebundene Objekt auch ein Bestandteil des Objekts ist, an das der zweite gebunden ist. Die Zuweisung kopiert in Python keine Werte, sondern nur Referenzen. Sie bindet Namen an Objekte; was einschließt, dass mehrere Namen mittelbar oder unmittelbar an dasselbe Objekt gebunden sein können. Während das bei unveränderlichen Objekten nicht weiter auffällt, kann es bei veränderlichen einerseits als Mechanismus zur Kommunikation zwischen unterschiedlichen Programmkomponenten fungieren, andererseits jedoch zur Quelle unangenehmer Überraschungen werden.

Man muss Namen nicht deklarieren. Auch Datentypen sind nicht fest mit ihnen verbunden. Nur Objekte besitzen einen festen Typ. Python assoziiert mit jeder aktiven Programmkomponente (Funktion, Klasse, Modul) einen Namensraum (namespace), der den ihr angehörenden Namen Objekte zuordnet. Ein Name tritt in den Namensraum durch eine Zuweisung ein und verlässt ihn durch deren explizite Auflösung mittels der del-Funktion. Es ist weder nötig noch möglich, Objekte zu löschen. Python kümmert sich automatisch um Objekte, die keine Bindungen mehr aufweisen.

Der Mangel an statischer Typsicherheit impliziert auch Risiken, denen jedoch die Vorzüge einer dynamischen Sprache gegenüberstehen. Im Übrigen nötigen die meisten objektorientierten Sprachen, die angeblich ein statisches Typkonzept haben, de facto dauernd dazu, es mittels unsicherer Typumwandlung zu durchbrechen (das gilt auch für Java, siehe [2]), ohne jedoch die schönen Seiten eines dynamischen Konzepts zu bieten.

Ein Namensraum bildet eine Menge von Namen in eine Menge von Objekten ab. Die heutigen Python-Implementierungen tun das mittels eines weiteren Grunddatentyps der Sprache. Da das entsprechende Datenobjekt als Attribut __dict__ der jeweiligen Komponente verfügbar ist, können Programme auch ihren eigenen Zustand reflektieren. Als einzige Art der Gattung der Abbildungen (mappings) bietet Python das Wörterbuch (dictionary). Dahinter verbergen sich Hash-Listen, als deren Schlüssel alle unveränderlichen Objekte fungieren können. Es ist durchaus möglich, der Gattung eine durch AVL-Bäume realisierte Art hinzuzufügen oder gar eine, deren Elemente im Hintergrundspeicher auf B-Bäumen residieren. Auch für Wörterbücher gibt es Literale, und ihre Elemente kann man durch eine der Indizierung analoge Schreibweise adressieren; wobei der Schlüssel an die Stelle des Indexes tritt:

mein_woerterbuch = {'max': 32767,
(96, 69, 102): 414669,
333: 'issos keilerei',
' 1789: 'Revolution',
'agenda': ['ix-artikel', 'swr-vortrag']}
mein_woerterbuch ['provider'] = 9230
print mein_woerterbuch[333]

Wörterbücher und Listen sind als einzige Grunddatentypen veränderlich: Fließkommazahlen und ganze Zahlen verhalten sich wie ihre Pendants in C; wobei integer eher dem long in C entspricht. Lange ganze Zahlen können beliebige Länge haben und lassen sich durch Literale wie 999999999999999999L schreiben.

Python bietet eine geringe Zahl von Konstrukten zur Ablaufsteuerung: Die if-Auswahl mit optionalen elif- und else-Zweigen und zwei Formen von Schleifen mit den von C her bekannten break- und continue-Anweisungen. Während die while-Schleife dem üblichen Muster entspricht, weist die for-Schleife die Besonderheit auf, dass sie nicht explizit einen Zähler inkrementiert beziehungsweise dekrementiert, sondern durch eine Sequenz iteriert:

wort_liste = []
for (k, w) in mein_woerterbuch.items
():
print k, ': ', w
wort_liste.append (w)
print len (wort_liste), " Einträge"

items liefert die Schlüssel-Wertpaare, keys allein die Schlüssel eines Wörterbuchs in einer Liste. Die for-Schleife funktioniert mit allen Sequenzen und jedem Objekt, das die Methode __getitem__ besitzt, ebenso wie jedes Objekt, für das eine Methode __repr__ eine Darstellung als Zeichenkette liefert, mittels print druckbar ist. Die einheitliche Form der Iteration dient der intellektuellen Ökonomie. Sie findet sich ursprünglich in Barbara Liskovs Clu (siehe [3]). Die Zuweisung eines Tupels an ein Tupel von Variablen zerlegt jenes in seine Komponenten.

Für die Gruppierung von Anweisungen (Blöcke) sind keine Begrenzer nötig. Dies besorgt allein die Tiefe der Einrückung. Diese in Miranda, Haskell und Clean bewährte Technik zwingt zu einem übersichtlichen Kodierstil. Die Regel ‘Eine Anweisung pro Zeile’ erspart spezielle Trennzeichen (wenn der Platz nicht reicht, ist es möglich, weitere Zeilen beispielsweise durch offen bleibende Klammern einzubeziehen).

Die Funktionsdefinition ist eine Anweisung, die ein Objekt erzeugt und an den angegebenen Namen bindet. Jenes Objekt, das sich auch an weitere Namen binden, als Parameter an Funktionen übergeben und von solchen als Resultat liefern lässt, steht erst zur Verfügung, nachdem die Definition erfolgreich ausgeführt worden ist. Die folgende Funktion dmap_p liefert die Liste der durch dict den Elementen von seq zugeordneten Werte in der entsprechenden Reihenfolge.

def dmap_p (seq, dict):
r = []
for e in seq:
if e in dict.keys ():
r.append (dict[e])
return r

Wie bei den strukturierten Anweisungen bestimmt das Layout, was zur Funktionsdefinition gehört: alle nachfolgenden Zeilen, die tiefer eingerückt sind als die Kopfzeile. Dieser Bereich begrenzt auch die Gültigkeit aller Namen, die dort gebunden werden. Der Aufruf einer Funktion erzeugt einen lokalen Namensraum, den der Python-Interpreter als ersten heranzieht, um Namen aufzulösen. Erst dann geht die Suche zum globalen Namensraum über und anschließend zu dem der eingebauten Module. Der globale Namensraum ist der des Moduls, das heißt der Datei, in der der Text der Funktionsdefinition enthalten ist. import eröffnet den Zugriff auf die mit anderen Modulen assoziierten Namensräume. Während es durchaus möglich ist, Funktionen innerhalb von Funktionen zu definieren, ergibt sich daraus keine Einbettung der Gültigkeitsbereiche. Die mit verschiedenen Funktionsaufrufen verbundenen Namensräume sind immer disjunkt. Eine Funktion muss keine return-Anweisung enthalten. In diesem Fall liefert sie den Wert None.

Python unterstützt sowohl den prozeduralen als auch den funktionalen Programmierstil. Letzterem stehen einige der von Lisp her bekannten Werkzeuge zur Verfügung:

  • map wendet eine Funktion auf alle Elemente einer Sequenz an und liefert die Reihe der Ergebnisse in einer Liste zurück,
  • filter extrahiert die Elemente einer Sequenz, die einem Prädikat genügen,
  • reduce faltet eine Sequenz, indem es deren Elemente mit einer binären Operation verknüpft,
  • und mit Hilfe der lambda-Abstraktion kann man anonyme Ad-hoc-Funktionen definieren (die man selbstverständlich auch an Namen binden darf).

Die Mächtigkeit des lambda-Operators bleibt allerdings hinter der seines Pendants in Lisp zurück: Da die Konstrukte zur Steuerung des Kontrollflusses keine Ausdrücke, sondern Anweisungen sind, dürfen sie in einem lambda-Ausdruck nicht vorkommen. Das obige Beispiel lässt sich mit diesem Werkzeug knapper und eleganter reformulieren (siehe Listing 1).

Mehr Infos

LISTING 1

dmap_f liefert, hier mit lambda-Operator, die Liste der den Elementen von seq zugeordneten Werte.

def dmap_f (seq, dict):
return map (lambda x, d = dict: d×,
filter (lambda x, k = dict.keys (): x in k, seq))

Die Parameterübergabe erfolgt durch Zuweisung. Python wertet die auf den aktuellen Parameterpositionen stehenden Ausdrücke aus und bindet die resultierenden Objekte an die formalen Parameter. Von der Möglichkeit, in der Funktionsdefinition Ersatzwerte für beim Aufruf fehlende aktuelle Parameter anzugeben, macht Listing 1 Gebrauch, um den Parameter dict in die eingebetteten lambda-Definitionen zu schmuggeln. Da die textuelle Einbettung der Definition keinesfalls zu einer Inklusion der zur Laufzeit assoziierten Namensräume führt, bliebe dict bei der Ausführung der lambda-Funktion sonst undefiniert. Python erlaubt eine variable Anzahl positionaler oder durch Schlüsselworte identifizierter Parameter. Dem formalen Parameter ist im ersten Fall ein Stern und im zweiten ein Doppelstern voranzustellen. Die Werte stehen dann in einer Liste beziehungsweise in einem Wörterbuch zur Verfügung.

Python ist objektorientiert, doch gibt es keine alle Typen umfassende Klassenhierarchie. Alle Daten und Programmkomponenten sind Objekte, ohne notwendigerweise einer Klasse angehören zu müssen. Auch ganz unterschiedliche Typen können sich konform verhalten: Indizier-, Vergleich- oder Druckbarkeit gewährleisten, indem sie gewisse Methoden zulassen. Das entspricht eher den Typklassen von Haskell (siehe [4]) als dem OO-Fundamentalismus von Smalltalk oder Eiffel.

Die Definition von Klassen ist mit der von Funktionen vergleichbar: eine Anweisung, deren Ausführung ein Klassenobjekt produziert und an den angegebenen Namen bindet. Auch hier legt die Layoutregel fest, wie weit sie sich textuell erstreckt, und der entsprechend eingerückte Textabschnitt begrenzt den Gültigkeitsbereich der dort eingeführten Namen. Die Methodendefinitionen, die sich prinzipiell nicht von Funktionsdefinitionen unterscheiden, bilden ebenfalls eigene Gültigkeitsbereiche die nicht an denen der umgebenden Klassendefinition teilhaben. Die Namensräume des Klassenobjekts und der Methodenobjekte sind disjunkt.

Innerhalb der Methoden ist der Zugriff auf Klassenattribute (zu denen auch die Methoden gehören) nur durch Qualifikation mit dem Klassennamen, auf Attribute der Klassenausprägungen (Instanzen) nur durch Qualifikation mit dem self-Parameter möglich. Diesen ersten formalen Parameter bindet der Aufruf automatisch an sein Zielobjekt. Man muss ihn nicht self nennen, das entspricht lediglich der Smalltalk-Tradition. In Python bündeln Klassen und Objekte Speicher mit Methoden, ohne einen Zugriffsschutz zu gewähren. Der Aufruf eines Klassenobjekts erzeugt eine Instanz. Die Methode __init__ ist für deren Initialisierung verantwortlich.

Listing 2 enthält die Definition der Klasse simple_stack, die eine Liste verpackt und mit den Stack-Methoden ausstattet. Die Methoden __getitem__, __repr__ und __len__ sorgen dafür, dass ihre Ausprägungen indizier- und druckbar sind und dass sich die Standardfunktion len auf sie anwenden lässt. Die Klasse calc_stack in Listing 3 erbt von ihr und ergänzt sie um arithmetische Operationen. Klassen können mehrere Basisklassen haben, doch gibt es keine mit denen von Eiffel oder CLOS vergleichbare Mittel, um Namenskollisionen zu handhaben. Der Interpreter führt eine Tiefensuche von links nach rechts in der Liste der Basisklassen aus, um Attributnamen aufzulösen.

Mehr Infos

LISTING 2

simple_stack verpackt eine Liste und ist mit Stack-Methoden ausstattet.

class simple_stack:
def __init__ (self):
self.st = []
def push (self, x):
self.st.append(x)
def pop (self):
del (self.st[-1])
def top (self):
return self.st[-1]
def __len__ (self):
return len (self.st)
def __getitem__ (self, i):
return self.st[i]
def __repr__ (self):
d = string.join (map
(lambda n: `n`, self), '\012')
if d == '':
return '<stack empty>'
else:
return ('<stack contents:\012'
+ d + '\012>')
def clear (self):
del (self.st[:])
Mehr Infos

LISTING 3

calc_stack erbt von simple_stack und ergänzt diese Klasse um arithmetische Operationen.

class calc_stack (simple_stack):
def take2 (self):
p = (self.st[-1], self.st[-2])
del (self.st[-2:])
return p
def xch (self):
self.st[-1], self.st[-2] = (self.st[-2],
self.st[-1])
def dup (self):
self.st.append (self.st[-1])
def add (self):
(x, y) = self.take2 ()
self.st.append (x + y)
def mul (self):
(x, y) = self.take2 ()
self.st.append (x * y)
def exp (self):
(x, y) = self.take2 ()
self.st.append (y ** x)
def sub (self):
(x, y) = self.take2 ()
self.st.append (y - x)
def div (self):
(x, y) = self.take2 ()
self.st.append (y / x)
def ru (self):
self.st[:0] = [self.st[-1]]
del (self.st[-1])
def rd (self):
self.st.append (self.st[0])
del (self.st[0])

Wenn die Anweisung cs = calc_stack () ein Stack-Objekt erzeugt und cs.push (3) ein Methodenaufruf ist, der 3 auf den Stack cs legt, dann bezeichnet cs.push die an cs gebundene Methode push, die sich auch an einen Namen wie csp zuweisen lässt: csp (3) legt dann ebenfalls 3 auf cs. Selbstverständlich kann man eine Funktion oder Methode auch in einer Liste oder in einem Wörterbuch speichern. Von dieser Möglichkeit macht die Klasse upn_calc in Listing 4 Gebrauch. Deren Methode command erlaubt es, Operanden und Operationen durch Zeichenketten zu spezifizieren (um einen Operanden auf den Stack zu legen, genügt es, ihn hinzuschreiben). Sie liefert das jeweils auf dem Stack liegende Ergebnis oder eine Fehlermeldung zurück. Die Ausnahmebehandlung mit try S except entspricht dem von Java her bekannten Schema.

Mehr Infos

LISTING 4

command erlaubt es, Operanden und Operationen durch Zeichenketten zu spezifizieren.

class upn_calc:
def __init__ (self):
self.cs = calc_stack()
self.choices = {
"+": self.cs.add,
"-": self.cs.sub,
"*": self.cs.mul,
"/": self.cs.div,
"de": self.cs.pop,
"xp": self.cs.exp,
"di": self.cs.__repr__,
"sw": self.cs.xch,
"du": self.cs.dup,
"cl": self.cs.clear,
"ru": self.cs.ru,
"rd": self.cs.rd}
def command (self, s):
try:
if s in self.choices.keys ():
r = apply (self.choices[s], ())
else:
r = self.cs.push (float (s))
if r == None:
r = self.cs.top ()
except:
r = "illegal operand/operation"
return r

Der Operationsname selektiert die dazugehörende Operation aus dem Wörterbuch. Das entspricht der Funktionsweise einer case-Anweisung, ist jedoch flexibler. Das Wörterbuch der Operationen kann zur Laufzeit wachsen. Der Lisp-Tradition folgend stellt Python die Funktion apply zu Verfügung, die es ermöglicht, Funktionsaufrufe mit einem Tupel als Parameterliste, zur Laufzeit zu konstruieren.

Hier nicht eingesetzte, ebenfalls von Lisp her stammende Funktionen (eval und exec) werten Ausdrücke beziehungsweise führen Anweisungen aus, die man zur Laufzeit in Zeichenketten übergibt. Es ist auch möglich, Klassen und Objekte dynamisch mit neuen Attributen zu versehen. Um dabei nicht, was der Sache viel von ihrem Reiz nehmen würde, auf statische Ausdrücke angewiesen zu sein, gibt es die Funktionen getattr, setattr und hasattr, bei deren Aufruf der Attributname als Zeichenkette zu übergeben ist.

Wer keine Python-Gurus im Hause hat, aber überlegt, die Sprache einzusetzen, braucht keine Perl- oder C++-Experten. Ansatzweise Kenntnisse der Objektorientierung und etwas Erfahrung in C oder einer funktionalen Programmiersprache reichen als Voraussetzung völlig aus.

RAINER FISCHBACH
ist Consultant bei der Engineering Consulting & Solutions GmBH in Neumarkt.

[1] Tobias Himstedt; Objektbeschwörung; Python: objektorientierte Skriptsprache fürs World Wide Web

[2] Rainer Fischbach; Kalter Kaffee: Java: Programmiersprache der Zukunft?; iX 8/96, S. 84

[3] Barbara Liskov, John Guttag; Abstraction and Specification in Program Development; Cambridge, MA (MIT Press) 1986, S. 118 ff.

[4] Paul Hudak, Joseph H. Fasel; A Gentle Introduction to Haskell; ACM SIGPLAN Notes, Mai 1992, S. 22

[5] Aaron Watters, Guido van Rossum, James C. Ahlstrom; Internet Programming with Python. New York (M&T Books) 1996

Mehr Infos

iX-TRACT

  • Python ist eine objektorientierte Scriptsprache, die neben Perl und Tcl mehr und mehr Verwendung findet.
  • Guido van Rossum hat sich beim Design von Python auf Weniges beschränkt und vertraut auf die Erweiterbarkeit der Sprache.
  • Zwar hat Python viel von Lisp ‘geerbt’, aber die Sprache unterstützt neben funktionalem auch prozedurales Programmieren.

(hb)