39. Exkurs: Operator overloading

Wenn man z.B. unsere Edelstrings addieren will, braucht es dazu immer Funktionsaufrufe. Wenn man aber dem +-Operator sagen könnte, was er mit Edelstrings tun soll, könnte man schöne Ausdrücke bauen, und alles würde hübscher aussehen. In C geht das nicht, wohl aber in C++ und Python.

In Python können Klassen durch die Definition geeigneter “magischer” Methoden sagen, wie Operatoren auf sie wirken sollen – die ganze Liste steht in der Language Reference. Auf diese Weise wird a+b zu a.__add__(b) und a%b zu a. mod__(b).

Anwendungsbeispiel: Logrithmische Wahrscheinlichkeiten. Im NLP werden Wahrscheinlichkeiten häufig so klein, dass man große Probleme mit der Darstellung dieser Zahlen bekommt. Deshalb rechnet man gerne mit ihren Logarithmen: Der dekadische Logarithmus von 10-308 ist eben -308, was natürlich weitaus handhabbarer ist. Üblicherweise macht man dann die Operationen per Hand so, dass sie das richtige für Logarithmen tun. In diesem Fall wollen wir das aber mal Python überlassen. Es gilt:

L(a) = log(a)        L(a ⋅ b) = L (a ) + L (b)

L(a + b) = L (a + b)     L (a∕b) = L (a ) - L (b)
                              b
L(a - b) = L (a - b)      L (a ) = L (a ) ⋅ L (b)

Eine mögliche Implementation:

from math import log10

class LogProb:
  def __init__(self, num=1, val=None):
    if val is None:
      self.val = log10(num)
    else:
      self.val = val
  def __repr__(self):
    return "LogProb(10**%f)"%self.val
  def __str__(self):
    return "%f"%(10**self.val)
  def __add__(self, other):
    return LogProb(10**self.val+10**other.val)
  def __sub__(self, other):
    return LogProb(10**self.val-10**other.val)
  def __mul__(self, other):
    return LogProb(val=self.val+other.val)
  def __div__(self, other):
    return LogProb(val=self.val-other.val)
  def __pow__(self, other):
    return LogProb(val=self.val*other.val)

if __name__=="__main__":
  a, b = LogProb(0.4), LogProb(0.9)
  print a*b, b-a, a+b, a/b, a**b

Nochmal: In realen Programmen würde man das wahrscheinlich nicht so machen, weil ein solches Programm kreuzlahm laufen würde – glücklicherweise kommen die meisten Algorithmen, für die diese logarithmischen Wahrscheinlichkeiten gut sind, mit Mulitplikationen aus, und so reicht es schon, die Multiplikations- durch Additionsoperatoren zu ersetzen und ggf. noch ein wenig an der Ein- und Ausgabe zu feilen, so dass diese Sorte von Klasse nicht nötig ist. Man könnte aber hier noch sanity checks einbauen (z.B. num>0 und num<=1), was so eine Klasse im Rahmen eines Experimentiermoduls durchaus sinnvoll wäre, da so Fehler in Algorithmen oder Implementationen schnell auffallen würden.

Grundsätzlich kann Operator Overloading, wenn es sparsam benutzt wird, durchaus helfen, Programme schöner und verständlicher zu machen. Gerade am Anfang neigt man aber dazu, damit zu großzügig umzugehen und komplett konfuse Overloadings zu machen, die niemand mehr versteht.

Operator Overloading in C++ funktioniert im Groben ganz ähnlich. Normalerweise definiert man dort Methoden der Art

int operator+ (double other)
{
  return value+(int)other;
}
int operator+ (int other)
{
  return value+other;
}

– hier tritt kein self auf, weil C++-Methoden sozusagen von selbst wissen, dass (in diesem Beispiel) value sich auf eine Instanzvariable ihres Objekts beziehen (es gibt auch this, das etwa Pythons self entspricht, nur dass es implizit definiert ist). Aufgrund der Sichtbarkeitsbeschränkungen von Instanzvariablen in C++ geht es aber häufig doch nicht ganz so einfach – aber das gehört nicht hierher.


Markus Demleitner

Copyright Notice