In einer neuen Python-Reihe berichtet das Linux-Magazin alle zwei Monate über aktuelle Entwicklungen und stellt Konzepte vor, die Python einzigartig machen. Thema dieser Folge ist die Objekt-Persistenz.
Willkommen zu der neuen Python-Reihe im Linux-Magazin. Hier sollen sowohl Themen für Anfänger als auch für Fortgeschrittene rund um Python behandelt werden. Dazu gehören Berichte über aktuelle Entwicklungen rund um Python und Problemlösungen, aber auch Grundlagen-Artikel zu bestimmten Themen. Der erste Artikel dieser Reihe befasst sich mit der dauerhaften Speicherung von Objekten. Zunächst jedoch ein kurzer Überblick über Python.
Python ist eine Skriptsprache, die Anfang der 90er Jahre von Guido van Rossum entwickelt wurde und sich seither zu einer universell einsetzbaren Programmiersprache gemausert hat. Heute ist Python neben Perl die wichtigste und am häufigsten verwendete Skriptsprache. Wie diese Bezeichnung schon impliziert, ist Python eine interpretierte Sprache, die Übersetzung eines Python-Programms erfolgt zur Laufzeit.
Eine Einführung in Python kann ein Zeitschriftenartikel zwar nicht geben, jedoch sollen einige Konzepte und Vorteile von Python genannt werden. Eine detaillierte Einführung findet man im Python-Tutorial unter http://www.python.org/doc, aber auch im neuen Open-Source-Buch “DiveInto Python” (http://www. diveintopython.org) oder in vielen anderen Büchern.
Python im Überblick
Ein paar Vorzüge und Besonderheiten von Python sollen hier kurz erwähnt werden:
- n Schnell zu lernen und zu lesen: Python besitzt eine einfache, verständliche Syntax und klare Funktionalität. Im Gegensatz zu Perl lassen sich auch sehr große Projekte erstellen, pflegen – und nach längerer Zeit immer noch verstehen. Der Sprachumfang von Python ist orthogonal, es gibt also in der Regel keine Funktionalität doppelt. Schleifen etwa werden über ein for– oder while-Konstrukte realisiert, repeat– oder do..until-Schleifen sind unnötig.
- n Modular: Zusammengehörige Funktionalitäten (zum Beispiel Sockets oder grafische Optionen) sind in Modulen zusammengefasst und werden bei Bedarf importiert. Module sind im Sinne von Code-Wiederverwendung von verschiedenen Applikationen nutzbar.
- n Interaktiv: Python besitzt einen interaktiven Modus, der es gerade Anfängern erlaubt, sich sehr einfach mit Python vertraut zu machen.
- n Kompakt: Im Vergleich zu Compiler-Sprachen wie C oder C++ sind Python-Programme sehr kompakt. Pythons Datentypen wie Wörterbücher, Listen und Tupel erlauben meist die Formulierung komplexer Operationen in einer Zeile. Der Gliederung eines Programms in Codeblöcke erfolgt durch Indentierung des Quellcodes. Das macht die Klammerung, die man von C kennt, überflüssig.
- n Objektorientiert: Im Gegensatz zu anderen Programmiersprachen ist Python objektorientiert konzipiert und nicht, wie etwa Perl, nachträglich um objektorientierte Konzepte erweitert worden. Dieses Konzept aus einem Guss hebt Python entscheidend von seiner Konkurrenz ab.
Die wachsende Beliebtheit von Python hat vor allem einen Grund: die klare und einfache Sprachstruktur, die einen leichten Einstieg erlaubt. Mittlerweile wird Python auch bereits an Schulen und Universitäten zur Vermittlung von Programmierkenntnissen eingesetzt.
Python ist jedoch nicht nur für Anfänger interessant, sie ist das schweizer Offiziersmesser für Programmierer. Die Anwendungsbereiche reichen von Web-Applikationen, String-Verarbeitung, administrativen und anderen Anwendungen, numerischen Berechnungen bis hin zur Steuerung komplexer Produktionsumgebungen in Betrieben [1]. Python bietet von sich aus viele nützliche Konzepte, die in anderen Sprachen nicht zu finden sind, zum Beispiel die Ansätze zur Objekt-Persistenz.
Die wichtigsten Neuerungen in Python 2.1 |
|
Geschachtelte Namensräume (nested scopes) Bis Python 2.0 gibt es drei Namensräume, in denen eine Variable in folgender Reihenfolge gesucht wird: lokaler Namesraum, Modul-Namesraum und Built-in-Namensraum. Diese Trennung ist nicht intuitiv, wenn man geschachtelte Funktionen betrachtet:
def f():
...
def g(value):
...
return g(value-1)
Der Aufruf der Funktion g() in der return-Anweisung wird eine Name-Error Exception auslösen, weil g in keinem der drei Namensräume definiert ist. Python 2.1 beseitigt diesen Mangel und erlaubt die Schachtelung von Namensräumen durch den Import des neuen nested_scopes-Moduls. __future__ Anweisungen Von Version zu Version werden neue Features in Python eingeführt, die unter Umständen die Kompatibilität zu bestehenden Applikationen brechen. Zur Milderung dieser Problematik können Neuerungen, die zum Beispiel in Python 2.2 fester Bestandteil sein werden, optional über eine __future__-Import-Anweisung eingebunden werden. Geschachtelte Namensräume werden etwa ab 2.2 fester Bestandteil von Python sein. Obwohl die Implementierung bereits abgeschlossen ist, ist sie in Python 2.1 noch nicht aktiviert. Um sie trotzdem benutzten zu können, muss sie über <b4>from __future__ import nested_scopes eingebunden und aktiviert werden. Warning Framework Im Laufe der Jahre haben sich viele Module angesammelt, die nicht mehr unterstützt werden, veraltet sind oder durch neuere mit verbesserter Funktionalität ersetzt wurden. Es ist für die Entwickler schwierig, Module zu entfernen, ohne dabei zu riskieren, dass Applikationen in späteren Versionen nicht mehr laufen. Das Warning Framework erlaubt es, versionsabhängige Warnung auszugeben, dass ein Modul in der nächsten Version nicht mehr eingebaut sein wird oder eine Funktionalität geändert oder entfernt wird. Python 2.1 gibt etwa beim Import des regex-Moduls die Warnung aus: <b1>>> import regex __main__:1: DeprecationWarning: the regex module ist deprecated; please use the re module Benutzer haben so einen Release-Zyklus Zeit, ihre Software auf das neuere Modul re für reguläre Ausdrücke umzustellen. Funktionsattribute In Python 2.1 darf man Funktionen Attribute zuweisen:
<b5>
def func():
....
func.author = "Holger Müller"
func.security = 1
Alle Attribute sind im Wörterbuch __dict__ der Funktion gespeichert. Bis zu Version 2.0 war es nur möglich, zusätzliche Informationen im so genannten Doc-String zu verbergen, der über f.__doc__ ausgelesen werden konnte. Neuer Installations-Mechanismus Die Installation erfolgt ab Version 2.1 mit Hilfe des Dist-Utils-Pakets, also des Standard-Installationswerkzeugs für Python-Module. Der Aufwand einer händischen Konfiguration der Module wie in den älteren Versionen entfällt damit. Das Installationsskript untersucht automatisch (ähnlich wie Configure) anhand der Header und Bibliotheken, welche Module es kompilieren kann, und baut sie automatisch. |
Was ist Objekt-Persistenz?
Objekte sind in jeder objektorientierten Programmiersprache Container von Methoden und Attributen. Bei vielen Applikationen ist es wünschenswert, ein Objekt dauerhaft auf einem Speichermedium abzulegen, um es zu einem späteren Zeitpunkt, beispielsweise nach einem Programmneustart, wieder zu benutzen. Bei einem Programmabbruch kann man dann wieder auf den letzten gespeicherten Zustand des Objekts zurückgreifen.
Es gibt natürlich immer die Möglichkeit, applikationsabhängigen Code zum Export von wichtigen Daten zu schreiben, aber mit jeder Modifikation des Objekts muss auch die Export-Funktionalität entsprechend angepasst werden. Was man sich an dieser Stelle wünscht, ist transparente Objekt-Persistenz, also ein Mechanismus, der es ohne zusätzlichen Code ermöglicht, Objekte dauerhaft zu speichern. Das sollte geschehen, ohne dass der Programmierer oder die Applikation ein besonderes Wissen über Persistenz besitzen muss.
Persistenz in Python
Python besitzt seit langer Zeit die beiden Module pickle und cPickle, mit denen sich Objekte serialisieren lassen. Serialisierte Objekte sind auf einem Stream speicherbar, beispielsweise auf einem Dateiobjekt. Diesen Vorgang bezeichnet man als Pickeln oder Pökeln, man konserviert gewissermaßen ein Objekt, um es später wieder zu verwenden. Umgekehrt kann Python ein solches serialisiertes Objekt einlesen und in ein Objekt zurückverwandeln (Unpickeln).
Die beiden Module sind in der Funktionalität identisch: cPickle ist die C-Reimplementierung des in Python geschriebenen pickle-Moduls. Ihr sollte man aus Effizienzgründen immer den Vorzug geben.
Im Beispiel in Listing 1 wird ein Objekt mit den beiden Attributen num=212 und txt=’Python ist cool’ erzeugt. Das Objekt wird durch den cPickle.dump()-Aufruf persistent in einem internen Format in der Datei instClass.p gespeichert. Der anschließende cPickle.load()-Aufruf lädt die Datei und generiert eine neue Instanz von myClass, die die gleichen Attribute wie das ursprüngliche Objekt besitzt.
Listing 1: Pickeln eines Objekts |
from ZODB import DB, FileStorage fstorage = FileStorage.FileStorage(`Data.fs') db = DB(fstorage) connection = db.open() root = connection.root() |
Diese Vorgehensweise ist generell für jedes Python-Objekt möglich, es gibt jedoch einige Ausnahmen. So sind etwa Datei-Objekte oder Sockets nicht serialisierbar, was aber in den genannten Fällen auch nicht sehr sinnvoll wäre. Die persistente Speicherung beliebiger Objekte – auch solcher mit mehrfacher Vererbung – ist durch Pickeln möglich, aber der Programmierer muss immer noch Teile des Code selber implementieren.
Persistenz in Python mit Hilfe der ZODB
Basierend auf dem Pickle-Mechanismus entstand während der Entwicklung des Zope-Appplikationsservers die sogenannte Zope Object Data Base (kurz ZODB), die den Entwickler auch von dieser Last befreit. Die Verwendung ist relativ einfach: Die ZODB stellt sich für den Entwickler als ein Mapping-Objekt dar, das genau wie ein Wörterbuch in Python angesprochen wird:
zodb[`instClass'] = instClass
Das Objekt wird hierbei in der ZODB an den Schlüssel `instClass’ gebunden und abgespeichert. Umgekehrt kann man aus der ZODB Objekte wieder einfach auslesen:
instClass = zodb[`instClass']
Das sieht sehr elegant aus und so ist es auch. Aber bis es soweit ist, will ZODB erst mal installiert sein. Zur Speicherung der Objekte ist die ZODB nicht auf ein bestimmtes Medium festgelegt. Im Normalfall legt sie die Objekte in einer Datei im Dateisystem ab. Es existieren jedoch auch Adapter für Datenbanken wie Oracle oder BerkleyDB. Das Speichermedium ist für die Applikation transparent. Lediglich beim Öffnen der ZODB muss man das Medium festlegen, also die gegebenenfalls darunter liegende tatsächliche Datenbankschicht.
Installation der ZODB
Die ZODB ist in Zope integriert und kann mitbenutzt werden, falls Zope bereits installiert ist. Wer Zope nicht braucht, kann stattdessen auf die Stand-alone-Version der ZODB zurückgreifen, die von A. M. Kuchling gepflegt wird [2].
Nach dem Entpacken des Archivs erfolgt die Installation mit Hilfe des Tools Dist-Utils (enthalten in Python 2.0/2.1, für Python 1.5.x muss Dist-Utils nachträglich installiert werden):
python setup.py install
Das sollte automatisch alle ZODB-Quellen und -Module kompilieren und installieren. Es empfiehlt sich, die aktuelle Python-Version 2.1 zu verwenden.
Benutzung der ZODB
Wie man die ZODB öffnet, wenn eine Datei als Speichermedium dient, ist in Listing 2 im Einzelnen nachzulesen. Das FileStorage-Objekt repräsentiert in diesem Fall das Speichermedium, das für die ZODB benutzt wird. (Bei der Verwendung eines ZODB-Adapters für eine relationale Datenbank ist der Aufruf entsprechend anzupassen.) Die dann folgenden Aufrufe öffnen die Datenbank und erzeugen das eigentliche `root’-Objekt, über das die ZODB von der Applikation aus angesprochen wird.
Listing 2: Öffnen einer ZODB-Datenbank |
from ZODB import DB, FileStorage fstorage = FileStorage.FileStorage(`Data.fs') db = DB(fstorage) connection = db.open() root = connection.root() |
Serialisierbare Python-Objekte sind jetzt einfach in der ZODB abzulegen:
root[`rot'] = `ZODB ist cool' root[`blau'] = [`Perl','ist','cool']
Durch die Zuweisungen werden die Objekte nur temporär in der ZODB gespeichert. Um sie persistent – also dauerhaft – zu speichern, muss die Transaktion bestätigt (commited) werden:
get_transaction().commit()
Eine Transaktion ist eine atomare Operation und umfasst eine Sequenz von Änderungen innerhalb der Datenbank. Der Transaktionsmechanismus der Datenbank garantiert, dass entweder alle oder keine der Änderungen durchgeführt werden. Das garantiert die Integrität der Daten zwischen zwei Commit-Aufrufen.
Nach dem Speichern der Daten in der ZODB kann man die Daten natürlich auch wieder auslesen. Das Öffnen der ZODB ist identisch mit dem Schreiben von Daten.
Das Auslesen der Daten erfolgt genauso wie die Benutzung eines Wörterbuchs:
print root[`root'] -> `ZDOB ist cool' print root[`blau'] -> [`Perl','ist','cool']
Änderungen in der ZODB
Die ZODB erkennt Veränderungen von Objekten automatisch und speichert sie auch. Allerdings mit einer Ausnahme: Veränderungen an Listen und Wörterbüchern werden nicht automatisch erkannt. Das gilt allgemein für alle Objekte, die man in der Python-Philosophie als mutable beziehungsweise veränderbar bezeichnet.
Eine Veränderung an einer Liste oder einem Wörterbuch darf in diesem Fall also nicht wie üblich über
root[`blau'].append(`a lot')
get_transaction().commit()
vorgenommen werden, sondern erfolgt über eine Neuzuweisung des Objekts:
temp = root[`blau']
temp.insert(2,'nicht')
root[`blau'] = temp
get_transaction().commit()
Persistente Klassen
Besonders einfach ist die Umwandlung von Klassen in persistente Klassen durchzuführen. Dazu müssen diese nämlich nur von der Klasse Persistence.Persistent abgeleitet werden. Das entsprechende Vorgehen im Einzelnen erläutert das in Listing 3 ausgeführte Beispiel.
Listing 3: Herstellung persistenter Klassen |
import ZODB
import Persistence
class PLanguage(Persistence.Persistent)
def __init__(self,lang,easy2learn):
self.language = lang
self.learneffort = easy2learn
self.authors = []
....
languages = []
languages.append( PLanguage(`Python','very easy') )
languages.append( PLanguage(`Perl','very hard') )
languages.append( PLanguage(`TCL','easy') )
zodb[`languages'] = languages
TCL = zodb[`languages][2]
TCL.learneffort = `not easy'
get_transaction().commit()
|
Wie oben schon erläutert, werden Änderungen an veränderbaren Datentypen nicht automatisch von der ZODB erkannt. Man muss also in solchen Fällen der Data Base die Veränderung extra signalisieren, indem man das Attribut _p_changed auf 1 setzt. Die ZODB wird daraufhin von sich aus das Objekt entsprechend updaten:
class PLanguage(Persistence.Persistent)
....
def setAuthor(self,author):
self.authors.append( author )
self._p_changed = 1
Ausblick
Mit der Zope-Erweiterung Zope Enterprise Objects (ZEO) lässt sich eine verteilte ZODB aufbauen. Damit sind auch Objekte verteilt speicherbar. Informationen gibt’s unter [4].
Der Artikel hat gezeigt, wie einfach die ZODB zu benutzen ist und dass sie dem Python-Entwickler ein sehr mächtiges Werkzeug in die Hand gibt, das mit wenig Lernaufwand und geringfügigen Source-Code-Änderungen transparente Objekt-Persistenz ermöglicht. ( uwo) n
Infos |
|
[1] Python in der Praxis: http://www.python.org/psa/Users.html [2] ZODB-Pages von A. M. Kuchling: http://www.amk.ca/zodb/ [3] M. Pelletier: ZODB for Python Programmers: http://www.zope.org/Documentation/Articles/ZODB1 [4] Zope Enterprise Objects (ZEO): http://www.zope.org/Products/ZEO |
Der Autor |
|
Andreas Jung lebt in der Nähe von Washington D.C. und arbeitet als Software Engineer für Zope Corporation (früher Digital Creations) im Coreteam von Zope. E-Mail: andreas@andreas-jung.com |




