11. Eine kurze Einführung in die Standardbibliothek - Teil II

Dieser zweite Teil der Tour beschäftigt sich mit Modulen für Fortgeschrittene. Diese Module sind selten in kleinen Skripten zu finden.

11.1. Ausgabeformatierung

Das Modul reprlib stellt eine Variante von repr() zur Verfügung, die für die verkürzte Anzeige von großen oder tief verschachtelten Containern ausgelegt ist:

>>> import reprlib
>>> reprlib.repr(set('supercalifragilisticexpialidocious'))
"set(['a', 'c', 'd', 'e', 'f', 'g', ...])"

Das Modul pprint bietet ausgefeiltere Kontrollmöglichkeiten für die Ausgabe von eingebauten und benutzerdefinierten Objekten, so dass diese im Interpreter gelesen werden können. Falls das Ergebnis länger als eine Zeile ist, fügt der “pretty printer” Zeilenumbrüche und Einrückungen hinzu, um die Datenstruktur klarer zu zeigen:

>>> import pprint
>>> t = [[[['schwarz', 'cyan'], 'weiß', ['grün', 'rot']], [['magenta',
...     'gelb'], 'blau']]]
...
>>> pprint.pprint(t, width=30)
[[[['schwarz', 'cyan'],
   'weiß',
   ['grün', 'rot']],
  [['magenta', 'gelb'],
   'blau']]]

Das Modul textwrap formatiert Textabsätze so, dass sie einer vorgegebenen Bildschirmbreite entsprechen:

>>> import textwrap
>>> doc = """Die Methode wrap() verhält sich wie fill(), außer dass sie
... anstatt einer einzigen großen Zeichenkette mit Zeilenumbrüchen, eine
... Liste aus Zeichenketten zurückgibt um die umgebrochenen Zeilen
... voneinander zu trennen."""
...
>>> print(textwrap.fill(doc, width=40))
Die Methode wrap() verhält sich wie
fill(), außer dass sie anstatt einer
einzigen großen Zeichenkette mit
Zeilenumbrüchen, eine Liste aus
Zeichenketten zurückgibt um die
umgebrochenen Zeilen voneinander zu
trennen.

Das Modul locale greift auf eine Datenbank mit länderspezifischen Datenformaten zu. Das grouping-Attribut der format-Funktion von locale bietet eine einfache Möglichkeit um Zahlen mit Tausendertrennzeichen zu formatieren:

>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
'de_DE.UTF-8'
>>> conv = locale.localeconv()          # die Zeichenkonventionen holen
>>> x = 1234567.8
>>> locale.format("%d", x, grouping=True)
'1.234.567'
>>> locale.format_string("%s%.*f", (conv['currency_symbol'],
...               conv['frac_digits'], x), grouping=True)
'€1.234.567,80'

11.2. Templating

Das Modul string enthält die vielseitige Klasse Template, die wegen ihrer vereinfachten Syntax zum verändern durch Endbenutzer geeignet ist. Das ermöglicht Anwendern ihre Anwendung anzupassen, ohne die Anwendung zu verändern.

Das Format benutzt Platzhalter, die aus $ und einem gültigen Python-Bezeichner (alphanumerische Zeichen und Unterstriche) bestehen. Umgibt man einen Platzhalter mit geschweiften Klammern, können andere alphanumerische Zeichen ohne Leerzeichen dazwischen folgen. Schreibt man $$, erzeugt man ein einzelnes escaptes $:

>>> from string import Template
>>> t = Template('Die Bürger von ${village} schicken 10 € für $cause.')
>>> t.substitute(village='Hannover', cause='den Grabenfond')
'Die Bürger von Hannover schicken 10 € für den Grabenfond.'

Die Methode substitute() verursacht einen KeyError, wenn ein Platzhalter nicht von einem Dictionary oder einem Schlüsselwortargument bereitgestellt wird. Bei Serienbrief-artigen Anwendungen können die vom Benutzer bereitgestellten Daten lückenhaft sein und die Methode safe_substitute() ist hier deshalb passender — sie lässt Platzhalter unverändert, wenn Daten fehlen:

>>> t = Template('Bringe $item $owner zurück.')
>>> d = dict(item='die unbeladene Schwalbe')
>>> t.substitute(d)
Traceback (most recent call last):
  . . .
KeyError: 'owner'
>>> t.safe_substitute(d)
'Bringe die unbeladene Schwalbe $owner zurück.'

Unterklassen von Template können einen eigenen Begrenzer angeben. Zum Beispiel könnte ein Umbenennungswerkzeug für einen Fotobrowser das Prozentzeichen als Platzhalter für das aktuelle Datum, die Fotonummer oder das Dateiformat auswählen:

>>> import time, os.path
>>> photofiles = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
>>> class BatchRename(Template):
...     delimiter = '%'
>>> fmt = input('Umbenennungsschema (%d-Datum %n-Nummer %f-Format):  ')
Umbenennungsschema (%d-Datum %n-Nummer %f-Format):  Ashley_%n%f

>>> t = BatchRename(fmt)
>>> date = time.strftime('%d%b%y')
>>> for i, filename in enumerate(photofiles):
...     base, ext = os.path.splitext(filename)
...     newname = t.substitute(d=date, n=i, f=ext)
...     print('{0} --> {1}'.format(filename, newname))

img_1074.jpg --> Ashley_0.jpg
img_1076.jpg --> Ashley_1.jpg
img_1077.jpg --> Ashley_2.jpg

Eine andere Anwendungsmöglichkeit für Templates ist die Trennung von Programmlogik und den Details der Ausgabeformate. Dies ermöglicht es eigene Vorlagen für XML-Dateien, Klartextberichte und HTML Web-Berichte zu ersetzen.

11.3. Arbeit mit strukturierten binären Daten

Das Modul struct stellt die Funktionen pack() und unpack() bereit, mit denen strukturierte binäre Daten verarbeitet werden können. Das folgende Beispiel zeigt, wie die Headerinformationen aus einem ZIP-Archiv ausgelesen werden, ohne das zipfile-Modul zu benutzen. Die Pack Codes "H" und "I" stellen zwei Byte respektive vier Byte lange unsigned Integers dar. Das Zeichen "<" bedeutet, dass damit Standardgrößen gemeint sind und in der “Little Endian”-Bytereihenfolge vorliegen:

import struct

with open('myfile.zip', 'rb') as f:
    data = f.read()

start = 0
for i in range(3):                      # zeige die ersten 3 Dateiheader
    start += 14
    fields = struct.unpack('<IIIHH', data[start:start+16])
    crc32, comp_size, uncomp_size, filenamesize, extra_size = fields

    start += 16
    filename = data[start:start+filenamesize]
    start += filenamesize
    extra = data[start:start+extra_size]
    print(filename, hex(crc32), comp_size, uncomp_size)

    start += extra_size + comp_size     # skip to the next header

11.4. Multi-threading

Threading ist eine Methode, um nicht unmittelbar voneinander abhängige Prozesse abzukoppeln. Threads können benutzt werden, um zu verhindern, dass Programme, die während Berechnungen Benutzereingaben akzeptieren, “hängen”. Ein ähnlicher Verwendungzweck ist es, einen Thread für I/O und einen anderen für Berechnungen zu benutzen.

Dieser Code zeigt wie das threading Modul benutzt werden kann um Prozesse im Hintergrund ablaufen zu lassen, während das Hauptprogramm parallel dazu weiterläuft:

import threading, zipfile

class AsyncZip(threading.Thread):
    def __init__(self, infile, outfile):
        threading.Thread.__init__(self)
        self.infile = infile
        self.outfile = outfile
    def run(self):
        f = zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED)
        f.write(self.infile)
        f.close()
        print('Zippen im Hintergrund abgeschlossen:', self.infile)

background = AsyncZip('mydata.txt', 'myarchive.zip')
background.start()
print('Das Hauptprogramm läuft inzwischen weiter.')

background.join()    # Warten bis sich der Thread beendet.
print('Das Hauptprogramm hat auf die Beendigung des Hintergrund-Prozesses
      gewartet.')

Das Hauptproblem von Programmen mit mehreren Threads ist die Koordination der Zugriffe auf gemeinsame Daten oder andere Ressourcen. Dafür bietet das threading Modul einige Synchronisationsmethoden wie Locks, Events, Condition Variables und Semaphoren an.

Der beste Weg ist es aber, allen Zugriff auf Ressourcen in einem Thread zu koordinieren. Das queue Modul wird benutzt, um die Anfragen von den anderen Threads in dieses zu bekommen. Programme die Queue Objekte als Kommunikation zwischen ihren Threads nutzen sind einfacher zu entwickeln, lesbarer und stabiler.

11.5. Logging

Das Modul logging ermöglicht ein ausführliches und flexibles Erstellen von Logfiles. Im einfachsten Fall werden Logs in eine Datei geschrieben oder an sys.stderr geschickt:

import logging
logging.debug('Debugging Information')
logging.info('Information')
logging.warning('Warnung:Datei %s nicht gefunden', 'server.conf')
logging.error('Fehler')
logging.critical('Kritischer Fehler!')

Die Ausgabe von Meldungen der Stufen info und debug wird standardmäßig unterdrückt; übrige Meldungen werden an sys.stderr geschickt. Darüber hinaus können Meldungen auch per E-Mail, über Datenpakete (UDP), Sockets (TCP) oder an einen HTTP-Server ausgeliefert werden. Filter können weiterhin entscheiden, worüber Meldungen ausgegeben werden - je nach Priorität: DEBUG, INFO, WARNING, ERROR und CRITICAL.

Das Logging-system kann entweder direkt mittels Python konfiguriert werden oder seine Konfiguration aus einer vom Benutzer definierbaren Konfigurationsdatei lesen, ohne dass dabei das Programm selbst geändert werden muss.

11.6. Weak References

Python bietet automatische Speicherverwaltung (Zählen von Referenzen für die meisten Objekte und garbage collection). Der Speicher wird kurz nachdem die letzte Referenz auf ein Objekt aufgelöst worden ist freigegeben.

Für die meisten Anwendungen funktioniert dieser Ansatz gut, gelegentlich kann es allerdings auch nötig werden, Objekte nur so lange vorzuhalten, wie sie an anderer Stelle noch verwendet werden. Das allein führt allerdings bereits dazu, dass eine Referenz auf das Objekt erstellt wird, die es permanent macht. Mit dem Modul weakref können Objekte vorgehalten werden, ohne eine Referenz zu erstellen. Wird das Objekt nicht länger gebraucht, wird es automatisch aus einer Tabelle mit so genannten schwachen Referenzen gelöscht und eine Rückruf-Funktion für weakref-Objekte wird aufgerufen. Dieser Mechanismus wird etwa verwendet, um Objekte zwischenzuspeichern, deren Erstellung besonders aufwändig ist:

>>> import weakref, gc
>>> class A:
...     def __init__(self, value):
...             self.value = value
...     def __repr__(self):
...             return str(self.value)
...
>>> a = A(10)                   # Eine Referenz erstellen
>>> d = weakref.WeakValueDictionary()
>>> d['primary'] = a            # Erstellt keine Referenz
>>> d['primary']                # Klappt, falls Objekt noch vorhanden
10
>>> del a                       # Einzige Referenz löschen
>>> gc.collect()                # Garbage collector aufrufen
0
>>> d['primary']                # Eintrag wurde automatisch gelöscht
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    d['primary']                # Eintrag wurde automatisch gelöscht
  File "C:/python33/lib/weakref.py", line 46, in __getitem__
    o = self.data[key]()
KeyError: 'primary'

11.7. Werkzeuge zum Arbeiten mit Listen

Viele Datenstrukturen können mit dem eingebauten Listentyp dargestellt werden. Jedoch gibt es manchmal Bedarf für eine alternative Implementierung mit anderen Abstrichen was Leistung angeht.

Das Modul array stellt die Klasse array bereit, die sich wie eine Liste verhält, jedoch nur homogene Daten aufnimmt und diese kompakter speichert. Das folgende Beispiel zeigt ein array von Nummern, die als vorzeichenlose binäre Nummern der Länge 2 Byte (Typcode "H") gespeichert werden, anstatt der bei Listen üblichen 16 Byte pro Python-Ganzzahlobjekt:

>>> from array import array
>>> a = array('H', [4000, 10, 700, 22222])
>>> sum(a)
26932
>>> a[1:3]
array('H', [10, 700])

Das Modul collections stellt die Klasse deque bereit, das sich wie eine Liste verhält, aber an das schneller angehängt und schneller Werte von der linken Seite “gepopt” werden können, jedoch langsamer Werte in der Mitte nachschlägt. Sie ist gut dazu geeignet Schlangen (Queues) und Baumsuchen, die zuerst in der Breite suchen (breadth first tree searches):

>>> from collections import deque
>>> d = deque(["task1", "task2", "task3"])
>>> d.append("task4")
>>> print("Handling", d.popleft())
Handling task1

unsearched = deque([starting_node])
def breadth_first_search(unsearched):
   node = unsearched.popleft()
   for m in gen_moves(node):
       if is_goal(m):
           return m
       unsearched.append(m)

Zusätzlich zu alternativen Implementierungen von Listen bietet die Bibliothek auch andere Werkzeuge wie das bisect-Modul an, das Funktionen zum verändern von sortierten Listen enthält:

>>> import bisect
>>> scores = [(100, 'perl'), (200, 'tcl'), (400, 'lua'), (500, 'python')]
>>> bisect.insort(scores, (300, 'ruby'))
>>> scores
[(100, 'perl'), (200, 'tcl'), (300, 'ruby'), (400, 'lua'), (500, 'python')]

Das heapq-Modul stellt Funktionen bereit, um Heaps auf der Basis von normalen Listen zu implementieren. Der niedrigste Wert wird immer an der Position Null gehalten. Das ist nützlich für Anwendungen, die wiederholt auf das kleinste Element zugreifen, aber nicht die komplette Liste sortieren wollen:

>>> from heapq import heapify, heappop, heappush
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> heapify(data)                      # in Heapreihenfolge neu ordnen
>>> heappush(data, -5)                 # neuen Eintrag hinzufügen
>>> [heappop(data) for i in range(3)]  # die drei kleinsten Einträge holen
[-5, 0, 1]

11.8. Dezimale Fließkomma-Arithmetik

Das Modul decimal bietet den Decimal-Datentyp für dezimale Fließkomma-Arithmetik. Verglichen mit der eingebauten float- Implementierung von binären Fließkomma-Zahlen ist die Klasse besonders hilfreich für

  • Finanzanwendungen und andere Gebiete, die eine exakte dezimale Repräsentation,
  • Kontrolle über die Präzision,
  • Kontrolle über die Rundung, um gesetzliche oder regulative Anforderungen zu erfüllen,
  • das Tracking von signifikanten Dezimalstellen

erfordern, oder

  • für Anwendungen bei denen der Benutzer erwartet, dass die Resultate den händischen Berechnungen entsprechen.

Die Berechnung einer 5% Steuer auf eine 70 Cent Telefonrechnung ergibt unterschiedliche Ergebnisse in dezimaler und binärer Fließkomma-Repräsentation. Der Unterschied wird signifikant, wenn die Ergebnisse auf den nächsten Cent gerundet werden:

>>> from decimal import *
>>> round(Decimal('0.70') * Decimal('1.05'), 2)
Decimal('0.74')
>>> round(.70 * 1.05, 2)
0.73

Das Decimal Ergebnis behält die Null am Ende, automatisch vierstellige Signifikanz aus den Faktoren mit zweistelliger Signifikanz folgernd. Decimal bildet die händische Mathematik nach und vermeidet Probleme, die auftreten, wenn binäre Fließkomma-Repräsentation dezimale Mengen nicht exakt repräsentieren können.

Die exakte Darstellung ermöglicht es der Klasse Decimal Modulo Berechnungen und Vergleiche auf Gleichheit durchzuführen, bei denen die binäre Fließkomma-Repräsentation untauglich ist:

>>> Decimal('1.00') % Decimal('.10')
Decimal('0.00')
>>> 1.00 % 0.10
0.09999999999999995

>>> sum([Decimal('0.1')]*10) == Decimal('1.0')
True
>>> sum([0.1]*10) == 1.0
False

Das decimal-Modul ermöglicht Arithmetik mit so viel Genauigkeit, wie benötigt wird:

>>> getcontext().prec = 36
>>> Decimal(1) / Decimal(7)
Decimal('0.142857142857142857142857142857142857')