Second Python Coding Dojo in Vienna

Standard

On Wednesday, September 21, 2011, the next Python Coding Dojo will take place at the monthly meeting of the Python User Group Austria in the Metalab. Python developers of all experience levels and people interested in Python are welcome.

What is a Coding Dojo?

A coding dojo is a meeting, which brings together a number of programmers to work on a programming challenge. It’s mainly about solving the task in a relaxed atmosphere together and to learn new things. A dojo is not a competition, but the focus is on the learning. More information is available at http://codingdojo.org.

Context-Manager in Python

Standard

Beim letzten Python-Coding-Dojo im August war mir aufgefallen, dass man im unittest-Modul der Python-Standardbibliothek Exceptions auf folgende Art testen kann:

    with self.assertRaises(SomeException):
        do_something()

Bisher dachte ich, dass das with-Statement wie das using-Statement in C# funktioniert. Das Äquivalent in C# wäre dann:

    using (this.AssertRaises(typeof(SomeException)))
        DoSomething();

In C# kann das nicht funktionieren, weil die Dispose-Methode des IDisposable-Objekts, das von AssertRaises zurückgegeben würde, keine Referenz auf die Exception, die von DoSomething geworfen wurde, erhält.

Wenn man außerdem weitere Eigenschaften der Exception überprüfen will, kann man in Python Folgendes schreiben:

    with self.assertRaises(SomeException) as cm:
        do_something()
        the_exception = cm.exception
        self.assertEquals(the_exception.error_code, 3)

Für mich war das einmal ein Grund, mich einmal mit dem with-Statement zu beschäftigen, das es erst seit Python 2.5 gibt.

Das with-Statement

Das with-Statement wird verwendet, um vor und nach der Ausführung eines Codeblocks Methoden auf einem Context-Manager auszuführen. Auf diese Art und Weise kann man in Python Ähnliches erreichen wie in C# mit dem using-Muster, z.B.:

    filename = "hello.txt"
    with open(filename, 'w') as file:
        file.writelines(["first line\n", "second line\n"])
        with open(filename, 'r') as f:
            for line in f:
                print line.strip()

In älteren Python-Versionen (älter als 2.5) musste man mit try...finally das File-Objekt selbst schließen:

    filename = "hello.txt"
    file = open(filename, 'w')
    try:
        file.writelines(["first line\n", "second line\n"])
    finally:
        file.close()
    f = open(filename, 'r')
    try:
        for line in f:
        print line.strip()
    finally:
        f.close()

Aber wie funktioniert with eigentlich?

Über Pythons und Enten

Das using-Statement in C# erwartet sich ein Objekt, das die IDisposable-Schnittstelle implementiert. Wenn der dem using-Statement folgende Codeblock verlassen wird, wird die Dispose-Methode auf dem IDisposable-Objekt aufgerufen. Dabei ist es egal, ob im Codeblock eine Exception geworfen wird oder der Codeblock auf normalem Weg verlassen wird.

Interfaces gibt es in Python nicht, aber es gibt Duck-Typing:

“Wenn es wie eine Ente läuft, wie eine Ente schwimmt und wie eine Ente
schnattert, dann ist es eine Ente.”

Das bedeutet, dass der Typ eines Objekts nicht durch die Klasse bestimmt wird, sondern durch die Methoden, die es implementiert. Eine Menge an Methoden, die ein Objekt für ein bestimmtes Feature implementieren muss, heißt Protokoll. Es entspricht somit einem Interface, nur dass man bei einem Protokoll nicht angeben muss, das eine Klasse es implementiert. Solange alle Methoden des Protokolls vorhanden sind, ist alles in Ordnung.

Das with-Statement erwartet sich ein Objekt, das das Context-Management-Protokoll implementiert.

Der Context-Manager

Dieses Objekt wird Context-Manager genannt. Das Context-Management-Protokoll besteht aus zwei Methoden:

  • contextmanager.__enter__()
  • contextmanager.__exit__(exc_type, exc_value, exc_tb)

Methoden, die dafür bestimmt sind, von der Python-Runtime selbst aufgerufen zu werden, haben einen Namen, der mit zwei Unterstrichen (__) beginnt und mit zwei Unterstrichen endet.

Die Methode __enter__ wird aufgerufen, bevor der Codeblock aufgerufen wird, die Methode __exit__ danach.

Ein Beispiel:

    class HelloContext:
        def __init__(self, text):
            self._text = text
        
        def __enter__(self):
            print "Enter"
            return self
            
        def __exit__(self, exc_type, exc_val, exc_tb):
            print "Exit"
            return False
            
        def show(self):
            print self._text
            
    with HelloContext("Hello, world!") as hc:
        hc.show()

Zugegeben, ein wenig viel Code für “Hello, world!”, aber es geht ja hier um den Context-Manager. Das, was die Methode __enter__ zurückgibt, wird im with-Statement der Variable zugewiesen, die nach der Instanzierung des Context-Managers mit as angegeben ist. In unserem Beispiel ist das hc, das auf den Context-Manager verweist, weil __enter__ den Context-Manager (self) zurückgibt. Das kann anders sein:

    class Hello:
        def __init__(self, name):
            self._name = name
            
        def show(self):
            print "Hello, %s!" % self._name
            
    class HelloContext:
        def __init__(self, name):
            self._name = name
            
        def __enter__(self):
            print "Enter"
            return Hello(self._name)
            
        def __exit__(self, exc_type, exc_value, exc_tb):
            print "Exit"
            return False
            
    with HelloContext("world") as hc:
        hc.show()

Die Variable hc verweist auf eine Instanz der Klasse Hello, die von der Methode __enter__ des Context-Managers erzeugt und zurückgegeben wird. Das with-Statement ruft trotzdem __enter__ und __exit__ am Context-Manager auf.

Die Behandlung von Exceptions

In den bisherigen Beispielen gibt die Methode __exit__ False zurück. Was bedeutet das? Und was sind das für Argumente, die an __exit__ übergeben werden?

Wenn alles gut geht und der ausgeführte Codeblock keine Exception wirft, wird an __exit__ für alle Argumente (außer self natürlich) None übergeben und der Rückgabewert ignoriert. Wurde eine Exception geworfen, wird an __exit__ der Typ, der Wert und die Traceback-Informationen (entspricht in .NET dem Stacktrace) übergeben.

    class HelloContext:
        def __enter__(self):
            print "Enter"
            return self
            
        def __exit__(self, exc_type, exc_value, exc_tb):
            print "Exit"
            if exc_type != None:
                print exc_type
                print str(exc_value)
                print exc_tb
                
            return True
            
    class MyError(Exception):
        def __init__(self, message, item):
            self.message = message
            self.item = item
            
        def __str__(self):
            return self.message + ' ' + str(self.item)
            
    with HelloContext():
        print "Hello, world!"
        raise MyError("Exception!", 42)

    print "But life goes on..."

Wenn man diesen Code ausführt, wird folgendes ausgegeben:

Enter
Hello, world!
Exit
<class ‘__main__.myerror’>
Exception! 42
<traceback object at 0x1e45320>
But life goes on...

Der Rückgabewert von __exit__ gibt an, ob die Exception unterdrückt werden soll. Deshalb wird in unserem Beispiel der Text “But life goes on…” ausgegeben. Würde __exit__ False zurückgeben, käme stattdessen der normale Python-Traceback und das Programm würde unterbrochen.

Verschachtelte Context-Manager

Das with-Statement kann mit mehr als einem Context-Manager aufgerufen werden:

    with open('a.txt', 'r') as a, open('b.txt', 'w') as b:
        b.writelines(a.readlines())

Dieser Code ist gleichbedeutend mit Folgendem:

    with open('a.txt', 'r') as a:
        with open('b.txt', 'w') as b:
            b.writelines(a.readlines())

Context-Manager in der Standardbibliothek

Neben dem bereits erwähnten unittest.TestCase.assertRaises und io.open gibt es noch weitere Beispiele für Context-Manager in der Python-Standardbibliothek, z.B. im Modul threading. Objekte vom Typ Lock, RLock, Condition, Semaphore und BoundedSemaphore können mit dem with-Statement benutzt werden.

Ein weiteres Modul mit einem Context-Manager ist das Modul decimal, das bei der Fließkommaarithmetik unterstützt. Mit decimalcontext kann man einen Bereich mit anderer Genauigkeit definieren als der umgebende Code.

Das Modul contextlib der Standardbibliothek

Die Python-Standardbibliothek kennt das Modul contextlib. Es enthält einige Hilfsmittel für allgemeine Aufgaben, die mit dem with-Statement zu tun haben.

Der contextmanager-Decorator

Eines dieser Hilfsmittel ist ein Decorator, der die Implementierung eines Context-Managers vereinfacht. Wir können unser obiges Beispiel auch so schreiben:

    from contextlib import contextmanager
    import sys
    
    @contextmanager
    def hello():
        print "Enter"
        try:
            yield
            print "Exit"
        except:
            exc_type, exc_value, exc_tb = sys.exc_info()
            print exc_type
            print str(exc_value)
            print exc_tb
            
    class MyError(Exception):
        def __init__(self, message, item):
            self.message = message
            self.item = item
            
        def __str__(self):
            return self.message + ' ' + str(self.item)
            
    with hello():
        print "Hello, world!"
        raise MyError("Exception!", 42)
        
    print "But life goes on..."

Ein früheres Beispiel würde so aussehen:

    from contextlib import contextmanager
    
    class Hello:
        def __init__(self, name):
            self._name = name
            
        def show(self):
            print "Hello, %s!" % self._name
            
    @contextmanager
    def hello(name):
        print "Enter"
        try:
            yield Hello(name)
        finally:
            print "Exit"
            
    with hello("world") as h:
        h.show()

Wenn bei yield ein Argument angegeben wird, kann dieses beim with-Statement mit as referenziert werden.

Der Context-Manager closing

Ein weiteres wichtiges Hilfsmittel aus dem contextlib-Modul ist die Klasse closing. Damit kann man Klassen wrappen, die zwar eine Methode mit dem Namen close haben, aber selbst kein Context-Manager sind und daher die Methoden __enter__ und __exit__ nicht implementieren. In der Methode __exit__ wird die close-Methode des gewrappten Objekts aufgerufen.

Zum Beispiel:

    from contextlib import closing
    import urllib
    
    with closing(urllib.urlopen(‘http://www.python.org’)) as page:
        for line in page:
            print line

Der nested-Decorator

Der nested-Decorator verbindet mehrere Context-Manager zu einem und ermöglicht somit verschachtelte Context-Manager. Seit Version 2.7 bzw. 3.1 unterstützt das with-Statement verschachtelte Context-Manager und dieser Decorator wurde damals als deprecated markiert und sollte daher nicht mehr verwendet werden. Wenn man ältere Python-Versionen unterstützen muss, kann man den nested-Decorator z.B. so anwenden:

    from contextlib import nested
    
    with nested(open('a.txt', 'r'), open('b.txt', 'w')) as cms:
        cms[1].writelines(cms[0].readlines())

Ein Vorteil des nested-Decorators gegenüber dem with-Statement ist, dass man eine Liste von Context-Manager angegeben kann:

    from contextlib import nested
    
    cms = [open('a.txt', 'r'), open('b.txt', 'w')]
    
    with nested(*cms):
        cms[1].writelines(cms[0].readlines())

Der Stern bei nested ist notwendig, damit an nested die Elemente der Liste als Argumente übergeben werden und nicht die Liste als einzelnes Argument.

Mehr über das Modul contextlib findet man in der Referenzdokumentation oder im Source-Code.

Python 3

In Python 3 hat sich bei der Funktionsweise der Context-Manager nichts geändert. Die Beispiele in diesem Artikel wurden mit Python 2.7 getestet und müssten für Python 3 angepasst werden, weil nicht alle benutzten Module und Statements mit Python 3 kompatibel sind.

Zusammenfassung

Obwohl es auf dem ersten Blick so aussieht wie das using-Statement in .NET, ist Pythons with-Statement wesentlich mächtiger. Die IDisposable-Schnittstelle ermöglicht nur das Aufräumen von Ressourcen, wenn ein Codeblock verlassen wird. Ein Context-Manager kann schon am Anfang eines Codeblocks aktiv werden. Außerdem kann ein Context-Manager beim Verlassen eines Codeblocks eine eventuell aufgetretene Exception behandeln und unterdrücken. So ist auch klar, warum man in Unit-Tests mit dem with-Statement Exceptions testen kann.