Gleitkommazahlen

Nach den ganzen Zahlen, die ich im letzten Beitrag behandelt habe, wollen wir uns nun den „anderen Zahlen“ zuwenden, die Python uns anbietet. Das sind die Gleitkommazahlen, Datentyp float. Gleitkommazahlen werden als Näherung für reelle Zahlen (\(\mathbb{R})\) verwendet. Für das Rechnen mit rationalen Zahlen (\(\mathbb{Q})\), also Zahlen die sich als Bruch ganzer Zahlen darstellen lassen, die eine Teilmenge der reellen Zahlen sind, bietet uns Python eine spezielle Lösung in Form der Klasse Fraction.

Kurz zur literalen Notation von Gleitkommazahlen: Wir notieren Gleitkommazahlen mit einem Dezimalpunkt und ggf. einem e oder E, welches dann zu lesen/verstehen ist als „mal 10 hoch“.

42.    42.0    42e0    4.2e1    .42e2

Das sind alles valide Literale, um den dezimalen Wert 42 als Gleitkommazahl in Python einzugeben.

Mischen wir in einem Ausdruck ganze Zahlen mit Gleitkommazahlen, oder verwenden die Gleitkommadivision, dann wird die Berechnung immer mit Gleitkommazahlen durchgeführt. Im Gegensatz zu ganzen Zahlen werden sie im sogenannten IEEE-754-Format gespeichert. Durch die andersartige Speicherung müssen auch die Rechenoperationen anders implementiert werden, weshalb ganze Zahlen in Gleitkommazahlen umgewandelt werden, wenn sie gemeinsam in Ausdrücken vorkommen.

IEEE-754-Format

Fast alle aktuellen Computer stellen Fließkommazahlen binär im sogenannten Format IEEE 754 dar. Nicht nur Dezimalzahlen können Nachkommastellen besitzen, auch Binärzahlen. Deren Stellenwerte sind jedoch nicht 1/10, 1/100, 1/1000 usw., sie lauten: 1/2, 1/4, 1/8, …

Vereinfachte Darstellung

Die Kodierung der Ganzzahlen war noch ganz übersichtlich. Da Gleitkommazahlen etwas komplexer in ihrer Kodierung sind, erlaube ich mir, erst einmal eine nicht ganz korrekte Darstellung zu präsentieren. Ich werde Dir natürlich auch die korrekte Darstellung zeigen. Ungeachtet der Vereinfachungen, die ich gleich vornehmen werde, wird der sittliche Nährwert, der daraus resultiert, auch in Bezug auf die korrekte Darlegung der Verhältnisse, erhalten bleiben.

Der Standard IEEE 754 definiert zwei Arten von Fließkommazahlen, die sich durch Ihre Größe und damit durch Ihren Wertebereich und ihre Genauigkeit unterscheiden. Sie nennen sich float (32 Bit) und double (64 Bit). Der Datentyp float von Python korrespondiert mit double. Der kleinste von Null verschiedene Wert ist \(\pm2.2E-308\) (bzw. gar \(\pm4,9E-324\) in der subnormal representation), der Wertebereich umfasst \(\pm1,7E+308\). Die Genauigkeit beträgt 15-16 Stellen.

Wie man auf die Wertebereiche kommt, sehen wir uns an, wenn wir die Kodierung kennen. Ebenso wissen wir dann, was die Angabe der Genauigkeit bedeutet. Ganzzahlen werden exakt gespeichert und Berechnungen mit ihnen sind, sofern wir innerhalb der jeweils gültigen Wertebereiche bleiben (bei Python nicht relevant), korrekt. Gleitkommazahlen sind dagegen nicht genau. Dafür reicht der Wertebereich einer float-Variablen aber aus, um problemlos alle Atome in unserem Kosmos zählen zu können. Schlaue Physiker haben nämlich errechnet, dass es \(10^{84}\) bis \(10^{89}\) sein sollen. Uns bleibt also noch reichlich Luft für die Entdeckung weiterer Galaxien.

Gleitkommazahlen werden in Python in der Normaldarstellung oder im wissenschaftlichen Format notiert. 12.5 kann also auch als 1.25E1 geschrieben werden. Ingenieure kokettieren gerne mit der zweiten Notation. Wie auch immer, entscheidend ist der Fakt, dass es Nachkommastellen gibt, die kodiert werden wollen.

Eine Fließkommazahl besteht aus einem Vorzeichenbit, einem Exponenten und einer Mantisse. Beim Datentyp float haben 11 Bits für den Exponenten und 52 Bits für die Mantisse.

Eine Gleitkommazahl wird zu ihrer Darstellung im Rechner in Exponent (\(e\)) und Mantisse (\(m\)) zerlegt, so dass sich der Wert (\(w\)) einer Gleitkommazahl mathematisch wie folgt darstellt, wobei \(s\) das Vorzeichen (0 oder 1) darstellt:

\[ w=(-1)^{s}\cdot2^{e}\cdot{m} \]

Um zu verstehen, wie das genau funktioniert, betrachten wir eine einfache Gleitkommazahl: 12.0. Das Vorzeichenbit ist, weil unsere Zahl positiv ist, 0. Schauen wir uns nun an, wie wir unsere 12.0 in Exponent und Mantisse zerlegen können. Dabei muss der Exponent positiv oder negativ ganzzahlig sein.

\[ \begin{align*} \begin{split} 12 &= 2^{0} \cdot 12 \\ 12 &= 2^{1} \cdot 6 \\ 12 &= 2^{2} \cdot 3 \\ 12 &= 2^{3} \cdot 1.5 \\ \end{split} \end{align*} \]

Offenkundig ist das nicht eindeutig. Da wir jedoch eine eindeutige Darstellung benötigen, hat man sich darauf geeinigt, dass die Mantisse einen Wert \(m\) mit \(1 \le m \lt 2\) haben muss. Hierbei wird jedoch nur der um 1 verminderte Wert gespeichert. Man nennt das die reduzierte Mantisse.

Anhand dieser Regel, wissen wir nun, dass wir für unsere 12.0 den Exponenten 3 und die Mantisse 0.5 speichern müssen. Dabei gelten für den Exponenten die bereits bekannten Regeln der vorzeichenbehafteten Ganzzahlen. Er wird also im Zweierkomplement codiert. Für die 3 ergibt sich dann, wir betrachten einen 64-Bit-Datentypen, das Bitmuster 00000000011 (11 Bits).

Kommen wir nun zur Mantisse. In ihr werden, gemäß Definition nur Werte zwischen 0 und näherungsweise 1 gespeichert. De facto ergibt das Werte zwischen 1 und fast 2. Wir erinnern uns: reduzierte Mantisse – Eins im Sinn. In der Binärdarstellung haben wir es demnach mit folgenden Wertigkeiten bezüglich der einzelnen Digits in der Mantisse zu tun (Darstellung verkürzt):

-1 -2 -3 -4 -5 -6 -7 -8 -9 -10
\(2^{-1}\) \(2^{-2}\) \(2^{-3}\) \(2^{-4}\) \(2^{-5}\) \(2^{-6}\) \(2^{-7}\) \(2^{-8}\) \(2^{-9}\) \(2^{-10}\)

Der Wert der Mantisse errechnet sich, wie wir leicht erkennen, nach dem gleichen Schema, wie bei den Ganzzahlen. Nur der Index hat sich geändert.

\[ w = \sum_{i=-1}^{-k}{d_{i} \cdot 2^{i}} \]

Zur Findung des Bitmusters unserer Mantisse gehen wir im Prinzip genauso vor, wie wir das für positive Ganzzahlen getan haben. Nur dividieren wir jetzt nicht durch 2, vielmehr multiplizieren wir damit und schauen, ob der sich ergebende Wert größer 1 ist. Ist das der Fall, haben wir eine 1 für unser Bitmuster errechnet, ansonsten eine 0. War unser Wert größer oder gleich 1, subtrahieren wir 1 und rechnen weiter. Das treiben wir solange, bis wir 0 erreicht haben. So füllen wir unser Bitmuster von links nach rechts.

Für unser Beispiel geht das schnell: \(0.5\cdot2=1.0\), was eine 1 ergibt. Subtrahieren wir hiervon 1, sind wir bei 0 angelangt und somit fertig.

Damit ergibt sich folgende Darstellung unserer Gleitkommazahl 12.0:

0 00000000011 100000000000000000000000000000000000000000000000000

Zur besseren Lesbarkeit habe ich je ein Leerzeichen vor dem Exponenten und der Mantisse eingefügt.

Jetzt müssen wir nur noch wissen, wie die Grenzen definiert sind, wozu wir uns die größt- beziehungsweise kleinstmöglichen Bitmuster ansehen. Der Exponent hat mit -1024 seinen kleinsten Wert. Das ist als 0 definiert. 1023 ist der größte Wert. Dieser markiert in Verbindung mit dem Vorzeichenbit minus/plus unendlich. Alle Werte dazwischen sind „echte“ Zahlen, also \((-1)^{s}\cdot2^{e}\cdot(1.m)\) für \(-1024 \lt \lt 1023\). \(s\) ist, wie gehabt, das Vorzeichenbit.

Damit liegt die größte darstellbare positive Zahl im vereinfachten Format knapp unter \[ 2 \cdot 2^{1022} = 2^{1023} \approx 10^{308} \] Die kleinste darstellbare positive Zahl im vereinfachten Format lautet \[ 1.0 \cdot 2^{-1023} = 2^{-1023} \approx 10^{-308} \]

Kommen wir nun zu einem Fakt, der unbedarfte Leser verwirren wird. Nehmen wir uns die handelsübliche 0.3 und versuchen sie zu codieren. Offensichtlich sind Vorzeichen und Exponent 0. Kümmern wir uns daher um die reduzierte Mantisse. Wir benutzen den bereits bekannten Algorithmus.

\[ \begin{split} 2\cdot 0.3 = 0.6 \Longrightarrow 0 \\ 2\cdot 0.6 = 1.2 \Longrightarrow 1 \\ 2\cdot 0.2 = 0.4 \Longrightarrow 0 \\ 2\cdot 0.4 = 0.8 \Longrightarrow 0 \\ 2\cdot 0.8 = 1.6 \Longrightarrow 1 \\ 2\cdot 0.6 = 1.2 \Longrightarrow 0 \\ \ldots \end{split} \]

Ich brauche hier nicht weiter zu machen. Da sich die 1.2 wiederholt, erkennen wir, in eine nicht endende Periode geraten zu sein. Erkenntniswert: Was uns wie eine handelsübliche Zahl erscheint, muss für den Rechner nicht zwingend auch handelsüblich sein. Es gibt im Dezimalsystem Zahlen, die sich dual nicht codieren lassen. Diese Schwäche ist übrigens jedem Zahlensystem innewohnend.

Stellen wir uns ein Zahlensystem zur Basis 3 vor. Dann ist 0.1 eine ziemlich präzise und auch endliche Gleitkommadarstellung vom Dezimalbruch \(\frac{1}{3}\), der als Gleitkommazahl im Dezimalsystem nur als unendliche Periode darstellbar ist.

Es sollte Dir nun klar sein, weshalb das, was wir handschriftlich exakt berechnen können, von Computerprogrammen oft nur näherungsweise genau berechnet wird.

Korrekte Darstellung der IEEE-Kodierung

Nun, ich habe ja gesagt, ich würde eine zuerst vereinfachte Darstellung der Gleitkommazahlen vorstellen. Der aufmerksame Leser wird auch bemerkt haben, dass sich die Wertebereiche in der vereinfachten Darstellung von denen unterscheiden, die ich oben angegeben habe. Ich habe bei den Kodierungsregeln für die vereinfachte Darstellung etwas geschummelt.

Wenn Du recherchierst, so stellst Du fest, dass die tatsächlichen Regeln zur Kodierung von double etwas anders aussehen, als die oben dargestellten Regeln. Sie lauten wie folgt:

  • Wenn \(0 \lt e \lt 2047\), dann \(w = (-1)^{s} \cdot 2^{e-1023} \cdot (1.m)\).
  • Wenn \(e = 0\) und \(m \ne 0\), dann \(w = (-1)^{s} \cdot 2^{-1022} \cdot (0.m) \).
  • Wenn \(e = 0\) und \(m = 0\), dann \(w = (-1)^{s} \cdot 0\).
  • Wenn \(e = 2047\) und \(m = 0\), dann \(w = (-1)^{s} \cdot \text{Infinity}\).
  • Wenn \(e = 2047\) und \(m <> 0\), dann \(w = \text{NaN}\).
Diese Regeln haben auch einen Namen. Es handelt sich um den so genannten IEEE-754-Standard, sehr schön nachzulesen unter https://de.wikipedia.org/wiki/IEEE_754. Hieraus ergeben sich dann auch der tatsächliche Wertebereich, die ich oben angegeben habe.

Abweichungen zum IEEE-Standard

Gemäß dem Standard IEE 754 kann man Gleitkommazahlen durch 0 teilen. Das ergibt unendlich. Ausnahme: 0.0 geteilt durch 0.0 ist keine Zahl (NaN). In Python sind diese Operationen, analog zur Schulmathematik, nicht gestattet. Wenn Du aber beispielsweise Java kennst, wirst Du bereits auf das geschilderte Verhalten gestoßen sein.

Rationale Zahlen

Rationale Zahlen sind eine Teilmenge der reellen Zahlen. Sie lassen sich also, wie diese, näherungsweise mittels Gleitkommazahlen, wie Python sie bietet, abbilden. Allerdings ist das vielfach unschön. So liefert 1 / 3 den Wert 0.3333333333333333, was nur näherungsweise exakt ist. Python bietet uns jedoch, wenn auch mit einigen Einschränkungen, die Möglichkeit, mit Brüchen zu arbeiten. Im Modul fractions, welches dazu importiert werden muss, gibt es die Klasse Fraction.
>>> from fractions import Fraction
>>> Fraction(1, 3) * 3
    Fraction(1, 1)
>>> print(Fraction(1, 3) * 3)
    1
mit print() ist die Ausgabe etwas schöner und dem Gewohnten näher. Der Konstruktor von Fraction akzeptiert verschiedene Argumente, um einen Bruch zu erzeugen, so auch entsprechende Zeichenketten.

>>> print(Fraction(1, 3) + Fraction("2/3"))
    1
Bei der Notation mit einer Zeichenkette ist unbedingt darauf zu achten, dass Du keine Leerzeichen um den Slash schreibt. "2 / 3" erzeugt einen Fehler! Mittels Fraction kannst Du vielfach die Ungenauigkeiten vermeiden, die den Gleitkommazahlen anhaften.

Fazit

Das Wissen über die Codierung von Zahlen im Computer ist kein Hexenwerk. Für den Informatiker ist es die Voraussetzung, um Datentypen für den gedachten Anwendungszweck korrekt zu wählen und/oder die Ergebnisse von Berechnungen einschätzen zu können. Für alle anderen mag es die Grundlage sein, Nachsicht üben zu können, wenn bei komplexen Berechnungen mal wieder nicht das herauskommt, was denn hätte herauskommen sollen. Die dabei wesentlichste Erkenntnis ist aber wohl die, dass das Rechnen mit Gleitkommazahlen nur selten genau ist. Daher ist der unbedarfte Vergleich zweier Gleitkommazahlen in der Regel immer zum Scheitern verurteilt.

In Python brauchen wir nicht explizit Datentypen auswählen. Python erkennt die Datentypen anhand der literalen Wertzuweisungen. Außerdem gibt es jeweils nur einen Datentypen für ganze Zahlen beziehungsweise Gleitkommazahlen. Wenn Du aber einmal mit anderen Programmiersprachen arbeitest, wird Dir das hier vermittelte Wissen hilfreich sein. Es macht aber auch das Verhalten von Python verständlich.

Ich hoffe, ich habe Dir die Codierung von Zahlen im Computer auf verständliche Weise näherbringen können. Das Wissen um diese Sachverhalte ist für Informatiker elementar.

 

Schreibe einen Kommentar