Letzte Woche blieb mein Blick an einem Stückchen Programmcode hängen. Der Code war zwar nicht falsch -- er würde zweifellos die geforderte Arbeit tun --, aber es verbarg sich dahinter ein übler Denkfehler. In einem anderen Zusammenhang würde das Programm unerwünschte Seiteneffekte erzeugen. Ob der Programmautor sich dessen bewusst war?
Es ging darum Zahlen zu summieren. Nach Eingabe einer Zahl n soll die Funktion die Summe aus 1 + 2 + 3 + ... + n errechnen. Beispiel: Eingabe 5 => 1 + 2 + 3 + 4 + 5 = 15. Eine entsprechende Funktion ist einfach programmiert, hier am Beispiel mit Python. Python markiert Blöcke über Einrückungen statt über geschweifte Klammern oder ähnliches. (Da ich Schwierigkeiten mit dem Online-Editor habe, sind die führenden Leerezeichen durch Punkte markiert).
def sum1toN_V1(n):
....assert n >= 1
....res = 0
....for i in range(1,n+1): res += i
....return res
Um den Code zu verstehen, muss man einzig wissen, was range(1,n+1) macht: es liefert eine Liste von Zahlen von 1 bis n+1 zurück. Zum Beispiel ergibt sich für n=5 range(1,6) => [1,2,3,4,5]. In der for-Schleife werden die Zahlen nacheinander dem Wert i zugewiesen. (Für die Pythoniker unter Ihnen: xrange ist bei großen n vorzuziehen, ist aber ein wenig schwieriger zu erklären und hier im Moment unwichtig.) Pro Durchgang wird der aktuelle Summenwert aus i und dem Vorgängerwert der Hilfsvariablen res gebildet und in res gespeichert. Das assert-Statement stellt sicher, dass die Funktion nur mit positiven Eingaben arbeitet; das assert-Statement ist als Voraussetzung (precondition) zu verstehen. Siehe dazu auch "Netze spannen mit Design by Contract".
Nebenbei bemerkt: Die Summe kann auch rekursiv berechnet werden. Oben, Version 1, beschreibt ein iteratives Vorgehen
Der Code, an dem sich mein Blick verfing, sah nur minimal anders aus:
def sum1toN_V2(n):
....assert n >= 1
....for i in range(1,n): n += i
....return n
Der Programmierer hat es geschafft, die Variable res und damit eine ganze Zeile einzusparen. Er hat die Eingabe n genommen, addiert die fehlenden Zahlen aus der Reihe von 1 bis n-1 hinzu und gibt das Ergebnis aus. Raffiniert, oder?
Ja und Nein! Version 2 funktioniert zwar, aber Sie sollten sowas niemals programmieren. Gerade in einer dynamisch typisierten Programmiersprache wie Python ist das evil. Warum?
Denken Sie daran: Moderne Sprachen arbeiten fast ausschließlich mit Referenzen auf Objekte. Eine Referenz ist eine Art Zeiger auf ein Objekt. Einzig Zahlen, Strings und andere Basistypen werden typischerweise direkt durch ein Objekt und nicht durch eine Referenz darauf realisiert. Darum haben Sie bei Version 2 mit Zahlen Glück. Machen Sie dasselbe mit Referenzen, dann können Sie eine böse Überraschung erleben.
Um das zu demonstrieren, führe ich eine Klasse Number ein, die als Attribut einen Wert (value) hat. Lassen Sie sich von dem "self" nicht irritieren; es kommt ungefähr einem "this" in Java gleich. Die __init__-Methode ist der Konstruktor in Python.
class Number(object):
....def __init__(self,value):
........self.value = value
Passen wir die Funktion von eben auf die Verarbeitung von Numbers an:
def sum1toN_V3(n):
....assert n.value >= 1
....for i in range(1,n.value): n.value += i
....return n
Machen wir einmal einen Testlauf. Solche kleinen Programme kann man bei Python wunderbar einfach über die Konsole eintippen und interaktiv ausprobieren:
>>> a = Number(10)
>>> a.value
10
>>> b = sum1toN_V3(a)
>>> b.value
55
>>> a.value
55
Böse, gell?! Die Berechnung hat "nebenbei" a verändert. Ein Seiteneffekt, den Sie sich in aller Regel nicht wünschen. Es liegt an den Referenzen und call by reference. Sie haben der Summenfunktion eine Referenz auf das Number-Objekt a übergeben. Das Objekt wird über die Referenz manipuliert, die Referenz wird zurückgegeben und b zugewiesen. Folglich verweisen a und b auf ein und dasselbe Objekt.
Version 2 verhält sich dagegen unkritisch, da Zahlen nicht als Referenzen durchgereicht werden:
>>> a = 10
>>> a
10
>>> b = sum1toN_V2(a)
>>> b
55
>>> a
10
Mir selbst ist ein solcher Fehler im Umgang mit Referenzen auch schon unterlaufen. Wahrscheinlich muss da jeder Programmierer bzw. jede Programmiererin durch. Aber es hilft, wenn man diese Fehlerquelle im Kopf verankert hat. Man fällt ihr dann nicht so leicht zum Opfer. Und eine Lehre lässt sich daraus ziehen: Sie müssen exakt wissen, was eine Programmiersprache über Referenzen abbildet und was von dieser Regel ausgenommen ist.
Um die oben beschriebene Summenfunktion rankt sich übrigens eine Anekdote um Carl Friedrich Gauß. Sie kennen die Geschichte vielleicht. Carls Schullehrer stellte der Klasse die Aufgabe, die Zahlen von 1 bis 100 zu addieren. Reine Beschäftigungstherapie, der Lehrer wollte seine Ruhe haben. Klein Carlchen tat sich als Ruhestörer hervor, er war dafür zu clever. Er bemerkte eine besondere Eigenschaft. In der Zahlenreihe von 1 bis 100 ergeben die erste und letzte Zahl genau denselben Wert, nämlich 101, wie die zweite und die vorletzte Zahl usw. Das Spielchen kann man genau 50 mal machen. 50 mal 101 macht 5050. Fertig. Schenkt man dem wunderbaren Buch von Daniel Kehlmann "Die Vermessung der Welt" (Rowohlt Verlag) Glauben, dann bezog Carl dafür ein letztes Mal Prügel. Aber sein Talent ward entdeckt und wurde fortan gefördert.
P.S.: Ein Pythoniker hätte die Summenfunktion anders geschrieben, da es die eingebaute Funktion sum gibt. Damit hätte es ganz knapp gelautet:
def sum1toN(n):
....assert n >= 1
....return sum(range(1,n+1))