Neues in Python 3.4

Viel Neues in der Standardbibliothek, einige Verbesserungen unter der Haube, keine syntaktischen Änderungen – so lässt sich das Ergebnis des 18 Monate dauernden Entwicklungszyklus für Python 3.4 zusammenfassen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 29 Min.
Von
  • Christian Schramm
Inhaltsverzeichnis

Viel Neues in der Standardbibliothek, einige Verbesserungen unter der Haube, keine syntaktischen Änderungen – so lässt sich das Ergebnis des 18 Monate dauernden Entwicklungszyklus für Python 3.4 zusammenfassen.

Seit Mitte März steht die neue Version der Programmiersprache Python bereit. Die Highlights der Veröffentlichung sind die neuen Module asyncio, enum und pathlib, zudem liegt ein großer Schwerpunkt auf dem Thema Sicherheit.

So umfangreich Pythons Standardbibliothek ist, sie kann (und will) nicht alle Funktionen abdecken. Daher hat sich PyPi, analog zu Perls CPAN oder Rubys RubyGem, seit vielen Jahren als offizielle Sammelstelle für Python-Software etabliert. Der ursprüngliche Paketmanager easy_install wies allerdings einige Schwächen auf – am meisten vermissen seine Nutzer wohl die Fähigkeit, Pakete wieder zu deinstallieren. Da die Probleme von easy_install aufgrund von Design-Entscheidungen schlecht zu beheben waren, wurde es vor einiger Zeit vom neueren Paketmanager pip abgelöst.

Bislang war man allerdings gezwungen, pip von Hand herunterzuladen und zu installieren. Das war nicht nur etwas umständlich, sondern zudem ein potenzielles Sicherheitsrisiko. Da pip aber unabhängig von der Standardbibliothek entwickelt und in einem eigenen Release-Zyklus veröffentlicht wird, wäre eine direkte Übernahme in die Standardbibliothek nicht praktikabel. Daher wurde mit ensurepip ein Modul aufgenommen, mit dem sich pip in die aktuelle Python-Installation einfügen oder auf den neuesten Stand bringen lässt. Normalerweise muss der Endnutzer die Bibliothek allerdings nicht manuell benutzen – standardmäßig wird sie beim Kompilieren von Python gleich mit erstellt. Ebenso ist pip in einer virtuellen, mit pyenv erstellten Python-Umgebung sofort verfügbar.

Die Benutzung von pip ist denkbar einfach. Ein Paket wie Django lässt sich mit

$ pip install Django

installieren. Die Deinstallation ist mit

$ pip uninstall Django

ebenso selbsterklärend. Das Benutzerhandbuch zu pip beschreibt weitere Befehle und Optionen des Paketmanagers.

Das Gros der Änderungen ist in Pythons umfangreicher Standardbibliothek zu finden. Die Entwickler verfolgen das Ziel, dass sich typische Aufgaben in der Sprache ohne Fremdbibliotheken lösen lassen.

Um in die Standardbibliothek aufgenommen zu werden, muss ein Modul hohen Anforderungen genügen, denn es ist im Nachhinein nur noch schwer möglich, die API zu ändern. Da es andererseits oft schwierig ist, bevor ein Modul in der Standardbibliothek öffentlich getestet werden kann, vorauszusagen, was sinnvolle Schnittstellen sind, existiert seit Python 3.3 das Konzept der sogenannten Provisional API, also einer vorläufigen API.

Module, deren API mit provisional gekennzeichnet ist, geben noch keine Garantien bezüglich der Stabilität ihrer Schnittstellen – sie sollten folglich nur unter Vorbehalt in Produktions-Software zum Einsatz kommen. Im Allgemeinen lässt sich allerdings davon ausgehen, dass die meisten so gekennzeichneten Module ihre API nicht oder nur minimal verändern, bis sie – aller Voraussicht nach in der jeweils nächsten Version – als stabil deklariert werden. Im Folgenden sollen neue oder stark verbesserte Module der Standardbibliothek Thema sein.

Als Enum bezeichnet man in C/C++ und vielen anderen Sprachen eine Menge von (meist verwandten) Bezeichnungen, die sich durch Zuordnung eines konstanten Werts miteinander vergleichen lassen. Programmierer anderer Sprachen wird überraschen, dass Python über zwanzig Jahre lang nicht über ein Enum-Konstrukt verfügt hat. Erfahrene Python-Entwickler hingegen mögen gleichermaßen verblüfft darüber sein, dass es Menschen gibt, die dieses Feature in Python vermisst haben, da sich längst Konventionen herausgebildet haben, um ohne Enum auszukommen. Statt, wie in C, zu schreiben:

typedef enum {
DAY_MONDAY = 1,
DAY_TUESDAY = 2,
DAY_WEDNESDAY = 4,
/* ... */
} Day;

void handle_days(Day days) {
if (DAY_MONDAY & days) {
/* ...*/
}
}

void main() {
set_flags(DAY_MONDAY | DAY_WEDNESDAY);
}

nutzt man in Python typischerweise einfach Strings:

def handle_days(*days):
if "Monday" in days:
# ...

handle_days("Monday", "Wednesday")

Insbesondere Bindings zu C/C++-Bibliotheken nutzen eine andere Variante: Sie packen die Definitionen der Flags in ein separates Modul.

Inhalt day.py:

MONDAY = 1,
TUESDAY = 2,
WEDNESDAY = 4,

Inhalt main.py:

import day

def handle_days(days):
# ...

handle_days(day.MONDAY | day.WEDNESDAY)

Wieder andere Konventionen benutzen eine Klasse oder ein dict, um den Definitionen einen eigenen Namensraum zu geben.

Diese Möglichkeiten haben allerdings unterschiedliche Nachteile. Zwar sind Vergleiche von Strings in Python meist recht schnell, da oft nur ein Hash-Wert zu vergleichen ist – allerdings kann der Python- Parser Tippfehler in Strings nicht syntaktisch erkennen. Auch Syntax-Highlighter und Code-Vervollständigung in einer IDE sind für diese Variante nur eingeschränkt brauchbar. Zudem lassen sich die Strings nicht durch bitweise Operatoren (wie & und |) miteinander kombinieren, was die Kompatibilität mit C/C++-Bibliotheken erschwert.

Die Konstanten in einem anderen Modul unterzubringen (oder eine Klasse oder dict zu verwenden) vermeidet derartige Probleme. Dennoch haben diese Varianten eine subtilere, aber für die Python- Entwickler dennoch entscheidende Schwachstelle: Enum-Definitionen sind semantisch keine Zahlen. Ein Beispiel zeigt das besonders deutlich: Ein Wochentag ist keine ganze Zahl – dennoch tut man hier so, als ob. Operationen und Funktionen für Zahlen ergeben für Wochentage keinen rechten Sinn (was ist Montag plus Mittwoch?), und das gilt im Allgemeinen für die meisten Enums. Sie benötigen eine zugeordnete Zahl für die Identität, aber sie bedeuten nicht das, was der Zahlenwert darstellt.

Daher haben sich die Python-Entwickler entschlossen, eine spezielle Klasse namens Enum in der Standardbibliothek bereitzustellen, die derartige Probleme umgeht:

from enum import Enum

class Day(Enum):
Monday = 1
Tuesday = 2
Wednesday = 4
# ...

Sie verhalten sich nicht wie Zahlen, sondern unterstützen nur Vergleiche:

>>> Day.Monday is not Day.Wednesday
True
>>> Day.Monday is Day.Tuesday
False

Wenn Kompatibilität mit Integer-basierten Enums wie in C/C++ nötig ist, lässt sich hingegen IntEnum verwenden. Hier erben die Enum-Mitglieder von int:

class FileFlags(enum.IntEnum):
READ = 1
WRITE = 2
TEMP = 4

Auf Mitglieder von IntEnum lassen sich die bekannten bitweisen Operatoren anwenden:

>>> bool(FileFlags.READ & (FileFlags.WRITE | FileFlags.TEMP))
False

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.

Schon im Jahr 1999 stellte Dan Kegel das C10K-Problem vor: Wie skaliert man Webserver, sodass sie 10.000 Verbindungen gleichzeitig bedienen können? Heutzutage sind die Fähigkeiten der Server und auch die Anforderungen nochmals um einige Größenordnungen gewachsen, sodass das Problem, hochskalierende Webserver zu schreiben, aktueller denn je ist. Das traditionelle Modell von Webservern wie Apache, für jede Anfrage einen eigenen Thread (oder gar Prozess) zu eröffnen und nach Beendigung wieder zu schließen, stößt für sehr viele Verbindungen an seine Grenzen.

Das die Methode schlecht skaliert, liegt einerseits daran, dass jeder Thread einen eigenen Stack-Speicher reserviert, was die Speicheranforderungen linear mit den Verbindungen ansteigen lässt. Zudem wird das Umschalten zwischen Threads vom Betriebssystemkernel verwaltet, was einen großen Aufwand erfordert.

Viele moderne Frameworks vermeiden daher Kernel-Threads und implementieren asynchrone Modelle im User Space. Populäre hochskalierende Frameworks in Python umfassen Twisted und Tornado oder das auf greenlet aufbauende gevent. Bisher fehlte allerdings eine Unterstützung für solche Modelle in Pythons Standardbibliothek - wenn man vom veralteten asyncore einmal absieht.

Angesichts der Bedeutung von Webservern ist die wohl wichtigste Neuerung in Python 3.4 das neu hinzugekommene Modul asyncio. Es bietet ein vollständiges Framework für nichtblockierende und
asynchrone Ausführung. Es wurde jedoch mit einem Fokus auf Erweiterbarkeit geschrieben, sodass sich leicht auf die Funktionen anderer Frameworks zugreifen lässt. asyncio bietet im Vergleich zu Frameworks wie Node.js einen wesentlichen Unterschied: Statt auf Callbacks setzt [I]asyncio[/i] auf Koroutinen oder Futures – Callbacks nutzender Code wird allerdings unterstützt. Als Synchronisationspunkte dienen yield from-Ausdrücke. asyncio bringt einen eigenen Mainloop mit, der unter Linux mit epoll, unter BSD und Mac OS X mit kqueue und unter Windows mit IOCP implementiert ist. Hier darf ein "Hello World!"-Beispiel nicht fehlen:

@asyncio.coroutine
def greet_every_second():
while True:
print('Hello World')
yield from asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(greet_every_second())

Im Gegensatz zu Systemen, in denen jedes Mal aufs Neue ein Callback aufgerufen wird, ruft man die Funktion greet_every_second() nur einmal auf, danach bleibt sie aktiv. Bei yield from asyncio.sleep(1) übergibt die Koroutine die Kontrolle an den Mainloop. Nach einer Sekunde setzt Letzterer die Ausführung der Koroutine an eben der Stelle fort. Der Vorteil gegenüber Callbacks ist das Vermeiden sogenannter Pyramids of Doom, also unleserlich ineinander verschachtelter Callbacks. Es ist nicht notwendig, genau zu wissen, was yield from eigentlich macht – es ist fast immer ausreichend, so zu tun, als wäre der Code in Wirklichkeit sequenziell und blockierend geschrieben.

Neben Koroutinen stellt das Modul Futures bereit, die ähnlich wie Twisteds Deferreds gestaltet sind. Futures sind Objekte, die den Status einer asynchron laufenden Aufgabe abbilden und Methoden bereitstellen, um das Resultat abzufragen oder die Aufgabe abzubrechen. Als dritten Datentyp bietet asyncio sogenannte Tasks an – das sind Futures, in die Koroutinen eingebettet sind. Die Terminologie mag anfangs gewöhnungsbedürftig sein (und ist in der Dokumentation nicht besonders ausführlich erklärt), doch die Handhabung ist erstaunlich intuitiv. Diese Datentypen haben nichts Netzwerkspezifisches an sich, sondern bilden nur die asynchrone Laufweise ab. Man könnte sie so beispielsweise für die Spieleprogrammierung nutzen.

Auf der untersten Abstraktionsebene des I/O-Systems sind Transporte und Protokolle angesiedelt. Diese Begriffe sind, wie einige Designentscheidungen von asyncio, aus Twisted übernommen worden.

Während die Transporte für I/O und Pufferung zuständig sind, werden das Parsen der hereinkommenden Daten und die Schreibanfragen von den Protokollen geregelt. Von Haus aus bringt asyncio Transporte für TCP, UDP, SSL und Pipes mit.

Auf einer höheren Abstraktionsebene befindet sich die Stream-API, die viele Implementierungsdetails versteckt. Ein anschauliches Beispiel ist in der offiziellen Dokumentation zu finden. Es demonstriert nicht nur die verhältnismäßig einfache API, sondern auch die gute Lesbarkeit des Koroutinen-Konzepts.

Vermutlich werden viele Python-Nutzer asyncio nicht direkt nutzen, da es sich bewusst auf Basis- Funktionen beschränkt. Beispielsweise ist keine Implementierung des HTTP-Protokolls zu finden. Stattdessen sieht es sich als Grundlage für Webserver und moderne Web-Frameworks. Obwohl es noch jung ist, gibt es bereits auf asyncio aufbauende Projekte, etwa rainfall, das Tornado ähnelt, oder Vase, das von Flask inspiriert ist. Auch Autobahn hat ein asyncio-Backend.

Neben den schon erwähnten Modulen haben die Python-Entwickler in einer ganzen Reihe weiterer Module Verbesserungen vorgenommen. Alle diese Änderungen im Detail aufzulisten würde allerdings den Rahmen des Artikels sprengen, daher werden nun die wichtigsten Verbesserungen in Kürze beschrieben:

Das Serialisierungsformat pickle hat eine Reihe von Optimierungen erfahren (PEP 3154). In der aktuellsten Version des Formats verbrauchen serialisierte Daten weniger Speicherplatz, zudem ist die Serialisierung wie die Deserialisierung schneller. Um Abwärtskompatibilität zu gewährleisten, wird das neue Format nicht standardmäßig benutzt. Wer es verwenden möchte, muss explizit die Version 4 angeben oder pickle.HIGHEST_PROTOCOL verwenden:

with open("hello.pickle", "wb") as f:
pickle.dump("Hello World!", f, pickle.HIGHEST_PROTOCOL)

Neben pickle beherrscht Python ein weiteres Serialisierungsformat namens marshal, das vor allem intern benutzt wird, um kompilierten Python-Bytecode als .pyc-Datei zu speichern. Anders als bei pickle ist nicht garantiert, dass sich mit marshal serialisierte Daten in anderen Python-Versionen korrekt deserialisieren lassen. In Version 3.4 ist das von marshal verwendete Format deutlich kompakter geworden und benötigt so weniger Speicherplatz. Die Einsparungen belaufen sich auf etwa ein Viertel des vorher verwendeten Speichers.

Wie fast alles in Python ist auch das Importverhalten zur Laufzeit änderbar. Nachdem die Entwickler die Importlogik für Python 3.3 als importlib-Modul neu geschrieben hatten, wurde das Modul in der aktuellen Version um die Klasse ModuleSpec erweitert (PEP 0451). Die Klasse bündelt und strukturiert den Status importierbarer Module. Zwar war es auch vorher möglich, eine eigene Importlogik zu implementieren, doch lässt sich mit ModuleSpec einiges an unübersichtlichem Code vermeiden.

Eine ganze Reihe an sicherheitsrelevanten Änderungen hat das Modul ssl erfahren. Neben der Unterstützung von TLSv1.1 und TLSv1.2 gibt es neue Funktionen und Methoden für SSLContext, darunter create_default_context(), das einen SSLContext mit "einer vernünftigen Balance aus Sicherheit und Kompatibilität" erstellt. Es wird angeraten, den Rückgabewert der Funktion zu benutzen, wann immer ein context als Argument der API benötigt wird. Des Weiteren ließ sich die Fähigkeit zum Umgang mit Zertifikaten durch einige neue Funktionen erweitern. Nur für Windows sind die Funktionen enum_certificates() und enum_crls(), die Zertifikate und CRLs vom Windows cert store abfragen können.

Unter Unix unterstützt multiprocessing nun neben fork zwei weitere Arten, einen neuen Prozess zu starten: spawn, das unter Windows die Standardeinstellung ist, und forkserver, in dem ein separater Server-Prozess erstellt wird, von dem sich neue Prozesse per fork abspalten lassen, sobald sie benötigt werden. Die jeweilige Art kann man mit set_start_method() festlegen. Durch diese Änderungen werden potenzielle Sicherheitsprobleme vermieden, die damit zusammenhängen, dass mit fork erstellte Prozesse die Ressourcen des Elternprozesses erben. Zudem kann dessen Einsatz im Zusammenhang mit Multithreading problematisch sein.

Der Speicherverbrauch einer Python-Anwendung lässt sich mit dem neu hinzugekommenem Modul tracemalloc mit wenigen Zeilen Code schnell aufschlüsseln. Anders als die meisten Module der Standardbibliothek lässt es sich unter Umständen von anderen Python-Implementierungen nicht (oder nicht sinnvollerweise) bereitstellen.

CPython (die mit Abstand am meisten benutzte Implementierung von Python) verwendet intern sogenannte Hash Maps – nicht nur für den eingebauten Datentyp dict, sondern für die gesamte Objekt- und Klassenlogik. Der verwendete Hash-Algorithmus ist daher von zentraler Bedeutung. Als veröffentlicht wurde, dass sich Kollisionen in Pythons Hash-Algorithmus zu DoS-Attacken nutzen lassen, entschloss man sich, statt des bisherigen Algorithmus (einer modifizierten Fowler-Noll-Vo-Funktion) den kryptographisch sichereren SipHash-Algorithmus zu verwenden (PEP 456). Er ist ein wenig langsamer, bietet aber zuverlässigen Schutz gegen Kollisionsattacken.

Ebenfalls im Bereich der Sicherheit zu verorten ist die Entscheidung, alle geöffneten Dateihandles als "nichtvererbbar" zu markieren. Bislang erbten alle (beispielsweise mit dem Modul subprocess) erzeugten Subprozesse sämtliche vom Elternprozess geöffneten Datei-Handles, was zu Sicherheitslücken führen kann. Mit dieser Änderung (PEP 446) werden Dateihandles, wie sie etwa open(), os.pipe() oder socket.socket() zurückgeben, standardmäßig nicht an Kindprozesse vererbt. Um ein Datei-Handle explizit als vererbbar zu kennzeichnen, lässt sich die Funktion os.set_inheritable() einsetzen.

Um es Anwendungen zu ermöglichen, einen eigenen Speicher-Allokator zu verwenden, stellt CPython nun eine API für die Speicherverwaltung bereit. Denkbare Einsatzgebiete sind Anwendungen im Embedded-Bereich, die mit eigenen Allokatoren eine effizientere Speichernutzung erreichen wollen. Zudem lassen sich dadurch einfacher Debugging-Werkzeuge entwickeln, die Fehler oder Ineffizienzen in Anwendungen ermitteln können. Das bereits erwähnte Modul tracemalloc ist auf Basis dieser API implementiert.

CPython benutzt als Methode zur Garbage Collection (der automatischen Speicherbereinigung) einfaches Reference Counting. Da sich mehrere Objekte jeweils gegenseitig referenzieren können, bringt CPython außerdem einen Cycle Collector mit, der solcherlei zyklische Referenzen erkennen und mit Hilfe des Moduls gc auflösen kann. Er stieß jedoch an seine Grenzen, wenn während der Finalisierung eines Objekts (typischerweise in der __del__()-Methode) eine neue Referenz zu einem Objekt desselben Referenzzyklus erzeugt wurde. Mit einer Änderung der Logik in der Objektfinalisierung von CPython ist die Limitierung nun beseitigt worden. Dadurch fällt die bisher geltende Regel "Objekte mit __del__() -Methode sollten nicht Teil eines Referenzzyklus sein" weg. Da sie vielen Python-Nutzern nicht gerade geläufig war, mag der eine oder andere mysteriöse Fehler in bestehender Software dadurch behoben worden sein.

Auch an der Performance von CPython arbeiteten die Entwickler. Die Zeit, die der Interpreter zum Starten benötigt,ließ sich um fast ein Drittel reduzieren. Der Grund dafür ist (neben dem bereits erwähnten optimierten marshal-Format) vor allem darin zu finden, dass weniger Module standardmäßig zu laden sind. Auch der Speicherbedarf von CPython kann dadurch in vielen Fällen etwas sinken. Der Dekoder für die (eher selten verwendete) Unicode-Kodierung UTF-32 ist nun drei- bis viermal so schnell. Bereits in Version 3.3 wurden UTF-8- und UTF-16-Enkodierung und -Dekodierung sowie der Speicherverbrauch von Unicode-Strings optimiert. Damit sind die Geschwindigkeitsnachteile, die Python 3 gegenüber Python 2 durch die Umstellung von Byte- auf Unicode-Strings ursprünglich aufwies, weitestgehend ausgeglichen.

Die Adaption von Python 3 – oder vielmehr deren Mangel – bleibt weiter ein Thema in der Python-Community. Zwar sind mittlerweile die meisten der großen Bibliotheken und Toolkits portiert – zu nennen sind vor allem Django und scipy – und auch die großen Linux-Distributionen Ubuntu und Fedora planen für die kommenden Versionen den Umstieg. Viele Unternehmen, die Python einsetzen, berichten jedoch, dass ein Wechsel auf Version 3 nicht vorgesehen sei.

Python-Erfinder Guido van Rossum, der bei Dropbox arbeitet, hat selbst bestätigt, dass man auch dort bis auf Weiteres auf Python 2 setze. Fragen nach einer Version 2.8 hat man in der Python-Entwicklercommunity bisher stets verneint – weder glaube man, dass sich dadurch die Portierung auf Python 3 erleichtern ließe, noch habe jemand Lust, an der alten Codebasis weiterzuentwickeln. Dennoch ist zu erwarten, dass in Zukunft Änderungen wie PEP 461 (Formatierung mit dem %-Operator für byte-Objekte), die die Portierung zwischen den Versionen erleichtern, aufgenommen werden. Außerdem verlängerte man zuletzt die Unterstützung für Python 2.7 um fünf Jahre bis 2020.

Bis zu einer neuen Version dauert es gewöhnlich 18 Monate - man darf also frühestens im September 2015 mit Python 3.5 rechnen. Relativ sicher scheint, dass ein neuer Operator für Matrix-Multiplikationen in die Sprache Einzug halten wird. PEP 465 schlägt dafür das Zeichen @ vor. Guido van Rossum hat angedeutet, dass er den Vorschlag akzeptieren werde, sobald die Community rund um numerische Bibliotheken sich auf Details wie Rechts- oder Linksassoziativität geeinigt habe.

Diese Änderung trägt der stark gestiegenen Bedeutung von Python im wissenschaftlichen Umfeld Rechnung. Weitere Vorschläge, die eine realistische Chance darauf haben, in Python 3.5 umgesetzt zu werden, sind eine generalisierte Unpacking-Syntax für Containertypen, welche die bisherige Syntax von einigen Einschränkungen befreit (PEP 448), und ein spezielles Dictionary, das es erlaubt, den Wert der Schlüssel mit einer Funktion zu transformieren (PEP 455).

In den letzten Jahren hat außerdem die alternative Implementierung PyPy von sich reden gemacht. Sie implementiert Python nicht in C, sondern in Python (zumindest einem Subset davon). Das klingt eigenartig, aber mit seinem Just-In-Time Compiler (JIT) ist PyPy oft deutlich schneller als CPython, wie Benchmarks zeigen. Es gibt aber noch Baustellen, wie das Modul pypy3, das Python-3-Kompatibilität herstellen soll, und numpypy, eine Neu-Implementierung von numpy, die den JIT besser nutzen soll.

Ein weiteres Projekt aus dem PyPy-Umfeld ist CFFI, eine Schnittstelle zwischen Python und C, die sowohl mit CPython als auch mit PyPy funktioniert und dabei robuster und schneller ist als ctypes. Es ist nicht unwahrscheinlich, das in näherer Zukunft eine Aufnahme von CFFI in die Standardbibliothek vorgeschlagen wird.

Noch ein reines Forschungsprojekt ist pypy-stm, dass mit Software Transactional Memory eine Alternative zu locking in Multithread-Umgebungen anbietet. Einer der Vorzüge dieses Modells ist es, den berüchtigten Global Interpreter Lock (GIL) von Python zu beseitigen und somit ein Python-Programm nahezu linear mit den vorhandenen Prozessoren skalieren lassen zu können. Bis dahin ist es allerdings noch ein weiter Weg.

Dass hinter Python kein großes Unternehmen wie Microsoft oder Oracle steht, hat sich für die Sprache nicht als Nachteil erwiesen – ganz im Gegenteil: Der offene Entwicklungsprozess stellt sicher, dass die Python-Entwickler in engem Kontakt zu den Nutzern stehen. Der Spagat zwischen konservativer Stabilität und Anpassung an neue Bedürfnisse ist mit Python 3.4 gelungen. Das neue Modul asyncio etabliert Python als ernsthaften Konkurrenten zu Node.js, pip erleichtert den Umgang mit Fremdbibliotheken, und eine Vielzahl von Änderungen verbessert die Sicherheit von Python-Software. Ob die neuen Features Lockmittel genug sind, um verbliebene Python-2-Nutzer zur Migration zu bewegen, wird sich zeigen. Für den Autor ist asyncio der spannendste Neuzugang und Grund genug, Python 3.4 eine Chance zu geben.

Christian Schramm
begeistert sich für eleganten und lesbaren Programmcode. Er ist selbständiger Softwareentwickler und berät Unternehmen zum Einsatz von Open-Source-Software in der Entwicklung.
(jul)