Mutation als Seiteneffekt

Die Mutation des Zustands von Objekten ist ein Effekt. Effekte werden auch als Seiteneffekt bezeichnet, wenn sie sozusagen nebenbei erfolgen, zum Beispiel zusätzlich zu einem berechneten Ergebnis oder unabsichtlich neben einem weiteren beabsichtigten Effekt.

Betrachten Sie diese rekursive Definition der Funktion rev. Anders als von der zuvor definierten Prozedur reverse wird das Argument hier nicht mutiert. Stattdessen wird ein umgekehrtes Array zurückgeliefert. Diese Funktion hat keinen Effekt, also auch keinen Seiteneffekt.

def rev(a):
  if len(a) > 1:
    half = len(a) // 2
    
    # a == left + right
    left = a[0:half]
    right = a[half:len(a)]
    
    return rev(right) + rev(left)
  else:
    return a

Der rekursive Fall teilt das Argument in zwei Hälften, berechnet deren Umkehrung rekursiv und hängt die Ergebnisse in umgekehrter Reihenfolge aneinander. Die Implementierung nutzt die folgende Eigenschaft, die jede reverse-Funktion erfüllen sollte:

reverse(l + r) == reverse(r) + reverse(l)

Ist das Argument kurz genug (seine Länge höchstens 1), so wird die Rekursion abgebrochen. In diesem Fall ist das Argument gleich seiner Umkehrung, so dass es selbst zurückgegeben wird.

Betrachten Sie die folgende Prozedur, die rev verwendet, um aus einem übergebenen Array ein neues zu berechnen, in dem die Elemente erst rückwärts und dann vorwärts stehen.

def back_and_forth(a):
  result = rev(a)
  result[len(a):len(a)] = a
  print('Aus ' + str(a) + ' wird ' + str(result))

Bei einem Aufruf dieser Prozedur wird von der enthaltenen Ausgabeanweisung sowohl das als Argument übergebene Array als auch das neu berechnete Array ausgegeben:

>>> back_and_forth([1,2,3])
Aus [1,2,3] wird [3,2,1,1,2,3]

Das neu berechnete Array wird durch Mutation des Ergebnisses von rev erzeugt, indem das ursprüngliche Array hinten in das umgekehrte eingefügt wird.

Diese Mutation des in der Variablen result gespeicherten Ergebnisses von rev ist ein weiterer Effekt von back_and_forth, der zusätzlich zur Ausgabe erfolgt. Da die Variable result nur im Rumpf der Prozedur back_and_forth sichtbar ist, könnte man meinen, dass dieser zusätzliche Effekt lediglich ein Implementierungsdetail und von außen (außer in der erzeugten Ausgabe) nicht beobachtbar ist.

Allerdings hat die verwendete rev-Funktion eine Eigenschaft, durch die dieser zusätzliche Effekt ein unbeabsichtigter Seiteneffekt wird, wie der folgende Aufruf zeigt.

>>> back_and_forth([1])
Aus [1,1] wird [1,1]

Das ist nicht die beabsichtigte Ausgabe. Richtig hätte die Ausgabe lauten sollen: Aus [1] wird [1,1].

Im Rumpf von back_and_forth wird bei der Erzeugung des neu berechneten Arrays auch das als Argument übergebene Array mutiert, weil die Variablen a und result in diesem Fall auf dasselbe Objekt verweisen. Dass rev im Fall eines kurzen Argumentes (Länge höchstens 1) das Argument selbst zurückliefert, führt hier in Verbindung mit der Mutation des Ergebnisses zu einer unerwünschten Mutation des Argumentes. In diesem Fall ist back_and_forth eine ihr Argument mutierende Prozedur, was nicht beabsichtigt war.

Bei der Definition von back_and_forth kann leicht vergessen werden, dass die Funktion rev in einigen Fällen ihr Argument zurück liefert. Besser wäre es, wenn rev auch im Fall kurzer Argumente ein neues Objekt zurück liefern würde, damit unbeabsichtigte Mutationen wie in unserem Beispiel vermieden werden. Bei der Definition eigener Funktionen sollten Sie deshalb niemals das Argument selbst zurück liefern, es sei denn die Funktion ist als mutierend beschrieben und die Tatsache, dass das Argument zurück geliefert wird, ergibt sich aus diesem Kontext.

Bei der oben gezeigten rev-Funktion sollten wir dieser Maßgabe entsprechend die Anweisung return a durch return a + [] ersetzen, um ein neues Objekt zu erzeugen, dessen Wert der gleiche ist wie der des Argumentes a.