11. Kurzer Überblick über die Standardbibliothek — Teil II

Diese zweite Tour behandelt fortgeschrittenere Module, die professionelle Programmierbedürfnisse unterstützen. Diese Module kommen in kleinen Skripten selten vor.

11.1. Ausgabeformatierung

Das Modul reprlib bietet eine Version von repr(), die für die abgekürzte Anzeige großer oder tief verschachtelter Container angepasst ist.

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

Das Modul pprint bietet eine anspruchsvollere Kontrolle über die Ausgabe sowohl von integrierten als auch von benutzerdefinierten Objekten in einer für den Interpreter lesbaren Weise. Wenn das Ergebnis länger als eine Zeile ist, fügt der "pretty printer" Zeilenumbrüche und Einrückungen hinzu, um die Datenstruktur klarer darzustellen.

>>> import pprint
>>> t = [[[['black', 'cyan'], 'white', ['green', 'red']], [['magenta',
...     'yellow'], 'blue']]]
...
>>> pprint.pprint(t, width=30)
[[[['black', 'cyan'],
   'white',
   ['green', 'red']],
  [['magenta', 'yellow'],
   'blue']]]

Das Modul textwrap formatiert Textabsätze, damit sie auf eine bestimmte Bildschirmbreite passen.

>>> import textwrap
>>> doc = """The wrap() method is just like fill() except that it returns
... a list of strings instead of one big string with newlines to separate
... the wrapped lines."""
...
>>> print(textwrap.fill(doc, width=40))
The wrap() method is just like fill()
except that it returns a list of strings
instead of one big string with newlines
to separate the wrapped lines.

Das Modul locale greift auf eine Datenbank kulturspezifischer Datenformate zu. Das Gruppierungsattribut der Formatfunktion von locale bietet eine direkte Möglichkeit, Zahlen mit Gruppentrennzeichen zu formatieren.

>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'English_United States.1252')
'English_United States.1252'
>>> conv = locale.localeconv()          # get a mapping of conventions
>>> x = 1234567.8
>>> locale.format_string("%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. Vorlagen (Templating)

Das Modul string enthält eine vielseitige Klasse Template mit einer vereinfachten Syntax, die für die Bearbeitung durch Endbenutzer geeignet ist. Dies ermöglicht es Benutzern, ihre Anwendungen anzupassen, ohne die Anwendung selbst ändern zu müssen.

Das Format verwendet Platzhalternamen, die aus einem $ mit gültigen Python-Identifikatoren (alphanumerische Zeichen und Unterstriche) gebildet werden. Das Umgeben des Platzhalters mit geschweiften Klammern erlaubt es ihm, von weiteren alphanumerischen Buchstaben ohne dazwischenliegende Leerzeichen gefolgt zu werden. Das Schreiben von $$ erzeugt ein einzelnes maskiertes $.

>>> from string import Template
>>> t = Template('${village}folk send $$10 to $cause.')
>>> t.substitute(village='Nottingham', cause='the ditch fund')
'Nottinghamfolk send $10 to the ditch fund.'

Die Methode substitute() löst einen KeyError aus, wenn ein Platzhalter in einem Wörterbuch oder einem Schlüsselwortargument nicht bereitgestellt wird. Für Mail-Merge-ähnliche Anwendungen können die vom Benutzer bereitgestellten Daten unvollständig sein, und die Methode safe_substitute() kann besser geeignet sein – sie lässt Platzhalter unverändert, wenn Daten fehlen.

>>> t = Template('Return the $item to $owner.')
>>> d = dict(item='unladen swallow')
>>> t.substitute(d)
Traceback (most recent call last):
  ...
KeyError: 'owner'
>>> t.safe_substitute(d)
'Return the unladen swallow to $owner.'

Template-Unterklassen können ein benutzerdefiniertes Trennzeichen angeben. Beispielsweise kann ein Dienstprogramm zur Stapelumbenennung für einen Fotobrowser Prozentzeichen für Platzhalter wie das aktuelle Datum, die Bildsequenznummer oder das Dateiformat verwenden.

>>> import time, os.path
>>> photofiles = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
>>> class BatchRename(Template):
...     delimiter = '%'
...
>>> fmt = input('Enter rename style (%d-date %n-seqnum %f-format):  ')
Enter rename style (%d-date %n-seqnum %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 weitere Anwendung für Vorlagen ist die Trennung der Programmlogik von den Details mehrerer Ausgabeformate. Dies ermöglicht den Austausch benutzerdefinierter Vorlagen für XML-Dateien, einfache Textberichte und HTML-Webberichte.

11.3. Arbeiten mit Binärdaten-Aufbau-Layouts

Das Modul struct bietet die Funktionen pack() und unpack() für die Arbeit mit Binärdaten-Aufbau-Layouts variabler Länge. Das folgende Beispiel zeigt, wie man durch Kopfzeileninformationen in einer ZIP-Datei iteriert, ohne das Modul zipfile zu verwenden. Die Pack-Codes "H" und "I" stehen für zwei bzw. vier Byte große vorzeichenlose Zahlen. Das "<" gibt an, dass es sich um Standardgrößen und um Little-Endian-Byte-Reihenfolge handelt.

import struct

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

start = 0
for i in range(3):                      # show the first 3 file headers
    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. Multithreading

Threading ist eine Technik zur Entkopplung von Aufgaben, die nicht sequenziell voneinander abhängig sind. Threads können verwendet werden, um die Reaktionsfähigkeit von Anwendungen zu verbessern, die Benutzereingaben akzeptieren, während andere Aufgaben im Hintergrund laufen. Ein ähnlicher Anwendungsfall ist das parallele Ausführen von E/A-Operationen mit Berechnungen in einem anderen Thread.

Der folgende Code zeigt, wie das High-Level-Modul threading Aufgaben im Hintergrund ausführen kann, während das Hauptprogramm 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('Finished background zip of:', self.infile)

background = AsyncZip('mydata.txt', 'myarchive.zip')
background.start()
print('The main program continues to run in foreground.')

background.join()    # Wait for the background task to finish
print('Main program waited until background was done.')

Die Hauptherausforderung bei Multithreading-Anwendungen ist die Koordination von Threads, die Daten oder andere Ressourcen gemeinsam nutzen. Zu diesem Zweck bietet das Threading-Modul eine Reihe von Synchronisationsprimitiven, darunter Sperren (locks), Ereignisse (events), bedingte Variablen (condition variables) und Semaphoren.

Obwohl diese Werkzeuge mächtig sind, können kleine Designfehler zu Problemen führen, die schwer zu reproduzieren sind. Daher ist der bevorzugte Ansatz für die Aufgabenkoordination, den gesamten Zugriff auf eine Ressource in einem einzigen Thread zu konzentrieren und dann das Modul queue zu verwenden, um diesen Thread mit Anfragen von anderen Threads zu versorgen. Anwendungen, die Queue-Objekte für die Inter-Thread-Kommunikation und -Koordination verwenden, sind einfacher zu entwerfen, lesbarer und zuverlässiger.

11.5. Protokollierung (Logging)

Das Modul logging bietet ein voll ausgestattetes und flexibles Protokollierungssystem. Im einfachsten Fall werden Protokollnachrichten in eine Datei oder nach sys.stderr gesendet.

import logging
logging.debug('Debugging information')
logging.info('Informational message')
logging.warning('Warning:config file %s not found', 'server.conf')
logging.error('Error occurred')
logging.critical('Critical error -- shutting down')

Dies erzeugt die folgende Ausgabe.

WARNING:root:Warning:config file server.conf not found
ERROR:root:Error occurred
CRITICAL:root:Critical error -- shutting down

Standardmäßig werden Informations- und Debugging-Nachrichten unterdrückt und die Ausgabe an den Standardfehlerkanal gesendet. Weitere Ausgabeoptionen umfassen das Routing von Nachrichten über E-Mail, Datagramme, Sockets oder an einen HTTP-Server. Neue Filter können verschiedene Routen basierend auf der Nachrichtenpriorität auswählen: DEBUG, INFO, WARNING, ERROR und CRITICAL.

Das Protokollierungssystem kann entweder direkt aus Python konfiguriert oder aus einer vom Benutzer editierbaren Konfigurationsdatei geladen werden, um eine individuelle Protokollierung zu ermöglichen, ohne die Anwendung ändern zu müssen.

11.6. Schwache Referenzen (Weak References)

Python verwaltet den Speicher automatisch (Referenzzählung für die meisten Objekte und Garbage Collection zur Eliminierung von Zyklen). Der Speicher wird kurz nach dem Entfernen der letzten Referenz darauf freigegeben.

Dieser Ansatz funktioniert für die meisten Anwendungen gut, aber gelegentlich besteht die Notwendigkeit, Objekte nur so lange zu verfolgen, wie sie von etwas anderem verwendet werden. Leider schafft bereits die Verfolgung selbst eine Referenz, die sie permanent macht. Das Modul weakref bietet Werkzeuge zur Verfolgung von Objekten, ohne eine Referenz zu erstellen. Wenn das Objekt nicht mehr benötigt wird, wird es automatisch aus einer schwachen Referenztabelle entfernt und für schwache Referenzobjekte wird ein Rückruf ausgelöst. Typische Anwendungen sind das Caching von Objekten, deren Erstellung aufwendig ist.

>>> import weakref, gc
>>> class A:
...     def __init__(self, value):
...         self.value = value
...     def __repr__(self):
...         return str(self.value)
...
>>> a = A(10)                   # create a reference
>>> d = weakref.WeakValueDictionary()
>>> d['primary'] = a            # does not create a reference
>>> d['primary']                # fetch the object if it is still alive
10
>>> del a                       # remove the one reference
>>> gc.collect()                # run garbage collection right away
0
>>> d['primary']                # entry was automatically removed
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    d['primary']                # entry was automatically removed
  File "C:/python314/lib/weakref.py", line 46, in __getitem__
    o = self.data[key]()
KeyError: 'primary'

11.7. Werkzeuge für die Arbeit mit Listen

Viele Anforderungen an Datenstrukturen können mit dem integrierten Listentyp erfüllt werden. Manchmal besteht jedoch Bedarf an alternativen Implementierungen mit anderen Leistungskompromissen.

Das Modul array bietet ein array-Objekt, das wie eine Liste ist, aber nur homogene Daten speichert und diese kompakter speichert. Das folgende Beispiel zeigt ein Array von Zahlen, die als zweibyte große vorzeichenlose Binärzahlen (Typ-Code "H") gespeichert sind, anstatt der üblichen 16 Bytes pro Eintrag für reguläre Listen von Python-Integer-Objekten.

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

Das Modul collections bietet ein deque-Objekt, das wie eine Liste ist, mit schnelleren Anhänge- und Entnahmeoperationen von der linken Seite, aber langsameren Abfragen in der Mitte. Diese Objekte eignen sich gut für die Implementierung von Warteschlangen und Breitensuche-Algorithmen für Bäume.

>>> 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 Listenimplementierungen bietet die Bibliothek auch andere Werkzeuge, wie das Modul bisect mit Funktionen zur Manipulation sortierter Listen.

>>> 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 Modul heapq bietet Funktionen zur Implementierung von Heaps basierend auf regulären Listen. Der kleinste Wert befindet sich immer an Position Null. Dies ist nützlich für Anwendungen, die wiederholt auf das kleinste Element zugreifen, aber keine vollständige Sortierung der Liste durchführen möchten.

>>> from heapq import heapify, heappop, heappush
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> heapify(data)                      # rearrange the list into heap order
>>> heappush(data, -5)                 # add a new entry
>>> [heappop(data) for i in range(3)]  # fetch the three smallest entries
[-5, 0, 1]

11.8. Dezimal-Gleitkomma-Arithmetik

Das Modul decimal bietet einen Decimal-Datentyp für Dezimal-Gleitkomma-Arithmetik. Verglichen mit der integrierten float-Implementierung von Binär-Gleitkommazahlen ist die Klasse besonders hilfreich für:

  • Finanzanwendungen und andere Verwendungszwecke, die eine exakte Dezimaldarstellung erfordern,

  • Kontrolle über die Genauigkeit,

  • Kontrolle über die Rundung, um gesetzliche oder behördliche Anforderungen zu erfüllen,

  • Nachverfolgung von signifikanten Dezimalstellen oder

  • Anwendungen, bei denen der Benutzer erwartet, dass die Ergebnisse mit von Hand durchgeführten Berechnungen übereinstimmen.

Beispielsweise ergibt die Berechnung einer 5%igen Steuer auf eine 70-Cent-Telefongebühr unterschiedliche Ergebnisse in Dezimal-Gleitkomma- und Binär-Gleitkomma-Arithmetik. 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 eine nachgestellte Null und leitet automatisch eine Vier-Stellen-Signifikanz von Multiplikatoren mit Zwei-Stellen-Signifikanz ab. Decimal reproduziert die Arithmetik wie von Hand und vermeidet Probleme, die auftreten können, wenn Binär-Gleitkommazahlen Dezimalmengen nicht exakt darstellen können.

Die exakte Darstellung ermöglicht es der Klasse Decimal, Modulo-Berechnungen und Gleichheitstests durchzuführen, die für Binär-Gleitkommazahlen ungeeignet sind.

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

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

Das Modul decimal bietet Arithmetik mit der benötigten Genauigkeit.

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