Python – Das umfassende Handbuch

Ein kleiner “rant” über Python - Das umfassende Handbuch von GalileoComputing.

Anmerkung

Dieser Text bezieht sich auf das OpenBook, also die Version zu Python 2.x. Ob sich in der aktuellen Buchauflage, die Python 3 behandelt, an den angeführten Kritikpunkten etwas geändert hat, kann ich nicht sagen.

Vielleicht ist es ein wenig unfair ein Buch nur nach einem Kapitel zu beurteilen, aber gerade das Thema objektorientierte Programmierung (OOP) bereitet vielen Einsteigern Probleme und in diesem Kapitel versagt das Buch dem Leser pythonisches Programmieren näher zu bringen. Wer Programme im Stil dieses Kapitels abfasst, schwimmt innerhalb der Python-Gemeinschaft gegen den Strom. Teilweise ist das gezeigte nicht nur kein idiomatisches Python, sondern schlicht fehlerhaft. Das Buch, oder zumindest das OOP-Kapitel 12 Objektorientierung, hinterlässt den Eindruck, dass die Autoren Peter Kaiser und Johannes Ernesti ein Thema suchten um Geld mit einem Buch zu verdienen, sich umschauten welche Sprache, zu der es noch relativ wenig deutschsprachige Bücher gibt, sich dazu eignen könnte, und einfach losgelegt haben Wissen und Stil von anderen Sprachen 1:1 auf Python zu übertragen.

Destruktor

Die __del__()-Methode sollte man entweder gar nicht erwähnen, oder höchstens in einer Warnung, dass man sie nicht implementieren sollte. Der Text behandelt sie aber wie einen deterministischen Destruktor in C++, ohne jegliche Warnung, dass die ganzen Probleme, die man sich damit einhandeln kann, und die Garantien, die die Sprache (nicht) macht, diese Methode letztendlich nutzlos bis gefährlich machen.

Die Erklärung “Destruktoren werden aber häufig benötigt, um beispielsweise bestehende Netzwerkverbindungen sauber zu trennen, den Programmablauf zu dokumentieren oder Fehler zu finden.” ist jedenfalls falsch. Wenn man eine Ressource sauber abräumen will, kann man sich nicht auf __del__() verlassen, da weder garantiert wird, wann die Methode aufgerufen wird, noch ob sie überhaupt jemals aufgerufen wird. Den Programmablauf dokumentieren, ich nehme mal an hier ist protokollieren der Art “Objekt xy wurde gerade zerstört” gemeint, geht mit __del__() folglich auch nicht zuverlässig. Und eine Methode zur Fehlersuche zu verwenden, die durch ihre blosse Existenz schon drastischen Einfluss auf das Verhalten des Programms bezüglich der Speicherfreigabe haben kann, ist ebenfalls keine gute Idee.

Das Beispiel für Statische Member in Abschnitt 12.1.5 ist komplett an der Realität vorbei, weil man __del__() so eben nicht zuverlässig verwenden kann. Man kann damit sehen wieviele Exemplare von Konto gerade existieren, sieht aber nicht, welche davon wirklich von der Bank verwendet werden oder welche aus verschiedenen anderen Gründen noch im Speicher herumhängen. Zumal diese Verwendung auch bei einem echten deterministischen Destruktor keine gute Idee ist, da immer die “Gefahr” besteht, dass irgendwo im Programm temporäre Konto-Objekte erstellt werden, die aber bei einer regulären Zählung der Konten der modellierten Bank, nicht mitgezählt werden sollten.

Mal ein Beispiel, welches ein eventuell etwas überraschendes Ergebnis liefert:

>>> class Konto(object):
...     anzahl = 0
...
...     def __init__(self, inhaber, konto_nr, betrag):
...         self.inhaber = inhaber
...         self.konto_nr = konto_nr
...         self.betrag = betrag
...         Konto.anzahl += 1
...
...     def __repr__(self):
...         return '%s(%r, %r, %r)' % (self.__class__.__name__,
...                                    self.inhaber,
...                                    self.konto_nr,
...                                    self.betrag)
...
...     def __del__(self):
...         Konto.anzahl -= 1
...
>>> k1 = Konto("Florian Kroll", 3111987, 50000.0)
>>> k2 = Konto("Lucas Hövelmann", 25031988, 43000.0)
>>> k3 = Konto("Sebastian Sentner", 6091987, 44000.0)
>>> Konto.anzahl
3
>>> k3
Konto('Sebastian Sentner', 6091987, 44000.0)
>>> del k3
>>> Konto.anzahl    # Man könnte hier 2 als Antwort erwarten.
3

Das “Geheimnis” ist, dass die del-Anweiung nur den Namen k3 löscht, nicht aber das daran gebundene Objekt. Die Python-Shell hält noch eine Referenz auf Ergebnis der letzten Eingabe, das nicht None war, weshalb das Konto-Exemplar immer noch existiert. Ähnlich subtile Vorgänge können auch in laufenden Programmen vorkommen, zum Beispiel im Zusammenhang mit Ausnahmen, die eine Referenz auf einen Stapelabzug besitzen, wodurch alle lokalen Namen innerhalb der Aufrufhierarchie noch erreichbar sind.

Die Anzahl von Konten würde man eher in einem Container-Objekt mitverfolgen. Beispielsweise im einfachsten Fall die Länge einer Kontenliste oder ein Konten- oder Bank-Objekt, welches weitere Aufgaben übernimmt. Es müssen ja auch Rahmenbedingungen sichergestellt werden, wie die eindeutige Vergabe von Kontonummern.

“Private” und explizite Typprüfung

“Allerdings ist es immer noch möglich, außerhalb der Klasse auf die Attribute direkt zuzugreifen und diese zu verändern, […]” und “Auch die Zuweisung von Werten ungültiger Datentypen wird noch nicht verhindert.” lassen den Horror ahnen, der diesen Aussagen folgt.

Damit verlassen die Autoren die Welt der Python-Programmierung und betreten die Abgründe der “discipline & bondage”-Sprachen. Wer statische Typisierung und Zugriffsschutz mag, soll doch bitte nicht in Python programmieren und erst recht keine Bücher darüber schreiben.

In Abschnitt 12.1.3 werden dann auch erst einmal alle Attribute mittels doppelter führender Unterstriche “private” gemacht, um gleich darauf einen trivialen Getter einzuführen. Bezeichnet wird das als private members. Irgendwie nicht die übliche Python-Nomenklatur – warum können die nicht bei “Attribut” oder “attribute” bleiben? Das die beiden Unterstriche dazu gedacht sind, Namenskollisionen bei Mehrfachvererbung zu vermeiden, wird weder hier, noch später im Abschnitt über Vererbung erwähnt.

Danach folgt ein Setter, der das Argument mit type() auf float oder int prüft. Was a) dem “duck typing” zuwieder läuft und b) selbst für eine explizite Prüfung schlechter Stil ist, weil damit völlig unnötigerweise auch alle Objekte ausgeschlossen werden, deren Typ von int oder float abgeleitet wurde.

Getter und Setter werden im nächsten Abschnitt durch Properties ersetzt, wobei diese im Text als Managed Attributes bezeichnet werden – was ebenfalls ein für Python unüblicher Ausdruck ist.

Statische Methoden

Statische Methoden werden als relativ Nutzlos hingestellt, dabei werden sie – und Klassenmethoden, die gar nicht erwähnt werden – häufig für alternative Konstruktoren verwenden.

__slots__

Wenn wir schon “private” Attribute haben und auf Typen prüfen, darf in Abschnitt 12.3.1 __slots__ als Mittel zum verhindern vom dynamischen hinzufügen von Attributen nicht fehlen. Der eigentliche Zweck, dass Exemplare mit “slots” weniger Speicherplatz benötigen, wird erst an zweiter Stelle erwähnt.

Schlechtes Beispiel für Objektorientierung

Und gerade wo man denkt es kann nicht schlimmer werden, kommt am Ende, unter der Überschrift Objektphilosophie, noch ein Beispiel wie konsequente Objektorientierung es erleichtert wiederverwendbaren Code zu schreiben, das so schlecht ist, dass es einem die Sprache verschlägt:

class ListeMitDurchschnitt(list):
    def durchschnitt(self):
        summe, i = 0.0, 0
        for e in self:
            if type(e) in (int, float):
                summe += e
                i += 1
        return summe / i

Das funktioniert nur mit Exemplaren von ListeMitDurchschnitt, dass heisst wenn man den Durchschnitt von beliebigen iterierbaren Objekten berechnen will, sieht der Aufruf so aus result = ListeMitDurchschnitt(values).durchschnitt(). Es wird also ein Exemplar erstellt, nur um darauf eine Methode aufzurufen, und das Exemplar gleich wieder wegzuwerfen. Das ist ein “code smell”, der darauf hinweist, dass man hier besser eine Funktion verwenden sollte.

Desweiteren hat auch dieses Beispiel wieder das Problem, dass völlig unnötigerweise die Typen eingeschränkt werden, obwohl die Berechnung des Durchschnitts durchaus auch Sinn bei anderen Typen aus der Standardbibliothek oder Bibliotheken von Drittanbietern machen würde. Beispiele wären long, decimal.Decimal, oder fractions.Fraction und mpz, mpq, und mpf aus dem gmpy-Modul, oder die numpy-Typen. Und natürlich auch alle von den genannten Typen abgeleitete Klassen.

Eine vernünftigere und flexiblere Implementierung als Funktion könnte so aussehen:

def average(iterable, start_value=0):
    total = start_value
    i = None
    for i, value in enumerate(iterable):
        total += value
    if i is None:
        raise ValueError('iterable must contain at least one argument.')
    return total / (i + 1)

Diese Funktion verarbeitet nicht nur alle Typen auf denen die Addition und die Division durch eine ganze Zahl entsprechend implementiert ist, sondern ist zudem auch noch “lazy”, dass heisst sie kopiert die Elemente des iterable-Arguments nicht in eine Liste, sondern benötigt immer nur den Speicher um das Zwischenergebnis zu halten.

Fazit

Jemand der nach dem Buch OOP in Python lernt, wird sehr viele Änderungsvorschläge bekommen, wenn er seinen Quelltext in den einschlägigen Python-Newsgroups und -Foren präsentiert. Und das nicht nur wegen der Namensgebung im Buch, die sich nicht an den Python-Style-Guide (PEP 8) [1] hält, sondern wegen grundlegender Entwurfsentscheidungen, die kein idiomatisches Python repräsentieren.

Aus diesem Grund kann man das Buch meiner Meinung nach nicht für Anfänger empfehlen.


[1]Es gibt eine deutsche Übersetzung von PEP 8