von Carlos Maltzahn
Paos ist ein System zur netzwerkweiten und konsistenten Verwaltung von Python-Objekten. Carlos führt uns in der heutigen Folge der Python Tools in die Hintergründe dieses interessanten Werkzeugs ein. Als größere Anwendung wird zudem das damit entwickelte Workflow-Managment System Chautauqua vorgestellt und belegt die Einsatmöglichkeiten von Python in realen Anwendungen.
Der Python Modul shelve unterstützt das Abspeichern von Python Objekten in eine Datei. Nach dem Öffnen eines shelve files können beliebige Objekte mit einem Namen eingetragen werden:
>>> importWenn nun diese Interpreter-Sitzung beendet wird, bleiben die so eingetragenen Objekte erhalten. Beim nächsten Aufruf von Python können diese Objekte mittelsshelve>>> db =shelve.open('database') >>> db['first object'] = [1, 2, 3] >>> db['second object'] = ('hallo', []) >>> db['first object'] [1, 2, 3] >>> db.close()
shelve.open('database') wieder geladen
werden, d.h. diese Objekte sind persistent.
Die Implementierung von shelve kann zur Compile-Zeit des Python
Interpreters konfiguriert werden. Es stehen verschiedene Datenbank
C-Bibliotheken zur Verfügung, z.B. dbm, gdbm und bsddb. Diese Bibliotheken
implementieren Datenstrukturen, die einen schnellen Zugriff auf die
abgespeicherten Daten ermöglichen.
In zwei wichtigen Punkten bietet shelve jedoch keine
Unterstützung: Es implementiert keine parallele Zugriffskontrolle,
d.h. wenn mehrere Prozesse auf persistente Objekte der gleichen
shelve Datei zugreifen, kann die Datei inkonsistent werden.
Außerdem stellt shelve keine Anfragesprache zur Verfügung.
Paos (Python Active Object Server) baut auf shelve auf und
implementiert eine Client/Server Architektur mit paralleler Zugriffskontrolle
und einer einfachen Anfragesprache.
cheesy.cs.colorado.edu läuft und
auf Anfragen auf dem Port 5000 wartet. Das Beispiel erzeugt einige
Objekte von der Klasse Person, speichert sie ab und führt eine
Anfrage aus.
import Client
import ExampleSchema
# Baue Verbindung mit dem Paos Server auf
conn = Client.Connection('cheesy.cs.colorado.edu', 5000, 'example')
# Erzeuge Objekte
john = ExampleSchema.Person()
john.name = 'John'
sue = ExampleSchema.Person()
sue.name = 'Sue'
john.loves = sue
bill = ExampleSchema.Person()
bill.name = 'Bill'
sue.loves = bill
bill.loves = sue
# Registriere Objekte bei dem Server
conn.register_objs([john, sue, bill])
# Speichere Objekte ab
conn.commit([john, sue, bill])
# Hole alle Instanzen von 'Person', die in Sue verliebt sind
answer = conn.get('r', 'Person', [('loves', '==', sue)])
# Für jedes Objekt in der Antwort drucke den Namen der Geliebten aus.
for obj in answer:
if obj.hasattr('sibling'):
print obj.name, obj.sibling.name
Zunächst importieren wir den Modul Client, um eine Verbindung mit
dem Paos Server aufbauen zu können. Danach importieren wir den Modul
ExampleSchema, welches die Klasse Person definiert
(siehe weiter unten). Schließlich bauen wir eine Verbindung auf, indem wir die Klasse der Verbindungen instanzieren. Dabei geben wir den Host-Namen und den Port an, auf dem der
Paos Server läuft. Im dritten Argument kann ein beliebiger Name für
die Anwendung eingetragen werden. Dieser Name taucht dann in den
entsprechenden Log-Einträgen des Servers auf.
Wir erzeugen dann drei Person -Instanzen und weisen ihnen Attributwerte
zu. Bevor wir diese Objekte abspeichern können, müssen sie erst bei
dem Server registriert werden. Die Registrierung weist den neuen Objekten
eine eindeutige Datenbanknummer zu. Dies kommt uns in der folgenden Anfrage zugute,
die der Abspeicherung folgt: "Gib mir alle Objekte aus der Klasse Person,
die das Objekt sue lieben." Wenn sue nicht registriert worden wäre,
könnte der Server dieses Objekt nicht mit den abgespeicherten Objekten
vergleichen.
Das erste Argument 'r' in der Anfrage bedeutet, daß die Objekte
in der Antwort von der Anwendung nur gelesen werden. Wenn wir Objekte
modifizieren möchten, dann müssen wir entweder in der Anfrage statt
'r' das Argument 'rw' angeben, oder die
Schreibrechte für die zu manipulierenden Objekte
nachträglich mit der Methode conn.lock erwerben. Wir können die
Schreibrechte nur dann erwerben, wenn kein anderer die Schreibrechte
besitzt. Wenn wir die Schreibrechte besitzen, kann kein anderer die
korrespondierenden Objekte modifizieren. Bei jedem conn.commit
und bei Programmabbruch verlieren wir alle erworbenen Schreibrechte.
Die Anfrage liefert uns eine Liste mit zwei neuen Objekten, welche
equivalent zu john und bill sind.
In der darauf folgenden Schleife
drucken wir den Namen der jeweiligen Geliebten aus (beidemal 'Sue').
Diese harmlos aussehende Schleife hat es allerdings in sich: Der Client-Modul
stellt sicher, daß sue, john.loves und
bill.loves auf das gleiche
Objekt zeigen. Ermöglicht ist dies durch die Registrierung von
sue und eines Resolutionsprozesses, der in den Attributzugriff
von john.loves und bill.loves eingebaut ist.
Implementiert ist dieser Resolutionsprozeß
über die eingebaute Python Methode __getattr__, die die
Umdefinierung
von Attributzugriffen erlaubt. Dieser erweiterte Attributzugriff sorgt
außerdem für das dynamische Laden von Objekten, die noch nicht in der
Client-Anwendung existieren (die Implementierung von dem Attributzugriff
ist in Schema.py in der Klasse DBobject definiert. Die Methode
register_objs der Klasse Connection in Client.py installiert diesen Attributzugriff für jedes Object in der Argumentliste).
Es ist wichtig zu verstehen, daß dieser Resolutionsprozeß nur für
die referentielle Konsistenz von registrierten Objekten
untereinander
sorgen kann. In dem obigen Beispiel zeigen die Variablen john
und bill
auf Objekte, welche nicht in der Antwort enthalten sind. Es liegt hier
in der Verantwortung des Programmierers zu erkennen, wann Variablen
auf veraltete Objekte zeigen. Mit einem einfachem Trick können
Variablen "aufgefrischt" werden: john = conn.cache[john.db_id].
Dazu ist es nötig zu wissen, daß das
Connection-Objekt einen Cache
für geladene Objekte verwaltet und auf diesen Cache über die
Datenbanknummern der registrierten Objekte zugreift. Jedes regestrierte
Objekt besitzt das Attribut "db_id mit einer eindeutige
Datenbanknummer. Der Cache enthält die jeweils zuletztgeladene Version
von allen geladenen Objekten.
Paos interessanteste Eigenschaft ist jedoch der Notifikations-Service. Das folgende Beispiel zeigt, wie dieser Dienst verwendet wird:
import Client
import ExampleSchema
import Utilities
import os
import pickle
# Definiere eine Pipe für Notifikationen
(read_pipe_fd, write_pipe_fd) = os.pipe()
# Baue Verbindung mit dem Server auf
conn = Client.Connection('cheesy.cs.colorado.edu', 5000,
'example', (read_pipe_fd, write_pipe_fd))
# Registriere Anfrage bei dem Notifikations Service
request_id = conn.register('Person', [('name', '==', 'Sue')])
while 1:
# Warte auf eine Notifikation und lese sie ein
data = Utilities.READ(read_pipe_fd, 10000)
# Packe Notifikation aus
(req_id, obj_list, other_client) = pickle.loads(data)
# Packe Identifikation von anderem Client aus
(other_host, other_pid, other_uid, other_name) = other_client
# Mache irgendwas damit
Im Vergleich zu dem ersten Beispiel müssen drei zusätzliche Module
geladen werden: Utilities ist ein Modul mit Hilfsprozeduren,
die in allen Paos Modulen verwendet werden. os und
pickle sind eingebaute
Python Module, die Betriebssystem Funktionen und Funktionen für
die Umwandlung von Objekten in einen String (Serialisierung) zur
Verfügung stellen.
Zunächst definieren wir eine Pipe, die uns später als Empfänger
für Notifikationen dienen wird. Wir bauen dann eine Verbindung zu
dem Paos Server auf. Der Aufruf der Connection-Funktion hat
die Pipe als viertes Argument, so daß die Pipe mit dem Verbindungsobjekt
assoziert werden kann. Dann registrieren wir eine Anfrage, die dafür
sorgt, daß der Server uns alle neuen Person-Objekte mit dem Namen
'Sue' schickt, sobald diese Objekte in die Datenbank neu eingetragen
werden. Der conn.register(...) Aufruf liefert eine
Registrierungsnummer zurück.
Die Notifikattionen erhalten wir über die Pipe. Wir benutzen dafür
eine Hilfsprozedur, die garantiert, daß die volle Länge der
Notifikation von der Pipe gelesen wird. Die Notifikation wird
als String über das Netz geschickt und muß bei der Empfängerseite
wieder in Python Objekt umgewandelt werden. Dies geschieht mithilfe
des Aufrufs pickle.loads(data). Eine Notifikation besteht
aus einem Tripel, welches die
Client.Connection-Funktionsaufrufs in der Anwendung.
Das Besondere an Chautauqua ist, daß es den Benutzern ermöglicht, die Struktur der Büroprozesse während ablaufender Arbeitsvorgänge zu verändern. In dem folgenden Schnappschuß sehen wir die Struktur eines Büroprozesses:
Mitarbeiter werden durch Sterne, Bürorollen durch Quadrate, und Aktivitäten durch Kreise und Dreiecke dargestellt. Kleine Punkte rechts oberhalb von Aktivitäten stellen "Token" dar, die den Zustand der Arbeit repräsentieren, und die mit dem Fortschritt der Arbeit durch den Graphen wandern. Mit dem Editor ist es nun möglich, einen beliebigen Teil des Graphen zu verändern. Wenn Aktivitäten gelöscht werden, können Token ihren Standort verlieren. Chautauqua bietet Mechanismen an, diese verlorenen Token aufzusammeln und ihnen neue Standorte in dem veränderten Graphen zuzuweisen.
ftp://ftp.cs.colorado.edu/users/carlosm/paos-1.4.tar.gz
ftp://ftp.cs.colorado.edu/users/carlosm/chautauqua-1.4.tar.gz (Chautauqua enthaelt Paos)
Detailliertere Dokumentation für Paos und Chautauqua ist zur Zeit in Vorbereitung und wird in der Newsgruppe comp.lang.python angekündigt.
Copyright © Linux Magazin