Neues in Python 3.4

Seite 2: Statistik, generische Funktionen und Pfade

Inhaltsverzeichnis

Es gibt noch zwei weitere Varianten: OrderedEnum, in der die Mitglieder des Enums eine feste Reihenfolge haben, und DuplicateFreeEnum, die garantiert, dass jedes Mitglied einen einzigartigen Wert hat. Zudem steht für enum eine funktionale API zur Verfügung.

Letztere ist bewusst einfach gehalten, um das Verhalten der Enums erwartbar und leicht erlernbar zu gestalten. Im Verlauf der Entwicklung des Features sind einige Vorschläge verworfen worden – beispielsweise war angedacht, den Mitgliedern von Enums automatisch Werte zuzuweisen. Diese Ideen lehnte man allerdings ab, da die Implementierung sonst "zu magisch" geworden wäre. Zudem verstoße es gegen die Python-Regel "Explicit is better then implicit".

Während das bisherige math-Modul im Wesentlichen die Funktionen der C-Bibliothek math.h im Python-Gewand angeboten hat, geht statistics darüber hinaus. Es bietet einige grundlegende mathematische Funktionen, die häufig im Zusammenhang mit statistischen Datenauswertungen auftreten. Manche der bereitgestellten Funktionen erscheinen trivial selbst zu implementieren zu sein – mean() etwa, die das arithmetische Mittel aus einer Menge Zahlen bestimmt. Durch das Benutzen der Funktionen des statistics-Moduls kann man aber sichergehen, dass das Programm fehlerhafte Eingaben als solche erkennt (und ein StatisticsError wirft). Zudem garantiert statistics, dass nicht nur int und float, sondern auch der dezimale Zahlentyp decimal.Decimal und der Typ der rationalen Zahlen fractions.Fractions korrekt behandelt werden. Damit hat Python das Rüstzeug beisammen, um mit wenig Aufwand beispielsweise Finanzdaten auswerten zu können, da dort keine Fließkommazahlen vorgesehen sind.

Neben dem bereits erwähnten arithmetischen Mittel stellt statistics Funktionen für weitere Mittel- oder Zentralwertbildungen bereit, darunter Modus, sowie eine Reihe Funktionen zur Ermittlung des Median. Darüber hinaus bietet es Optionen zur Berechnung von Varianzen und Standardabweichungen.

Der Funktionsumfang des Moduls ist noch klein – zuerst sollten häufig benutzte Funktionen aufgenommen werden, die beispielsweise auch grafische Taschenrechner anbieten. Außerdem soll die API klar und verständlich bleiben (ein Grund dafür, warum die Berechnung von linearer Regression noch außen vor bleiben musste – für Funktionssignaturen solcher Art steht der Konsens unter den Python-Entwicklern noch aus). Das Ziel von statistics ist es nicht, mit etablierten Fremdbibliotheken wie pandas oder numpy zu konkurrieren. Vielmehr soll sie es Python-Nutzern ermöglichen, einfache, aber häufig benutzte statistische Funktionen zu nutzen, ohne sich mit einem großen statistischen Framework zu beschäftigen oder das Rad jedes mal (womöglich fehlerhaft) neu zu erfinden.

Die Bezeichnung "single dispatch generic functions" hört sich nach komplexer Theorie an – dahinter steckt aber ein ganz einfaches Konzept. Zum Erklären soll ein Ausschnitt aus dem Quellcode von Python dienen, genauer gesagt aus dem Modul ast in der Standardbibliothek:

def _convert(node):
if isinstance(node, (Str, Bytes)):
return node.s
elif isinstance(node, Num):
return node.n
elif isinstance(node, Tuple):
return tuple(map(_convert, node.elts))
elif isinstance(node, List):
return list(map(_convert, node.elts))
# ...
raise ValueError('malformed node or string: ' + repr(node))

Abgesehen davon, dass manch einer ein switch-Statement vermissen mag, könnte verwundern, dass hier explizit Typen überprüft werden. So etwas ist in Python meist nicht notwendig, da Pythons Typsystem mit Duck Typing eine andere Philosophie verfolgt. Manchmal jedoch kommt man nicht darum herum, das Verhalten einer Funktion vom Typ seines Eingabewerts abhängig zu machen. In diesem Fall ist es vollkommen in Ordnung, wie im obigen Beispiel mit isinstance() Fallunterscheidungen in die Funktion zu übernehmen.

Bei Plug-in-Systemen könnte nun die Fähigkeit gefragt sein, benutzerdefinierte Klassen von convert() handhaben zu lassen – praktischerweise ohne die Funktion selbst verändern zu müssen. Eine solche Fähigkeit stellt singledispatch zur Verfügung. Obiges Beispiel sähe damit so aus:

from functools import singledispatch

@singledispatch
def _convert(node):
raise ValueError('malformed node or string: ' + repr(node))

@_convert.register(Str)
@_convert.register(Bytes)
def _(node):
return node.s

@_convert.register(Num)
def _(node):
return node.n

@_convert.register(Tuple)
def _(node):
return tuple(map(_convert, node.elts))

@_convert.register(List)
def _(node):
return list(map(_convert, node.elts))

Zwar sieht der Code nun nicht unbedingt lesbarer aus, doch ist der wesentliche Unterschied leicht erkennbar: Statt die Logik für sämtliche Typen innerhalb einer Funktion zu behandeln, wird nun pro Typ eine eigene Funktion deklariert – nach außen treten sie jedoch als eine einzige Funktion auf. Dadurch lässt sich die Fähigkeit einer Funktion modular erweitern.

Wie der Name schon andeutet, ist es mit singledispatch nur möglich, für das erste Argument spezialisierte Funktionen bereitzustellen. Das hat ganz praktische Gründe: Einerseits ist die Handhabung einer beliebigen Anzahl von Typen recht komplex, andererseits scheint der häufigste Fall, den man in bestehendem Code antrifft, eben nur das Single Dispatching zu verlangen.

Üblicherweise werden Dateipfade wie /usr/bin/python als String dargestellt und behandelt. Diese augenscheinlich naheliegende Lösung weist allerdings ein paar Probleme auf. Als offensichtlichstes sind Unterschiede zwischen Unix und Windows zu nennen, was Separatoren, Groß- und Kleinschreibung oder Laufwerksbezeichnungen angeht. Zudem sind die im Modul os.path angebotenen Möglichkeiten zur Manipulation von Dateipfaden zwar recht vollständig, aber etwas umständlich und – für Python-Verhältnisse – nicht übersichtlich lesbar.

pathlib bietet eine objektorientierte Syntax mit einer aufgeräumten API an, die ein bequemes Arbeiten mit Dateipfaden ermöglicht. Als besonderes Schmankerl unterstützt sie den /-Operator als Separator, wie das Beispiel an der Python-Konsole zeigt:

>>> usr_path = Path("/home")
>>> usr_path / "bin"
PosixPath('/home/bin')
>>> str(usr_path / "lib")
'/home/lib'

Je nach Plattform fällt die Wahl der Klasse automatisch auf PosixPath oder WindowsPath. Man kann diese Klassen auch explizit instantiieren – allerdings nur, wenn das jeweilige System damit kompatibel ist.

pathlib bietet einige Fähigkeiten, die über die von os.path hinausgehen. So enthält sie etwa Befehle zum Öffnen einer Datei zum Lesen oder Schreiben:

my_path = Path.cwd() / "out.txt"
with my_path.open("w", encoding="utf-8") as f:
f.write("Hello World!\n")

Auf die Komponenten des Pfads lässt sich über die Eigenschaft parts einzeln zugreifen. Zusammen mit Pythons Unpacking-Syntax können Entwickler Dateipfade damit einfach unterteilen:

>>> root, *dirs, file = Path("/usr/bin/python").parts
>>> root, dirs, file
('/', ['usr', 'bin'], 'python')

Für Unix-artiges Pattern-Matching lässt sich die Methode glob() verwenden:

>>> [p.stem for p in Path(".").glob("*.py")]
['python-config', 'python-gdb', 'setup']

Wenn nicht auf Funktionen des Betriebssystems zurückgegriffen werden muss (wie beim Öffnen von Dateien oder beim Auflösen von Symlinks), lässt sich die Klasse PurePath benutzen. Auch für sie gibt es zwei spezifische Implementierungen, PurePosixPath und PureWindowsPath. Da nur symbolische Pfadmanipulationen erlaubt sind, kann man sie auch auf inkompatiblen Systemen nutzen.

Das pathlib-Modul mag eine willkommene Neuerung für Systemadministratoren sein, bei denen Python-Skripte gängige Aufgaben automatisieren. Mit seiner objektorientierten API lassen sich Skripte auf pathlib-Basis gut modularisieren. Die API ist "pythonic", das heißt, sie entspricht den Erwartungen erfahrener Python-Benutzer, insbesondere im Hinblick auf die Lesbarkeit.