Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
1 Einleitung
2 Überblick über Python
3 Die Arbeit mit Python
4 Der interaktive Modus
5 Grundlegendes zu Python-Programmen
6 Kontrollstrukturen
7 Das Laufzeitmodell
8 Basisdatentypen
9 Benutzerinteraktion und Dateizugriff
10 Funktionen
11 Modularisierung
12 Objektorientierung
13 Weitere Spracheigenschaften
14 Mathematik
15 Strings
16 Datum und Zeit
17 Schnittstelle zum Betriebssystem
18 Parallele Programmierung
19 Datenspeicherung
20 Netzwerkkommunikation
21 Debugging
22 Distribution von Python-Projekten
23 Optimierung
24 Grafische Benutzeroberflächen
25 Python als serverseitige Programmiersprache im WWW mit Django
26 Anbindung an andere Programmiersprachen
27 Insiderwissen
28 Zukunft von Python
A Anhang
Stichwort

Download:
- ZIP, ca. 4,8 MB
Buch bestellen
Ihre Meinung?

Spacer
 <<   zurück
Python von Peter Kaiser, Johannes Ernesti
Das umfassende Handbuch - Aktuell zu Python 2.5
Buch: Python

Python
gebunden, mit CD
819 S., 39,90 Euro
Galileo Computing
ISBN 978-3-8362-1110-9
Pfeil 12 Objektorientierung
  Pfeil 12.1 Klassen
    Pfeil 12.1.1 Definieren von Methoden
    Pfeil 12.1.2 Konstruktor, Destruktor und die Erzeugung von Attributen
    Pfeil 12.1.3 Private Member
    Pfeil 12.1.4 Versteckte Setter und Getter
    Pfeil 12.1.5 Statische Member
  Pfeil 12.2 Vererbung
    Pfeil 12.2.1 Mehrfachvererbung
  Pfeil 12.3 Magic Members
    Pfeil 12.3.1 Allgemeine Magic Members
    Pfeil 12.3.2 Datentypen emulieren
  Pfeil 12.4 Objektphilosophie

»Abstraction is selective ignorance« – Andrew Koenig

12 Objektorientierung

In diesem Kapitel wird endlich die Katze aus dem Sack gelassen: Sie werden in das wichtigste und umfassendste Konzept von Python eingeführt, die Objektorientierung. Der Begriff Objektorientierung beschreibt ein Programmierparadigma, das die Wiederverwendbarkeit von Quellcode steigert und es außerdem erleichtert, die Konsistenz von Datenobjekten zu sichern. Diese Vorteile werden dadurch erreicht, dass man Datenstrukturen und die dazugehörigen Operationen zu einem sogenannten Objekt zusammenfasst und den Zugriff auf diese Strukturen nur über bestimmte Schnittstellen erlaubt.

Diese Vorgehensweise werden wir an einem Beispiel veranschaulichen, indem wir zuerst auf dem bisherigen Weg eine Lösung erarbeiten und diese ein zweites Mal, diesmal aber objektorientiert, implementieren.

Stellen wir uns einmal vor, wir würden für eine Bank ein System für die Verwaltung von Konten entwickeln, das das Anlegen neuer Konten, Überweisungen sowie Ein- und Auszahlungen ermöglicht. Ein möglicher Ansatz sähe so aus, dass wir für jedes Bankkonto ein Dictionary anlegen, in dem dann alle Informationen über den Kunden und seinen Finanzstatus gespeichert sind. Um die gewünschten Operationen zu unterstützen, würden wir Funktionen definieren. Ein Dictionary für ein stark vereinfachtes Konto könnte folgendermaßen aussehen:

konto = { 
    "Inhaber" : "Hans Meier", 
    "Kontonummer" : 567123, 
    "Kontostand" : 12350.0, 
    "MaxTagesumsatz" : 1500, 
    "UmsatzHeute" : 10.0 
    }

Wir gehen modellhaft davon aus, dass jedes Konto einen "Inhaber" hat, der durch einen String mit seinem Namen identifiziert wird. Das Konto hat eine ganzzahlige "Kontonummer", um es von allen anderen Konten zu unterscheiden. Mit der Gleitkommazahl, die mit dem Schlüssel "Kontostand" verknüpft ist, wird das aktuelle Guthaben in Euro gespeichert. Die Schlüssel "MaxTagesumsatz" und "UmsatzHeute" dienen dazu, den Tagesumsatz eines jeden Kunden zu seinem eigenen Schutz auf ein bestimmtes Limit zu begrenzen. "MaxTagesumsatz" gibt dabei an, wie viel Geld pro Tag maximal von dem bzw. auf das Konto bewegt werden darf. Mit "UmsatzHeute" »merkt« sich das System, wie viel am heutigen Tag schon umgesetzt worden ist. Zu Beginn eines neuen Tages wird dieser Wert wieder auf null gesetzt. Die von uns betrachteten Konten sollen prinzipiell nicht überzogen werden können, der Kontostand bleibt also immer positiv.

Ausgehend von dieser Datenstruktur wollen wir nun die geforderten Operationen als Funktionen definieren. Als Erstes brauchen wir eine Funktion, die ein neues Konto nach bestimmten Vorgaben erzeugt:

def neues_konto(inhaber, kontonummer, kontostand, 
                max_tagesumsatz=1500): 
    return { 
        "Inhaber" : inhaber, 
        "Kontonummer" : kontonummer, 
        "Kontostand" : kontostand, 
        "MaxTagesumsatz" : max_tagesumsatz, 
        "UmsatzHeute" : 0 
        }

Da diese einfache Funktion selbsterklärend ist, wenden wir uns gleich den Überweisungen zu.

An einem Geldtransfer sind immer ein Sender (das Quellkonto) und ein Empfänger (das Zielkonto) beteiligt. Außerdem muss zum Durchführen der Überweisung der gewünschte Geldbetrag bekannt sein. Die Funktion wird also drei Parameter erwarten: quelle, ziel und betr. Nach unseren Voraussetzungen ist eine Überweisung nur dann möglich, wenn auf dem Quellkonto genug Geld vorhanden ist (es darf nicht überzogen werden) und die Tagesumsätze der beiden Konten ihr Limit nicht überschreiten. Die Überweisungsfunktion soll einen Wahrheitswert zurückgeben, der angibt, ob die Überweisung ausgeführt werden konnte oder nicht. Damit ließe sie sich folgendermaßen implementieren:

def geldtransfer(quelle, ziel, betr): 
    # Hier erfolgt der Test, ob der Transfer möglich ist 
    if(quelle["Kontostand"] < betr or 
       quelle["UmsatzHeute"] + betr > quelle["MaxTagesumsatz"] or 
       ziel["UmsatzHeute"] + betr > ziel["MaxTagesumsatz"]):
return False # Transfer unmöglich else: # Alles OK - Auf geht's
quelle["Kontostand"] -= betr quelle["UmsatzHeute"] += betr ziel["Kontostand"] += betr ziel["UmsatzHeute"] += betr return True

Die Funktion überprüft zuerst, ob der Transfer durchführbar ist, und beendet den Funktionsaufruf frühzeitig mit dem Rückgabewert False, falls dies nicht der Fall ist. Wenn genug Geld auf dem Quellkonto vorhanden ist und kein Tagesumsatzlimit überschritten wird, aktualisiert die Funktion Kontostände und Tagesumsätze entsprechend der Überweisung und gibt True zurück.

Die letzten Operationen für unsere Modellkonten sind das Ein- beziehungsweise Auszahlen am Geldautomaten oder Bankschalter. Beide Funktionen benötigen als Parameter das betreffende Konto und den jeweiligen Geldbetrag als Parameter. Da die Funktionen sehr einfach sind, möchten wir uns nicht weiter mit Erklärungen aufhalten, sondern direkt den Quellcode präsentieren:

def einzahlen(konto, betrag): 
    if konto["UmsatzHeute"] + betrag > konto["MaxTagesumsatz"]: 
        return False # Tageslimit überschritten 
    else: 
        konto["Kontostand"] += betrag 
        konto["UmsatzHeute"] += betrag 
        return True 
 
def auszahlen(konto, betrag): 
    if konto["UmsatzHeute"] + betrag > konto["MaxTagesumsatz"]: 
        return False # Tageslimit überschritten 
    else: 
        konto["Kontostand"] -= betrag 
        konto["UmsatzHeute"] += betrag 
        return True

Auch diese Funktionen geben abhängig von ihrem Erfolg einen Wahrheitswert zurück.

Um einen Überblick über den aktuellen Status unserer Konten zu erhalten, wollten wir eine einfache Ausgabefunktion definieren:

def zeige_konto(konto): 
    print "Konto von %s" % konto["Inhaber"] 
    print "Aktueller Kontostand: %.2f Euro" % konto["Kontostand"] 
    print "(Heute schon %.2f von %d umgesetzt)" % ( 
        konto["UmsatzHeute"], konto["MaxTagesumsatz"])

Mit diesen Definitionen könnten wir beispielsweise folgende Bankoperationen simulieren:

>>> k1 = neues_konto("Heinz Meier", 567123, 12350.0) 
>>> k2 = neues_konto("Erwin Schmidt", 396754, 15000.0) 
>>> geldtransfer(k1, k2, 160) 
True 
>>> geldtransfer(k2, k1, 1000) 
True 
>>> geldtransfer(k2, k1, 500) 
False 
>>> einzahlen(k2, 500) 
False 
>>> zeige_konto(k1) 
Konto von Heinz Meier 
Aktueller Kontostand: 13190.00 Euro 
(Heute schon 1160.00 von 1500 umgesetzt) 
>>> zeige_konto(k2) 
Konto von Erwin Schmidt 
Aktueller Kontostand: 14160.00 Euro 
(Heute schon 1160.00 von 1500 umgesetzt)

Zuerst eröffnet Heinz Meier ein neues Konto k1 mit der Kontonummer 567123 mit dem Startguthaben von 12350 Euro. Erwin Schmidt zahlt 15000 Euro auf sein neues Konto k2 mit der Kontonummer 396754 ein. Beide haben den standardmäßigen maximalen Tagesumsatz von 1500 Euro gewählt. Nun treten die beiden in geschäftlichen Kontakt miteinander, wobei Herr Schmid einen DVD-Recorder von Herrn Meier für 160 Euro kauft, der per Überweisung bezahlt wird. Am selben Tag erwirbt Herr Meier Herrn Schmidts gebrauchten Spitzenlaptop, der für 1000 Euro den Besitzer wechselt. Als Herr Meier in den Abendstunden stark an der Heimkinoanlage von Herrn Schmid interessiert ist und ihm dafür 500 Euro überweisen möchte, wird er enttäuscht, denn die Überweisung schlägt fehl. Völlig verdattert zieht Herr Schmidt den voreiligen Schluss, er habe zu wenig Geld auf seinem Konto. Deshalb möchte er den Betrag auf sein Konto einzahlen und anschließend erneut überweisen. Als aber auch die Einzahlung abgelehnt wird, wendet er sich an einen Bankangestellten. Dieser lässt sich die Informationen der beteiligten Konten anzeigen. Dabei sieht er, dass die gewünschte Überweisung das Tageslimit von Herrn Schmidts Konto überschreitet und deshalb nicht ausgeführt werden kann.

Wie Sie sehen, arbeitet unsere Banksimulation wie erwartet und ermöglicht uns eine relativ einfache Handhabung von Kontodaten. Sie weist aber einige unschöne Eigenheiten auf, wir im Folgenden besprechen werden.

In dem Beispiel sind die Datenstruktur und die Funktionen für ihre Verarbeitung getrennt definiert, was dazu führt, dass das Konto-Dictionary bei jedem Funktionsaufruf als Parameter übergeben werden muss. Man kann sich aber auf den Standpunkt stellen, dass ein Konto nur mit den dazugehörigen Verwaltungsfunktionen sinnvoll benutzt werden kann und auch umgekehrt die Verwaltungsfunktionen eines Kontos nur in Zusammenhang mit dem Konto nützlich sind. Außerdem könnte ein findiger Bankangestellter, der diese Funktionsbibliothek verwendet, ein darauf aufbauendes Programm so formulieren, dass er seinen Kontostand ein wenig aufbessert: Er kann einfach die Werte des Dictionarys direkt verändern, da er nicht an die vorgesehenen Funktionen gebunden ist. Diese direkte Möglichkeit, Daten zu verändern, kann auch die Funktionsweise des Programms beeinflussen, wenn den Eigenschaften des Kontos Werte von nicht sinnvollen Datentypen zugewiesen werden. Beispielsweise könnte dem Kontostand direkt eine Liste zugewiesen werden, was spätestens bei der nächsten Überweisung zu einem TypeError führen würde:

>>> k1 = neues_konto("Heinz Meier", 567123, 12350.0) 
>>> k2 = neues_konto("Erwin Schmidt", 396754, 15000.0) 
>>> k1["Kontostand"] = [3, "Hehe, das gibt einen tollen Fehler"] 
>>> geldtransfer(k1, k2, 160) 
Traceback (most recent call last): 
  […] 
TypeError: unsupported operand type(s) for -=: 'list' and 'int'

Wir wünschen uns also eine Möglichkeit, die eigentlichen Daten, also im Beispiel das Konto, mit den Verarbeitungsfunktionen zu einer Einheit zu koppeln und diese Verbindung vor direkten Zugriffen auf die enthaltenen Daten zu schützen, um ihre Konsistenz zu sichern.

Genau diese Wünsche befriedigt die Objektorientierung, indem sie Daten und Verarbeitungsfunktionen zu sogenannten Objekten zusammenfasst. Dabei werden die Daten eines solchen Objekts Attribute und die Verarbeitungsfunktionen Methoden genannt. Attribute und Methoden werden unter dem Begriff Member einer Klasse zusammengefasst. Schematisch ließe sich das Objekt eines Kontos also folgendermaßen darstellen:


Tabelle 12.1  Schema eines Konto-Objekts
Konto
Attribute Methoden

Inhaber

Kontostand

MaxTagesumsatz

UmsatzHeute

neues_konto()

geldtransfer()

einzahlen()

auszahlen()

zeige_konto()


Die Begriffe »Attribut« und »Methode« sind Ihnen bereits aus früheren Kapiteln von den Basisdatentypen bekannt, denn jede Instanz eines Basisdatentyps stellt – auch wenn Sie es zu dem Zeitpunkt vielleicht noch nicht wussten – ein Objekt dar. Sie wissen auch schon, dass auf die Attribute und Methoden eines Objekts zugegriffen wird, indem man die Referenz auf das Objekt und das dazugehörige Member durch einen Punkt getrennt aufschreibt.

Angenommen, k1 und k2 seien Konto-Objekte, wie sie das obige Schema zeigt, mit den Daten von Herrn Meier und Herrn Schmidt, dann könnte man das letzte Beispiel folgendermaßen formulieren (der Code ist so natürlich noch nicht lauffähig, da die Definition für die Konto-Objekte fehlt):

>>> k1.geldtransfer(k2, 160) 
True 
>>> k2.geldtransfer(k1, 1000) 
True 
>>> k2.geldtransfer(k1, 500) 
False 
>>> k2.einzahlen(500) 
False 
>>> k1.zeige_konto() 
Konto von Heinz Meier 
Aktueller Kontostand: 13190.00 Euro 
(Heute schon 1160.00 von 1500 umgesetzt) 
>>> k2.zeige_konto() 
Konto von Erwin Schmidt 
Aktueller Kontostand: 14160.00 Euro 
(Heute schon 1160.00 von 1500 umgesetzt)

Die Methoden geldtransfer und zeige_konto haben nun beim Aufruf einen Parameter weniger, da das Konto, auf das sie sich jeweils beziehen, nun am Anfang des Aufrufs steht. Da Sie seit der Einführung der Basisdatentypen bereits mit dem Umgang mit Objekten vertraut sind, wird für Sie in diesem Kapitel nur die Technik wirklich neu sein, wie Sie Ihre eigenen Objekte mithilfe von Klassen definieren können.


Galileo Computing - Zum Seitenanfang

12.1 Klassen  Zur nächsten ÜberschriftZur vorigen Überschrift

Objekte werden über sogenannte Klassen definiert. Eine Klasse ist dabei einfach eine formale Beschreibung, wie bestimmte Objekte auszusehen haben, also welche Attribute und Methoden sie besitzen.

Mit einer Klasse allein kann man noch nicht sinnvoll arbeiten, da sie wirklich nur die Beschreibung von Objekten darstellt, selbst aber kein Objekt ist. Man kann das Verhältnis von Klasse und Objekt mit dem von Backrezept und Kuchen vergleichen: Das Rezept definiert die Zutaten und den Herstellungsprozess eines Kuchens und damit auch seine Eigenschaften. Trotzdem reicht ein Rezept allein nicht aus, um die Verwandten zu einer leckeren Torte am Sonntagnachmittag einzuladen. Erst beim Backen wird aus der abstrakten Beschreibung ein fertiger Kuchen.

Ein anderer Name für ein Objekt ist Instanz. Das objektorientierte Backen wird daher Instanziieren genannt. So, wie es zu einem Rezept mehrere Kuchen geben kann, so können auch mehrere Instanzen von einer Klasse erzeugt werden:

Abbildung 12.1  Analogie von Rezept/Kuchen und Klasse/Objekt

Zur Definition einer neuen Klasse in Python dient das Schlüsselwort class, dem der Name der neuen Klasse folgt. Die einfachste Klasse hat weder Methoden noch Attribute und wird folgendermaßen definiert:

class Konto(object): 
    pass

Lassen sie sich an dieser Stelle nicht von dem (object) hinter dem Klassennamen irritieren. Schreiben Sie es einfach immer wie oben gezeigt in Ihre Klassendefinitionen, bis Sie die Hintergründe dafür in Abschnitt 12.2, »Vererbung«, erfahren.

Wie bereits gesagt wurde, lässt sich mit einer Klasse allein nicht arbeiten, weil sie nur eine abstrakte Beschreibung ist. Deshalb wollen wir nun eine Instanz der noch leeren Beispielklasse Konto erzeugen. Um eine Klasse zu instanziieren, ruft man die Klasse wie eine Funktion ohne Parameter auf, indem man dem Klassennamen ein rundes Klammernpaar nachstellt. Der Rückgabewert dieses Aufrufs ist eine neue Instanz der Klasse:

>>> Konto() 
<__main__.Konto instance at 0x00BA75A8>

Die schwer lesbare Ausgabe soll uns mitteilen, dass der Rückgabewert von Konto() eine Instanz der Klasse Konto im Hauptnamensraum __main__ ist und im Speicher unter der Adresse 0x00BA75A8 abgelegt wurde – uns reicht als Information aus, dass eine neue Instanz der Klasse Konto erzeugt worden ist.

Nun ist dieses Konto-Objekt weit davon entfernt, unseren Anforderungen vom Anfang des Kapitels zu genügen, und ist somit bis jetzt der bisherigen Dictionary-Implementation unterlegen. Wir werden vor der Erzeugung von neuen Konten erst die Definition von Methoden behandeln.


Galileo Computing - Zum Seitenanfang

12.1.1 Definieren von Methoden  Zur nächsten ÜberschriftZur vorigen Überschrift

Im Prinzip unterscheidet sich eine Methode nur durch zwei Aspekte von einer normalen Funktion: Erstens wird sie innerhalb eines von class eingeleiteten Blocks definiert, und zweitens erhält sie als ersten Parameter immer eine Referenz auf die Instanz, über die sie aufgerufen wird. Dieser erste Parameter muss nur bei der Definition explizit hingeschrieben werden und wird beim Aufruf der Methode automatisch mit der entsprechenden Instanz verknüpft. Da sich die Referenz auf das Objekt selbst bezieht, gibt man dem ersten Parameter den Namen self (dt. selbst). Methoden besitzen genau wie Funktionen einen eigenen Namensraum, können auf globale Variablen zugreifen und Werte per return an die aufrufende Ebene zurückgeben.

Damit können wir unsere Kontoklasse um die noch fehlenden Methoden ergänzen, wobei wir zunächst nur die Methodenköpfe ohne den enthaltenen Code aufschreiben, da wir noch nicht wissen, wie man mit Attributen eigener Klassen umgeht:

class Konto(object): 
    def geldtransfer(self, ziel, betrag): 
        pass 
 
    def einzahlen(self, betrag): 
        pass 
 
    def auszahlen(self, betrag): 
        pass 
 
    def zeige_konto(self): 
        pass

Beachten Sie den self-Parameter am Anfang jeder Methode, für den automatisch eine Referenz auf die Instanz übergeben wird, die beim Aufruf auf der linken Seite des Punktes steht:

>>> k = Konto() 
>>> k.einzahlen(500)

Hier wird an die Methode einzahlen eine Referenz auf das Konto k übergeben, auf das dann innerhalb von einzahlen über den Parameter self zugegriffen werden kann.

Im nächsten Abschnitt werden Sie dann lernen, wie Sie auch die Erzeugung neuer Objekte nach Ihren Vorstellungen anpassen können und wie man neue Attribute anlegt.


Galileo Computing - Zum Seitenanfang

12.1.2 Konstruktor, Destruktor und die Erzeugung von Attributen  Zur nächsten ÜberschriftZur vorigen Überschrift

Der Lebenszyklus jeder Instanz sieht gleich aus: Sie wird erzeugt, benutzt und anschließend wieder beseitigt. Da es eines der Hauptziele der Objektorientierung war, die Daten eines Objekts vor direktem Zugriff von außen zu schützen, können wir einem Objekt nicht beim Erzeugen seinen Anfangswert direkt zuweisen. Stattdessen geschieht diese Zuweisung mittels einer speziellen Methode, die automatisch beim Instanziieren eines Objekts aufgerufen wird. Man nennt diese Methode auch Konstruktor (engl. construct = »errichten«) einer Klasse. Pythons Konstruktoren haben alle den Namen __init__ und werden genau wie jede andere Methode definiert:

class Beispielklasse(object): 
    def __init__(self): 
         print "Hier spricht der Konstruktor"

Wenn wir jetzt wie gehabt eine Instanz der Klasse Beispielklasse erzeugen, wird implizit die __init__-Methode aufgerufen, und der Text »Hier spricht der Konstruktor« erscheint auf dem Bildschirm:

>>> Beispielklasse() 
Hier spricht der Konstruktor 
<__main__.Konto instance at 0x00BA3670>

Konstruktoren können sinnvollerweise keine Rückgabewerte haben, da sie nicht direkt aufgerufen werden und beim Erstellen einer neuen Instanz schon eine Referenz auf diese zurückgegeben wird.

Dem Konstruktor steht der sogenannte Destruktor (engl. destruct = »zerstören«) gegenüber, der immer dann aufgerufen wird, wenn eine Instanz von der Garbage Collection aus dem Speicher entfernt wird. Ein Destruktor ist eine bis auf self parameterlose Methode, die auf den Namen __del__ hört:

class Beispielklasse(object): 
    def __init__(self): 
        print "Hier spricht der Konstruktor" 
 
    def __del__(self): 
        print "Und hier kommt der Destruktor"

Das folgende Beispiel zeigt, dass der Destruktor beim Entfernen der Instanz mit dem del-Statement aufgerufen wird:

>>> obj = Beispielklasse() 
Hier spricht der Konstruktor 
>>> del obj 
Und hier kommt der Destruktor

Dieses Verhalten und der Umstand, dass der Destruktor sehr ähnlich heißt wie das del-Statement, führen oft zu der falschen Annahme, dass der Destruktor bei jedem del-Statement aufgerufen würde. Dies ist aber nur dann der Fall, wenn die letzte Referenz auf ein Objekt mit del entfernt wurde, da erst dann die Garbage Collection aktiv wird, wie es das folgende Beispiel zeigt:

>>> v1 = Beispielklasse() 
Hier spricht der Konstruktor 
>>> v2 = v1 
>>> del v1 
>>> del v2 
Und hier kommt der Destruktor

Wie Sie sehen, wurde __del__ einmalig nach dem zweiten del-Statement aufgerufen und nicht zweimal. Dies wird auch dann noch einmal klar, wenn man sich vor Augen hält, dass ein Objekt zum Entfernen erst einmal erzeugt werden muss: Für einen Konstruktor-Aufruf gibt es genau einen Destruktor-Aufruf desselben Objekts.

Im Gegensatz zu Konstruktoren werden Destruktoren relativ selten benutzt, was daran liegt, das Python schon von sich aus einen Großteil der »Drecksarbeit« erledigt und man sich in der Regel nicht um das Aufräumen im Speicher kümmern muss. Destruktoren werden aber häufig benötigt, um beispielsweise bestehende Netzwerkverbindungen sauber zu trennen, den Programmablauf zu dokumentieren oder Fehler zu finden.

Neue Attribute anlegen

Da es die Hauptaufgabe eines Konstruktors ist, einen konsistenten Initialzustand einer Instanz herzustellen und sie damit in einen benutzbaren Zustand zu versetzen, sollten alle Attribute einer Klasse auch dort definiert werden. [Es gibt sehr wenige Sonderfälle, in denen diese Regel eine unpraktische Einschränkung ist. Deshalb muss man nicht zwingend alle Attribute in der __init__-Methode definieren. Sie sollten aber im Regelfall, soweit es möglich ist, alle Attribute Ihrer Klassen im Konstruktor erstellen. ] Die Definition neuer Attribute erfolgt durch eine einfache Wertezuweisung, wie Sie sie von normalen Variablen her kennen. Damit können wir die Funktion neues_konto durch den Konstruktor der Klasse Konto ersetzen, der dann wie folgt implementiert werden kann. Für den Parameter self wird dabei beim Aufruf automatisch eine Referenz auf die neu erzeugte Konto-Instanz übergeben:

class Konto(object): 
    def __init__(self, inhaber, kontonummer, kontostand, 
                       max_tagesumsatz=1500): 
        self.Inhaber = inhaber 
        self.Kontonummer = kontonummer 
        self.Kontostand = kontostand 
        self.MaxTagesumsatz = max_tagesumsatz 
        self.UmsatzHeute = 0 
 
    # hier kommen die restlichen Methoden hin

Da self eine Referenz auf die zu erstellende Instanz enthält, können wir über sie die neuen Attribute anlegen, wie in dem Beispiel gezeigt wird. Auf dieser Basis können auch die anderen Funktionen der nicht objektorientierten Variante auf die Kontoklasse übertragen werden. Wir werden uns hier aus Platzgründen auf die Methode geldtransfer beschränken. Es sollte dann kein Problem mehr für Sie darstellen, auch die anderen Methoden zu implementieren.

class Konto(object): 
    # hier kommt der Konstruktor hin 
 
    def geldtransfer(self, ziel, betrag): 
        # Hier erfolgt der Test, ob der Transfer möglich ist 
        if(self.Kontostand < betrag or 
           self.UmsatzHeute + betrag > self.MaxTagesumsatz or 
           ziel.UmsatzHeute + betrag > ziel.MaxTagesumsatz): 
            return False # Transfer unmöglich 
        else: 
            # Alles OK - Auf geht's
self.Kontostand -= betrag self.UmsatzHeute += betrag ziel.Kontostand += betrag ziel.UmsatzHeute += betrag return True
# hier wären die restlichen Methoden

Bis zu dieser Stelle haben wir unser erstes großes Ziel erreicht, die Kontodaten und die dazugehörigen Verarbeitungsfunktionen zu einer Einheit zu verbinden. Allerdings ist es immer noch möglich, außerhalb der Klasse auf die Attribute direkt zuzugreifen und diese zu verändern, und folgender Code würde Hotzenplotz immer noch unrechtmäßig bereichern:

>>> k = Konto("Hotzenplotz", 321987, 10000.0) 
>>> k.Kontostand = 500000.0 
>>> k.Kontostand 
500000.0

Auch die Zuweisung von Werten ungültiger Datentypen wird noch nicht verhindert. Erst mithilfe der privaten Member, die im nächsten Abschnitt beschrieben werden, erreichen wir eine Lösung, die auch die Konsistenz unserer Objekte sichert.


Galileo Computing - Zum Seitenanfang

12.1.3 Private Member  Zur nächsten ÜberschriftZur vorigen Überschrift

Attribute und Methoden von Klassen, die von außen nicht sichtbar sein sollen, weil sie bei falscher Verwendung die Konsistenz von Objekten beeinträchtigen, können so gekennzeichnet werden, dass nur die Klasse selbst darauf zugreifen kann. Die Manipulation der Objekte erfolgt ausschließlich über die von außen sichtbaren und dadurch dafür vorgesehenen Methoden und Attribute. Die für die Verwendung von außen bestimmten Methoden und Attribute werden auch als Schnittstelle der Klasse (engl. Interface) bezeichnet.

Für das Benutzerprogramm, das eine Klasse einsetzt, ist nur die Definition der Schnittstelle von Bedeutung. Was hinter den Kulissen, also im Innern der Objekte wirklich passiert, ist dabei vollkommen unerheblich, solange sich die Klasse nach außen hin gemäß der Schnittstelle verhält.

Unsere Kontoklasse könnte also beispielsweise bei jeder größeren Bareinzahlung automatisch eine Benachrichtigung an die Bankdirektion verschicken, dass höchstwahrscheinlich nicht rechtmäßig erworbenes Geld eingezahlt wurde. Das würde uns als Benutzer der Klasse so lange nicht interessieren, wie die Methode einzahlen auch den Kontostand korrekt anpassen und abhängig vom Erfolg der Einzahlung True oder False zurückgeben würde.

Um definierte Schnittstellen zu implementieren, müssen wir eine Möglichkeit haben, Member explizit als öffentlich, also als Teil der Schnittstelle, oder als privat, also als Implementationsdetail, zu deklarieren.

Im Gegensatz zu vielen anderen Programmiersprachen, die dieses Konzept mit eigenen Schlüsselwörtern implementieren, legt in Python der Name eines Attributs fest, ob es von außen explizit verwendet werden soll oder nicht. Dabei gibt es drei Kategorien: [Wenn man es ganz genau nimmt, sind auch diese Member nicht wirklich gegen Zugriffe von außen geschützt: Sie werden intern von Python durch Namen des Schemas _Klassenname_Attributname ersetzt, und deshalb führen Versuche, von außen auf die ursprünglichen Namen zuzugreifen, zu Fehlern. Über den geänderten Namen kann aber weiterhin von überall aus auf die Attribute zugegriffen werden. ]


Tabelle 12.2  Namensschemata für öffentliche, private und geschützte Member
Namensschema Bezeichnung Bedeutung

name

Public

(Öffentlich)

Normale Member ohne führende Unterstriche sind sowohl innerhalb einer Klasse also auch von außen les- und schreibbar.

_name

Protected

(Geschützt)

Auf Member, deren Namen mit einem Unterstrich beginnt, kann zwar sowohl von innen als auch von außen lesend und schreibend zugegriffen werden, aber der Entwickler einer Klasse teilt den anderen Programmierern dadurch mit, dass dieses Member nicht direkt benutzt werden sollte.

__name

Private

(Privat)

Namen mit zwei führenden Unterstrichen sind für wirklich private Member gedacht, die von außen nicht sichtbar sind und deshalb nur über Methoden der Klasse verändert und ausgelesen werden können.2


Protected Members sind weiterhin nach außen sichtbar und voll veränderbar. Sie sind nur nach einer Konvention geschützt, die es allen Programmierern empfiehlt, solche Attribute von außen nicht zu benutzen. Es handelt sich hierbei um eine Schnittstellendefinition, die nicht durch eine technische Lese- bzw. Schreibsperre erreicht wird, sondern auf einer Konvention zwischen allen Python-Programmierern beruht: Member, die mit einem Unterstrich beginnen, sollen von außen nicht benutzt werden. Wer es trotzdem tut, sollte sich darüber im Klaren sein, dass dies zu nicht beabsichtigtem Verhalten führen kann. Der Vorteil einer solchen Privatisierung durch eine Abmachung besteht gegenüber der technischen Sperre darin, dass immer noch auf die Member zugegriffen werden kann, wenn dies unbedingt erforderlich sein sollte. Dies erleichtert beispielsweise das Entwickeln von Debuggern zur Fehlersuche in Programmen oder Analysetools enorm.

Wenn Sie einem Membernamen zwei Unterstriche voranstellen, so verändern sich die Zugriffsbestimmungen auf technischer Ebene – er wird zu einem Private Member. In unserem Kontobeispiel soll insbesondere der Kontostand nicht mehr von außen direkt verändert werden können, sondern nur über die dazu vorgesehenen Methoden. Deshalb benennen wir das Attribut Kontostand um in __Kontostand um, womit es nach außen hin geschützt wird. Da auch die anderen Attribute nur noch über die Verarbeitungsroutinen mit neuen Werten versehen werden sollen, werden sie ebenfalls als private deklariert:

class Konto(object): 
     def __init__(self, inhaber, kontonummer, kontostand, 
                        max_tagesumsatz=1500): 
        self.__Inhaber = inhaber 
        self.__Kontonummer = kontonummer 
        self.__Kontostand = kontostand 
        self.__MaxTagesumsatz = max_tagesumsatz 
        self.__UmsatzHeute = 0 
 
    # hier wären die restlichen Methoden

Nun führen alle Zugriffe von außen auf diese Member zu einem AttributeError:

>>> k = Konto("Hotzenplotz", 321987, 10000.0) 
>>> k.__Kontostand 
Traceback (most recent call last): 
  File "<pyshell#2>", line 1, in <module> 
    k.__Kontostand 
AttributeError: Konto instance has no attribute '__Kontostand'

Es aber so, dass wir gar nichts dagegen haben, dass jemand den Kontostand ausliest, der Kontostand soll nur nicht von außen direkt verändert werden können. Abhilfe schaffen sogenannte Getter-Methoden, deren einfache Aufgabe es ist, die Werte privater Attribute zurückzugeben. Das folgende Beispiel definiert eine Methode kontostand, die den Wert des privaten Attributs __Kontostand zurückgibt. Das ist möglich, weil kontostand als Methode von Konto auf dessen Attribute, egal ob privat oder nicht, zugreifen darf:

class Konto(object): 
    # hier wäre der Konstruktor 
 
    def kontostand(self): 
        return self.__Kontostand 
 
    # hier wären die restlichen Methoden

Durch diese einfache Maßnahme ist nun unser Ziel erreicht, dass der Kontostand zwar gegen unzulässige Schreibzugriffe geschützt ist, aber trotzdem noch von außen gelesen werden kann. Folgendes Beispiel verdeutlicht noch einmal das Ergebnis:

>>> k = Konto("Hotzenplotz", 321987, 10000.0) 
>>> k.kontostand() 
10000.0 
>>> k.__Kontostand = 99999999.0 
>>> k.kontostand() 
10000.0

Zwar führt der Versuch, den Kontostand von außen zu erhöhen, zu keinem Fehler, aber der Rückgabewert von kontostand nach der vermeintlichen Zuweisung zeigt, dass sich der Wert des Attributs nicht verändert hat.

Das Konzept der Getter-Methoden zum Auslesen von versteckten Attributen wird durch sogenannte Setter-Methoden ergänzt, die die genauen Gegenspieler der Getter sind. Mit ihnen lässt sich eine Schnittstelle definieren, die Werte von außen zu manipulieren, wobei die Setter-Methode dafür Sorge tragen sollte, dass keine ungültigen Werte gesetzt werden. Würde Herr Schmidt aufgrund seiner Probleme beim Bezahlen sein Tageslimit für die Zukunft erhöhen wollen, so müsste ein Bankangestellter das private Attribut __MaxTagesumsatz verändern können, was mit der aktuellen Konto-Klasse nicht möglich ist. Zu diesem Zwecke könnte man eine Setter-Methode setMaxTagesumsatz definieren, die als einzigen Parameter neben self den gewünschten neuen Tagesumsatz neues_limit erhält. Bevor nun das neue Tageslimit gesetzt werden kann, wird der übergebene Wert auf Gültigkeit geprüft – ein Tageslimit muss eine positive Ganz- oder Gleitkommazahl und größer als 0 sein:

class Konto(object): 
    # hier wäre der Konstruktor 
 
    # Getter-Methode für das Tageslimit 
    def maxTagesumsatz(self): 
        return self.__MaxTagesumsatz 
 
    # Setter-Methode für das Tageslimit 
    def setMaxTagesumsatz(self, neues_limit): 
        if(type(neues_limit) in (float, int) and 
           neues_limit > 0): 
            self.__MaxTagesumsatz = neues_limit 
            return True 
        else: 
            return False 
 
    # hier wären die restlichen Methoden

Das Methoden-Paar maxTagesumsatz und setMaxTagesumsatz ermöglicht nun den komfortablen und trotzdem sicheren Zugriff auf den maximalen Tagesumsatz, indem sichergestellt wird, dass nur gültige Werte gespeichert werden. Die Setter-Methode prüft, ob der Datentyp von neues_limit entweder float oder int ist und ob sein Wert im gültigen Bereich liegt, und setzt abhängig vom Ausgang dieser Prüfung das Attribut __MaxTagesumsatz auf den neuen Wert oder eben nicht. Anhand des Rückgabewertes der Funktion kann der Bankangestellte dann sehen, ob er einen Fehler bei der Übergabe gemacht hat. [Eine elegantere Methode, die aufrufende Ebene auf solche Fehler hinzuweisen, lernen Sie in Abschnitt 13.1, »Exception Handling«, kennen. Sie könnten dann beispielsweise bei ungültigen Werten einen ValueError produzieren. ]


Galileo Computing - Zum Seitenanfang

12.1.4 Versteckte Setter und Getter  Zur nächsten ÜberschriftZur vorigen Überschrift

Das im letzten Abschnitt angesprochene Konzept, mithilfe von Setter- und Getter-Methoden das Lesen und Schreiben von Attributen anzupassen, hat den oft als negativ empfundenen Nebeneffekt, dass man beim Benutzen von Attributen auf Methoden zurückgreifen muss. Viel schöner wäre es, wenn man von außen weiterhin Attribute »sehen« und benutzen könnte, die Klasse aber intern die Werte auf Gültigkeit prüfen und so die Konsistenz der Objekte sichern könnte. Schauen Sie sich einmal die beiden gleichwertigen, ohne die dazugehörigen Definitionen natürlich noch nicht funktionierenden Beispiele an:

>>> k = Konto("Hotzenplotz", 321987, 10000.0) 
>>> k.kontostand() 
10000.0 
>>> k.setMaxTagesumsatz(2000)

Dieses Beispiel nutzt den bekannten Getter/Setter-Ansatz und liest sich schlechter als das folgende Beispiel, weil syntaktisch die Zugriffe auf Attribute durch Methoden verschleiert werden:

>>> k = Konto("Hotzenplotz", 321987, 10000.0) 
>>> k.Kontostand 
10000.0 
>>> k.MaxTagesumsatz = 2000

In Python wird dieser Wunsch durch die Möglichkeit befriedigt, beim Lesen und Schreiben von Attributen implizit Methoden aufzurufen, die sich um den Ablauf kümmern. Solche sogenannten Managed Attributes (dt. verwaltete Attribute) werden durch Instanzen des Datentyps property unterstützt. Der Konstruktor von property erwartet vier optionale Parameter:

property([fget[, fset[, fdel[, doc]]]])

Der Parameter fget erwartet eine Referenz auf eine Getter-Methode für das neue Attribut, und fset eine Referenz auf die dazugehörige Setter-Methode. Mit dem Parameter fdel kann zusätzlich eine Methode angegeben werden, die dann ausgeführt werden soll, wenn das Attribut per del gelöscht wird. Mit dem Parameter doc kann das Managed Attribute mit einem sogenannten Docstring versehen werden. Was ein Docstring ist, können Sie in Abschnitt 13.3 nachlesen und wird an dieser Stelle nicht weiter behandelt.

Wir werden beispielhaft das Attribut MaxTagesumsatz als property implementieren. Alle property-Attribute einer Klasse werden außerhalb jeder Methode direkt auf der ersten Einrückebene innerhalb des class-Blocks definiert, indem man dem gewünschten Namen des Attributs den Rückgabewert von property zuweist. Im Falle unseres Kontos würde MaxTagesumsatz auf folgende Weise zum Managed Attribute:

class Konto(object): 
    # hier wäre der Konstruktor 
 
    # Getter-Methode für das Tageslimit 
    def maxTagesumsatz(self): 
        print "Getter wurde gerufen" 
        return self.__MaxTagesumsatz 
 
    # Setter-Methode für das Tageslimit 
    def setMaxTagesumsatz(self, neues_limit): 
        if(type(neues_limit) in (float, int) and 
           neues_limit > 0): 
            print "Setter wurde mit %s aufgerufen" % neues_limit 
            self.__MaxTagesumsatz = neues_limit 
        else: 
            print "Fehlerhafter Setter-Parameter:", neues_limit 
 
    # folgende Zeile erzeugt das Property-Attribut 
    MaxTagesumsatz = property(maxTagesumsatz, setMaxTagesumsatz) 
 
    # hier wären die restlichen Methoden

Die print-Anweisungen dienen nur dazu, dass wir in unserem Beispiel gleich sehen können, dass die Methoden auch wirklich aufgerufen werden. Außerdem wurden die Rückgabewerte von setMaxTagesumsatz entfernt, da diese die aufrufende Ebene nicht mehr erreichen können und somit sinnlos geworden sind. [Um Fehler zu signalisieren, sollte der Setter Exceptions werfen. Wie das geht, lernen Sie später. ]

Nun können wir das neue Attribut wie ein gewöhnliches benutzen, und trotzdem haben wir durch die impliziten Methodenaufrufe volle Kontrolle über seine Werte:

>>> k = Konto("Hotzenplotz", 321987, 10000.0) 
>>> k.MaxTagesumsatz 
Getter wurde aufgerufen 
1500 
>>> k.MaxTagesumsatz = 9999.0 
Setter wurde mit 9999.0 aufgerufen 
>>> k.MaxTagesumsatz 
Getter wurde aufgerufen 
9999.0 
>>> k.MaxTagesumsatz = ("Fehlerhafter Wert", "Hehe") 
Fehlerhafter Setter-Parameter: ('Fehlerhafter Wert', 'Hehe') 
>>> k.MaxTagesumsatz 
Getter wurde aufgerufen 
9999.0

Das Beispiel demonstriert die Funktion des property-Attributs, und durch die Ausgaben lässt sich sehr schön verfolgen, wann die Setter bzw. Getter aufgerufen werden.


Galileo Computing - Zum Seitenanfang

12.1.5 Statische Member  topZur vorigen Überschrift

Bisher war es so, dass die Klasse den Bauplan für ihre Instanzen definierte und nur benutzt wurde, um Instanzen zu erzeugen. Während des Programmlaufs drehte sich die eigentliche Arbeit nur um die Instanzen, während die Klassen selbst in den Hintergrund traten. Insbesondere hatte jedes Objekt seine eigenen Attribute und seine eigenen Methoden, die von denen der anderen Objekte unabhängig waren. Das ist auch sinnvoll, denn schließlich hat jedes Konto seine eigene Kontonummer, und diese soll auch unabhängig von allen anderen Konten gespeichert werden.

Diese Art von Membern wird nicht-statisch genannt, weil sie für jedes Objekt einer Klasse dynamisch neu erstellt werden. Demgegenüber stehen die sogenannten statischen Membern, die sich alle Instanzen einer Klasse teilen.

Angenommen, wir wollten zählen, wie viele Konten unsere Bank gerade besitzt, dann könnten wir dies erreichen, indem wir die Instanzen der Klasse Konto zählen. Eine Möglichkeit wäre, einen globalen Zähler bei jedem Konstruktoraufruf von Konto um eins zu erhöhen und bei jedem Aufruf von __del__ wieder und eins zu verringern. Dieser Ansatz würde allerdings das Kapselungsprinzip verletzen, da wir direkt von einer tieferen Ebene auf globale Daten zugreifen würden. Da dies die Gefahr unerwünschter Seiteneffekte bietet, ist es als schlechter Stil verpönt. Eine wesentlich elegantere Lösung bestünde darin, der Klasse Konto einen internen Zähler ihrer eigenen Instanzen als statisches Attribut zu geben. Dieser würde dann bei den entsprechenden Konstruktor- und Destruktoraufrufen herauf- bzw. heruntergezählt.

Statische Attribute werden im Gegensatz zu nicht-statischen Attributen außerhalb des Konstruktors definiert, indem sie wie property-Attribute direkt in dem class-Block durch Zuweisung mit einem Anfangswert versehen werden. Es hat sich eingebürgert, dass dies in der Regel direkt unterhalb der class-Anweisung noch vor der Konstruktordefinition erfolgt. Im Falle unseres Instanzenzählers – wir nennen ihn Anzahl – sieht das wie folgt aus:

class Konto(object): 
    Anzahl = 0 # Zu Beginn ist die Instanzanzahl 0 
 
    # hier wäre der Konstruktor 
    # hier wären die restlichen Methoden

Damit besitzt die Klasse Konto ein statisches Attribut Anzahl, das sich alle ihre Instanzen teilen. Damit Anzahl auch wirklich die Instanzen zählt, passen wir den Konstruktor an und erstellen einen Destruktor. Der Zugriff auf statische Member erfolgt etwas anders als der auf nicht-statische, da beim Verändern der Werte statt des self eine Referenz auf die Klasse (in diesem Fall Konto) vor dem Punkt stehen muss. Weil sich statische Attribute immer auf die jeweiligen Klassen beziehen – der Zugriff mithilfe des Klassennamens macht es noch einmal deutlich –, werden statische Member auch Klassen-Member (engl. class members) genannt.

class Konto(object): 
    Anzahl = 0 # Zu Beginn ist die Instanzanzahl 0 
 
    def __init__(self, inhaber, kontonummer, kontostand, 
                       max_tagesumsatz=1500): 
        self.__Inhaber = inhaber 
        self.__Kontonummer = kontonummer 
        self.__Kontostand = kontostand 
        self.__MaxTagesumsatz = max_tagesumsatz 
        self.__UmsatzHeute = 0 
        Konto.Anzahl += 1 # Instanzzähler erhöhen 
 
    def __del__(self): 
        Konto.Anzahl -= 1 
 
    # hier wären die restlichen Methoden

Zur Demonstration der Funktion des statischen Members folgt jetzt ein kleines Beispiel:

>>> k1 = Konto("Florian Kroll", 3111987, 50000.0) 
>>> k2 = Konto("Lucas Hövelmann", 25031988, 43000.0) 
>>> k3 = Konto("Sebastian Sentner", 6091987, 44000.0) 
>>> Konto.Anzahl 
3 
>>> k1.Anzahl 
3 
>>> del k2 
>>> Konto.Anzahl 
2 
>>> del k1 
>>> k3.Anzahl 
1 
>>> del k1 
>>> del k3 
>>> Konto.Anzahl 
0

Erst werden drei neue Konto-Instanzen erzeugt, und wie die Ausgabe zeigt, enthält das statische Attribut Anzahl die korrekte Anzahl. Dann werden die Referenzen nacheinander wieder freigegeben, was zur Folge hat, dass die Instanzen von der Garbage Collection entsorgt werden. Die Werte von Anzahl spiegeln dies wider. Außerdem zeigt der Zugriff auf Anzahl über die Klasse Konto direkt als Konto.Anzahl und indirekt über die Instanzen k1 und k2 als k1.Anzahl bzw k2.Anzahl, dass der Wert wirklich von allen Instanzen geteilt wird.

Wie der Zugriff mit Konto.Anzahl verdeutlicht, ist es auch dann möglich, auf statische Member einer Klasse zuzugreifen, wenn es gar keine Instanzen der Klasse gibt.

Neben statischen Attributen gibt es in Python auch statische Methoden, die allerdings kaum genutzt werden und eine untergeordnete Rolle spielen. Da sich statische Methoden nicht auf einzelne Instanzen beziehen, erwarten sie keinen self-Parameter, was aber auch dazu führt, dass sie keinen Zugriff auf die Attribute und Methoden der Instanzen haben. Ihre Definition erfolgt ähnlich wie die von property-Attributen, nur dass anstelle von property die Built-in Function staticmethod verwendet wird:

class Konto(object): 
    Anzahl = 0 # Zu Beginn ist die Instanzanzahl 0 
 
    def zeigeAnzahl(): 
        print "Die Instanzanzahl ist", Konto.Anzahl 
 
    zeigeAnzahl = staticmethod(zeigeAnzahl) 
 
    # Die restlichen Member wären hier

Statische Methoden können auch aufgerufen werden, wenn es noch gar keine Instanz der Klasse gibt:

>>> Konto.zeigeAnzahl() 
Die Instanzanzahl ist 0


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






 <<   zurück
  
  Zum Katalog
Zum Katalog: Python






Python
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Linux






 Linux


Zum Katalog: Ubuntu GNU/Linux






 Ubuntu GNU/Linux


Zum Katalog: Praxisbuch Web 2.0






 Praxisbuch Web 2.0


Zum Katalog: UML 2.0






 UML 2.0


Zum Katalog: Praxisbuch Objektorientierung






 Praxisbuch Objektorientierung


Zum Katalog: Einstieg in SQL






 Einstieg in SQL


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für Fachinformatiker


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de