Die Tage sind gezählt: End of Life für Python 2
Der Support für Python 2 endet 2019, und es ist höchste Zeit für Entwickler, Maßnahmen zum Umstieg zu ergreifen.
- Rainer Grimm
Nach langer Zeit ist es nun soweit: Die Unterstützung von Python 2 wird eingestellt. Am 3. Dezember 2008 erblickte Python 3 das Licht der Welt. Gut zehn Jahre bestand die Koexistenz der inkompatiblen Versionen Python 2 und Python 3. Dieser unglückliche Zwischenzustand neigt sich dem Ende zu: Wie 2015 angekündigt, endet der Support für Python 2 am 31. Dezember 2019.
Python 2 oder Python 3, das ist die Frage
Die jahrelang andauernde doppelte Unterstützung für beide Python-Versionen wirft die Frage auf, welche Strategien sich entwickelt haben, mit dem Zwischenzustand umzugehen. Kurz gesagt, kamen alle denkbaren Strategien zum Einsatz. Viele haben bestehende Projekte einfach in Python 2 weitergeführt. Entwickler haben teilweise sogar neue Projekte in Python 2 begonnen. Dafür gab es viele Gründe: Zum einen sprachen sie fließend Python 2, zum anderen waren notwendige Bibliotheken noch nicht nach Python 3 portiert. Zudem haben Kunden durchaus Python-2-Code verlangt. Manche Teams haben einfach die Codebasis in Python 2 und Python 3 gesplittet und mussten beide Versionen pflegen.
Zwar sind Python 2 und Python 3 inkompatibel, es ist jedoch durchaus möglich, Python-Code umzuschreiben, sodass er sowohl mit Python 2 als auch mit Python 3 arbeitet. Für diesen Schritt helfen die Skripte futurize und modernize. Letzteres ist bei der Änderung des Python-Codes konservativer als ersteres. Für die Modifikation müssen Projekte zwei Voraussetzung mitbringen: Eine Testabdeckung und Code, der auf Python 2.7 migriert ist.
Der Spickzettel "Cheat Sheet: Writing Python 2-3 compatible code" beschreibt im Detail, welche syntaktischen Unterschiede zwischen Python 2 und Python 3 bestehen und wie sich Python-Code schreiben lässt, den sowohl Python 2 als auch Python 3 unterstützt.
Migration von Python 2 nach Python 3
Zum Portieren von Python-2-Code nach Python 3 zeichnet sich ein klar definierter Pfad ab (s. Abb. 1), wobei Entwickler nach jedem Schritt den Code testen und Probleme beseitigen müssen.
Folgende Codezeilen sollen als Beispiel für die Migration von Python 2 nach 3 dienen. Alle Zeilen des Beispiels verwenden funktionale Komponenten von Python, da sich bei diesen Built-in-Funktionen einige Veränderungen vollzogen haben.
print "sum of the integers: " ,
apply(lambda a,b,c: a+b+c , (2, 3, 4))
print "factorial of 10 :",
reduce(lambda x,y: x*y, range(1,1 1) )
print "titles in text: ", filter( lambda word: word.istitle(),
"This is a long Test".split())
print "titles in text: ",
[ word for word in "This is a long Test".split()
if word.istitle()]
Die erste Funktion berechnet die Summe der drei Zahlen 2, 3 und 4, indem sie die Argumente auf die Lambda-Funktion anwendet. Die Built-in-Funktion reduce() reduziert sukzessive die Liste aller Zahlen von 1 bis einschließlich 10, indem sie das Ergebnis der letzten Multiplikation mit der nächsten Zahl aus der Sequenz multipliziert. Die letzten zwei Funktionen filtern aus dem String alle Wörter heraus, die mit einem Großbuchstaben beginnen. Der Code funktioniert bereits unter Python 2.6, sodass nur noch die Schritte 3 und 4 für die Portierung zu vollziehen sind.
Ein Aufruf des Python-2.7-Interpreters mit der Option -3 zeigt die Inkompatibilitäten zur Version 3 (s. Abb. 2): Sowohl apply() als auch reduce() sind in Python 3 keine Built-in-Funktionen mehr.
Der Code ist schnell repariert, damit die Deprecation-Warnungen unterbleiben:
print "sum of the integers: " ,
(lambda a,b,c: a+b+c)(*(2, 3, 4))
import functools
print "factorial of 10 :",
functools.reduce(lambda x,y: x*y, range(1, 11) )
print "titles in text: ",
filter( lambda word: word.istitle(),
"This is a long Test".split())
print "titles in text: ",
[ word for word in "This is a long Test".split()
if word.istitle()]
Das Script 2to3.py (s. Abb. 3).erweist sich bei der Korrektur des Python-2-Codes als hilfreich, denn es erzeugt im letzten Schritt automatisch Code für Python 3. Dazu bietet das Tool mehrere Optionen an.
Der direkte Weg besteht darin, die Ursprungsdatei zu überschreiben: python <path to 2to3.py> port.py -w. Das Ergebnis ist der nach Python 3 portierte Quellcode. Interessanterweise hat der Codegenerator den filter()-Ausdruck durch eine äquivalente List-Comprehension ersetzt:
print("sum of the integers: " ,
(lambda a,b,c: a+b+c)(*(2,3,4)))
import functools
print("factorial of 10 :",
functools.reduce(lambda x,y: x*y, list(range(1,11)) ))
print("titles in text: ",
[word for word in "This is a long Test".split()
if word.istitle()])
print("titles in text: ",
[word for word in "This is a long Test".split()
if word.istitle()])
Python 3: Die neuen Features
Freilich birgt die Migration von Python 2 nach Python 3 Fallstricke und Schwächen. Im Rahmen der Portierung sollten Entwickler sich mit den neuen Features von Python 3 vertraut machen.
print ist mit Python 3 eine Funktion. Die neue Syntax ist allgemein folgende:
print(*args, sep = " ",
end = "\n",
file = sys.stdout,
flush = True)
Dabei bezeichnet args die Argumente, sep den dazwischen liegenden Separator, end das Zeilenendzeichen, file das Ausgabemedium und flush den Puffer. Die folgende Tabelle stellt die syntaktischen Veränderungen der print-Funktion inklusive ihrer Standardwerte gegenüber.
Folgende Tabelle zeigt die Unterschiede für print in Python 2 und Python 3:
Beispiel | Python 2.x | Python 3.x |
Allgemeine Form | print "x= ", 5 | print("x= ", 5) |
Zeilenumbruch | print() | |
Unterdürcken des Zeilenumbruchs | print x, | print(x, end="") |
Unterdrücken des Zeilenumbruchs ohne Leerzeichen | print(1, 2, 3, 4, 5, sep = "") | |
Umleitung der Ausgabe | print >> sys.stderr, "error" | print("error", file = sys.stderr) |
Der Vorteil der neuen Version offenbart sich aber erst auf den zweiten Blick, denn die print-Funktion lässt sich neuerdings überladen: Das Listing zeigt eine print-Funktion, die sowohl in die Standardausgabe als auch in eine Logdatei schreibt. Dazu instrumentalisiert sie die Built-in-Funktion __builtins__.print.
import sys
def print(*args, sep = " ", end = "\n",
file = sys.stdout, flush = True):
__builtins__.print(*args, sep = sep, end = end,
file = file, flush = flush)
__builtins__.print(*args, sep = sep, end = end,
file = open("log.txt", "a"))
Der größte Unterschied von Python 2 zu Python 3 findet sich in den Strings. Mussten Programmierer in Python 2 Strings noch explizit als Unicode deklarieren (u"unicode string"), bestehen Strings in Python 3 automatisch aus Unicode-Zeichen ("unicode string"). Python 3 kennt Text und (binäre) Daten anstelle von Unicode-Strings und 8-Bit Strings. Binäre Daten werden in Python 3 explizit durch "binary data" definiert. Text ist Unicode, enkodierter Unicode steht hingegen für binäre Daten. Der Datentyp, der Text beinhaltet, ist "str", der Datentyp, der Daten beinhaltet ist "bytes".
Die genauen Details zu Strings in Python 2 und Python 3 lassen sich im Unicode-How-to nachlesen.
Um Datentypen zu konvertieren, existieren die Funktionen str.encode() und bytes.decode(). Die Umwandlung benötigen Entwickler in Python 3, wenn sie beide Datentypen verwenden, da keine implizite Typkonvertierung mehr stattfindet.
Mit sogenannten Function Annotations lassen sich in Python 3 die Metadaten an eine Funktion binden. Letztere lässt sich im zweiten Schritt zusätzlich mit Dekoratoren versehen, die automatisch aus den Metadaten eine Dokumentation erzeugen oder die Typen zur Laufzeit prüfen. Die äquivalenten Funktionen sumOrig() und sumMeta() zeigen die Funktionsdeklaration mit und ohne Metadaten. Letztere finden sich in der zweiten Funktion zur Signatur und zum Rückgabewert. Die Metadaten lassen sich mit dem Funktionsattribut __annotations__ referenzieren.
Notwendige Aufräumarbeiten
Python 3 bringt nicht nur neue Feature mit, sondern räumt zusätzlich mit Altlassen von Python 2 auf. Die Bereinigungen betreffen Bibliotheken, die
- entfernt wurden,
- gemäß dem Python Style Guide klein geschrieben werden,
- neu in Pakete verpackt wurden oder
- in einer C- und einer Python-Implementierung koexistieren.
Das bekannte Python-Idiom, erst die schnelle C-Implementierung eines Moduls zu importieren und im Fehlerfall auf die Python-Implementierung zurückzugreifen, ist nicht mehr notwendig. Python kümmert sich automatisch darum.
try:
import cPickle as pickle
except ImportError:
import pickle
Genaueres zu den Änderungen der Standardbibliothek ist in einer Übersicht zu den Neuerungen zu finden.
Es gibt weitere Punkte, die das Schreiben von Code in Python 3 vereinfachen. Entwickler müssen bei kooperativen super-Aufrufen nicht mehr die Instanz der Klasse und den Klassennamen nennen. Python 3 kennt nur New-Style-Klassen, sodass das lästige Ableiten von object nicht mehr notwendig ist, um die neueren Features von Python anzusprechen.
Das automatische Evaluieren der Eingabe mit dem Befehl input() ist überflüssig, da die Eingabe in Python 3 als String zur Verfügung steht. Damit hat sich ein großes Sicherheitsloch in Python geschlossen. Konsequenterweise heißt raw_input() nun input().
Sinn und Zweck von Python 2.7 ist es, den Umstieg auf die Version 3 zu vereinfachen. Daher hat das Projekt viele Features von Python 3.0 auf Python 2.7 zurückportiert.
Rückportierungen auf Python 2
Der Kontext-Manager mit with ist ein wichtiges neues Feature, das mit Python 2.6 zur Verfügung steht. Eine Ressource wie eine Datei, ein Socket oder ein Mutex bindet Python automatisch beim Eintritt in den with-Block und gibt sie beim Austritt wieder frei. C++-Programmierer mag das Idiom an Resource Acquisition Is Initialization (RAII) erinnern.
Das with-Statement verhält sich aus Anwendersicht wie ein try ... finally, da sowohl der try-Block als auch der finally-Block immer zur Ausführung kommen. Allerdings geschieht das ohne explizite Ausnahmebehandlung.
In einem with-Block lässt sich jedes Objekt verwenden, das das Kontextmanagementprotokoll anbietet und damit die internen Methoden __enter()__ und __exit()__ besitzt. Beim Eintritt in den with-Block ruft Python die __enter()__- und beim Austritt _exit()__-Methode auf. Das Dateiobjekt bringt die passenden Methoden von Haus aus mit.
Ressourcen-Management lässt sich zudem eigenhändig durch die Methoden __enter()__ und __exit()__ implementieren. Wem das manuelle Schreiben zu viel Arbeit ist, kann den Decorator contextmanager aus der Bibliothek contextlib verwenden, um vom Kontextmanagement zu profitieren. Weitere Anwendungsfälle sind im Python Enhancement Proposal (PEP) 0343 zu finden.
with open('/etc/passwd', 'r') as file:
for line in file:
print line,
# file is automatically closed
Die wohl größte syntaktische Erweiterung vollzieht sich in Python 2.6 mit der Einführung von abstrakten Basisklassen. Ob ein Objekt sich in einem Kontext verwenden lässt, hing bisher von den Merkmalen des Objekts ab und nicht von dessen formaler Schnittstellenspezifikation.
Das Idiom trägt den Namen Duck-Typing – frei nach dem Gedicht von James Whitcomb Riley: "When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck".
Sobald eine Klasse eine abstrakte Methode besitzt, wird sie zur abstrakten Basisklasse und lässt sich nicht instanziieren. Von ihr abgeleitete Klassen lassen sich nur erzeugen, wenn sie die abstrakten Methoden implementieren. Abstrakte Basisklassen in Python verhalten sich ähnlich wie in C++, insbesondere dürfen abstrakte Methoden eine Implementierung enthalten.
Daneben kennt Python auch abstrakte Properties. Python 3 verwendet abstrakte Basisklassen in den Modulen numbers und collections.
Es steht noch die Antwort auf die Frage aus, wie eine Klasse zur abstrakten Klasse wird: Sie benötigt die Metaklasse ABCMeta. Daraufhin lassen sich die Methoden als @abstractmethod beziehungsweise Properties als @abstractproperty mit den jeweiligen Decorators deklarieren. Der Einsatz abstrakter Basisklassen bedeutet darüber hinaus, dass in die dynamisch typisierende Sprache die statische Typisierung einzieht.
Parallele Arbeit
Pythons Antwort auf Multiprozessor-Architekturen ist die neue Bibliothek multiprocessing. Sie imitiert das bekannte Python-Modul threading, erzeugt jedoch statt eines Thread einen Prozess – und zwar plattformunabhängig. Das Multiprocessing-Modul war notwendig, da in der Standardimplementierung CPython nur ein Thread im Interpreter laufen kann. Geschuldet ist das Verhalten dem sogenannten Global Interpreter Lock (GIL).
Fazit
Die Python-Community hat Workflows beschrieben und Werkzeuge bereitgestellt, um den Umstieg von
Python 2 auf Python 3 deutlich zu vereinfachen. Die typische Migrationsstrategie sollte – wie in Abbildung 1 zusammengefasst – mit einer Testabdeckung beginnen und dank dem Werkzeug 2to3 mit der automatischen Codemigration enden.
Wer die Migrationsstrategie plant und konsequent im Laufe des Jahres umsetzt, sollte keine Hindernisse im Lauf der benötigten Migration von Python 2 auf Python 3 stoßen. Ab dem Jahr 2020 ist Python 2 Geschichte und ausschließlich Python 3 erlaubt.
Rainer Grimm
ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem heise-Developer-Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.
(rme)