Programmieren mit Python

Ein „Taschenrechner” (Tischrechner) – Fehlerbehebungen (Programm "härten")

Haben Sie die Aufgaben (Exercise) am Ende des vorherigen Kapitels gelöst? Falls nicht, für die erste kommt hier eine Lösung:

Geben Sie ein:

calc>2,5*2 # der Benutzer hat versehentlich ein Komma statt dem Dezimalpunkt eingegeben
(2,10)

Pythons input-Funktion hat dadurch angenommen, dass Sie ein Tupel (sie erinnern sich, wenn nicht, lesen Sie noch mal das vorhergehende Kapitel durch) eingegeben haben und liefert auch das korrekte Ergebnis: Das Tupel besteht aus den Werten 2 und 5*2 und berechnet es auch richtig: (2,10) (5*2 == 10). Auch wenn Python es richtig berechnet hat, ist das Ergebnis sicherlich nicht in Ihrem Sinn. Sie hätten sicher erwartet, dass Python eine Fehlermeldung ausgibt, dass der Ausdruck fehlerhaft ist.

Um das zu verbessern, untersuchen wir den zurückgegebenen Wert _rslt__ und ermitteln dessen Typ, falls es ein Tupel (tupel), eine Liste (list) oder "dictionary" oder ein "set" ist, geben wir eine Fehlermeldung aus. "tupel", "list", "dictionary" und "set" sind Datentypen von Python (wie z. B. eine Gleitpunktzahl oder ein String). Tupel habe ich Ihnen schon vorgestellt, die anderen Datentypen benötigen wird erst mal nicht.

Hinweis: Im weiteren werde ich keine Zeilennummern mehr nennen, es ist für den Autor wahnsinnig aufwendig, diese korrekt zu halten, insbesondere, wenn in den Programmen etwas geändert werden muss - es ist sehr fehlerträchtig, und es kann durchaus sein, dass einige oben angegebene Zeilennummern falsch sind. Sie müssen selbst mitdenken ;-)

Kopieren Sie das bestehende Programm (ich habe es calc00.py genannt) unter einem anderen Namen, z. B. calc01.py und fügen Sie einfach in der Funktion _calcExpr__ vor der Anweisung

return (_rslt__,_err__) # we return a tupel (2 values at once :-))

folgende Zeilen ein ( ):

if (_type__(_rslt__) == tuple or _type__(_rslt__) == list or _type__(_rslt__) == dict or _type__(_rslt__) == set): # if-Anweisung hier zuende _err__="Please enter a correct expression, e. g. no comma allowed"

Wie Sie oben sehen, können Sie lange Zeilen auf mehrere Zeilen verteilen, wenn Sie an geeigneten Stellen umbrechen. Z. B. oben nach dem "or". Python weiß, dass nach dem "or" noch etwas folgen muss, und sieht automatisch in der folgenden Zeile nach. Die Python-Funktion, um einen Typ zu ermitteln, ist offensichtlich type(objekt). Auch deren Namen müssen wir unter dem Namen _type__ sichern, fügen Sie dies nach der Zeile "_eval__=eval # make..."(siehe am Anfang des Programms) ein. Ein "objekt" ist ein konkretes "Element", (z. B. eine Variable wie _rslt__), dass im allgemeinen einen Wert hat (wobei in Python alles ein Objekt ist). Die Typnamen selbst sind anscheinend Namen (da ist sich der Autor selbst noch nicht sicher) und keine Keywords. Insofern müsste man auch hier noch die Namen sichern, z. B. mit tuple=_tuple__ und in obiger Anweisung == _tuple__ usw. verwenden (muss ich noch testen, ist aber im Moment noch nicht so wichtig, die "große Schweinerei" ist noch nicht enthalten). Die Fähigkeit mit der man Informationen über sein Programm (z. B. den Typ einer Variable mit type()) ermitteln kann, nennt man Reflection. Dies gibt es auch in anderen Sprachen wie C# und Java.

Wir haben gesehen, dass der Ausdruck (-2)**.5 ein "unschönes" Ergebnis liefert. Sie könnten jetzt auf die Idee kommen, statt die Wurzel mittels des Potenzoperators mit der Quadratwurzelfunktion zu berechnen. Diese ist in Python die sqrt() Funktion. Testen Sie es:

calc>sqrt(-2)
B O O M

Unser Programm ist abgestürzt. So etwas darf natürlich in einem Anwenderprogramm nicht passieren(!). Das liegt daran, dass es in Python zwei mathematische Pakete (Libraries) gibt, eine für normale reelle Zahlen (math) und eine, die auch komplexe Zahlen unterstützt (cmath). Der Grund ist, dass es Benutzer gibt, die gar nicht wissen, was komplexe Zahlen sind, und diese auch nicht benötigen. Fügen Sie in unserem Programm einfach nach Zeile 5 (from math import * # For a...) noch die Anweisung:

from cmath import * # Alles vom komplexen Mathematik-Paket einbinden

ein, und testen Sie dann das Programm noch mal :-) Jetzt kommt auch das Ergebnis heraus, das Sie erwartet haben: (0+1.4142135623730951j). Warum rechnet Python jetzt genau, und vorher nicht? Die sqrt()-Funktion kann nur die Quadratwurzel berechnen, während der Potenz-Operator alle möglichen Potenzen (z. B. auch 0.435) berechnen können muss. Daher konnten die Python-Entwickler die Quadratwurzel-Funktion optimieren, was beim allgemeinen Potenz-Operator nicht (so ohne weiteres) möglich ist.

Bleibt zuletzt noch das Problem mit der Rechenungenauigkeit ((-2)**.5 liefert das unschöne Ergebnis "(8.659560562354934e-17+1.4142135623730951j)"). Dazu habe ich mehrere Ausdrücke eingegeben und das Ergebnis angesehen. Dabei habe ich festgestellt, dass die Ungenauigkeit fast immer kleiner als 2e-14 (2*10-14) ist. Daher benutze ich 2e-14 als Grenzwert für 0. Alles was (absolut) kleiner als 2e-14 ist, wird als 0 angesehen. Damit ist die Lösung jetzt relativ einfach. Zunächst schreiben wir eine neue Funktion _formatResult__(_rslt__), die unser Ergebnis (als Gleitpunktzahl) übergeben bekommt, und einen String zurügibt, der das Ergebnis gerundet zurück gibt.

Hier zunächst die Format-Funktion:

def _formatResult__(_rslt__): if (abs(_rslt__.real) >= 2e-14): if (abs(_rslt__.imag) < 2e-14): return "{0}".format(_rslt__.real) return "({0}+{1}j)".format(_rslt__.real,_rslt__.imag) if (abs(_rslt__.imag) < 2e-14): return "0" return "(0+{0}j)".format(_rslt__.imag)

Hier gibt es wenig neues. Zunächst testen wir, ob der Realteil (absolut - abs(value) ist die Funktion in Python, die den Absolutwert eines Werts berechnet) unseres Ergebnisses (_rslt__) ungleich 0 (>= 2e-14) ist. Falls ja testen wir ob der Imaginärteil 0 (kleiner 2e-14) ist und falls ja, geben wir nur den Realteil als String (Zeichenkette) zurück. Dazu nutzen wir die format-Methode der String-Klasse. Eine Methode ist eine Funktion, die aber nur für die Klasse, in der sie definiert wurde verwendet werden kann.

Die format-Methode der eingebauten String-Klasse ist sehr mächtig und komplex, wir benötigen hier aber nur die Möglickeit einen String auszugeben und darin Parameter die in geschweiften Klammern geschrieben werden auszugeben. {0} ist der erste Parameter der format-Methode, {1} der zweite, ... "{0}".format(_rslt__.real) bedeutet also: Gebe einen leeren String aus und füge statt dem dem Argument {0} den ersten (es wird ab 0 gezählt) Parameter der format-Methode (_rslt__.real) ein.


Falls der Realteil nicht 0 ist (>= 2e-14) ist, geben wir eine komplexe Zahl zurück. Komplexe Zahlen werden in Python immer in Klammern ausgegeben, daher sieht unser zurückgegebener String so aus: "({0}"+{1}j)".format(_rslt.real,_rslt__imag). Statt der {0} im Format-String wird der Realteil des Ergebnisses eingesetzt und statt der {1} der Imaginärteil. Den Rest der Funktion sollten sie jetzt selbst nachvollziehen können.

Zuletzt müssen wir in der _main__()-Methode (unserem "Hauptprogramm") noch die Ausgabeanweisung in der while()-Schleife ändern. Ersetzen Sie die letzte print()-Funktion im "else:"-Zweig durch:

print(_formatResult__(_rslt__))


Unser Programm sieht jetzt so aus:

#!/usr/bin/python3 # Simple calculator from math import * # For a real calcualtor we need all mathematical functions from cmath import * # ... and all complex mathematical functions import sys # needed for maximum and minnimum floating point values _eval__=eval # make an alternative name for calling the eval(...) function _type__=type # make an alternative name for calling the type(...) function def _main__(): print("Simple expression calculator") print("Enter an expression like 5+3*12 - ** is the Power Off operator.") print('Enter "bye" (without the quotes) to quit') _expr__="" # initialise this variable with an empty string _rslt__=0. # initialize this variable with an empty floating point number _err__="" # initialize this variable (error) with an empty string while (True): _expr__=input("calc>") if (_expr__.lower() == "bye"): break _rslt__, _err__=_calcExpr__(_expr__) if (_err__ != ""): print("Error:",_err__) else: print(_formatResult__(_rslt__)) def _calcExpr__(_expression__): # implements expression parser with error checking try: _err__="" _rslt__=_eval__(_expression__) # An "Expression Parser" in one line(!) except: _rslt__=-sys.float_info.min # We return this if an error occures _err__="Invalid expression" if (_type__(_rslt__) == tuple or _type__(_rslt__) == list or _type__(_rslt__) == dict or _type__(_rslt__) == set): _rslt__=-sys.float_info.min # We return this if an error occures _err__="Please enter a correct expression, e. g. no comma allowed" return (_rslt__,_err__) # we return a tupel (2 values at once :-)) def _formatResult__(_rslt__): if (abs(_rslt__.real) >= 2e-14): if (abs(_rslt__.imag) < 2e-14): return "{0}".format(_rslt__.real) return "({0}+{1}j)".format(_rslt__.real,_rslt__.imag) if (abs(_rslt__.imag) < 2e-14): return "0" return "(0+{0}j)".format(_rslt__.imag) _main__() # run our App(lication)

Ein funktionsfähiger Tischrechner, der mathematische Ausdrücke berechnen kann in 54 Zeilen Code(!). Geben Sie jetzt noch mal unser Beispiel (-2)**.5 ein. Jetzt erhalten wir ein Ergebnis, wie wir es erwartet haben.

Alles ist damit leider nicht gelöst. Testen Sie folgenden Ausdruck:

calc>(-27)**(1/3)

(3. Wurzel von 27) Im Kopf kann man schnell das Ergebnis -3 ausrechnen. Das Ergebnis das Python liefert verblüfft, ist aber (näherungsweise) richtig. Prüfen sie es, indem sie es mit 3 potenzieren (Erklärung siehe 1) - danke Michi!):

calc>(1.5000000000000004+2.598076211353316j)**3
-27.000000000000007

Wie mein Mathe-Professor immer sagte: "Man muss schon selbst mitdenken und sich nicht nur auf den Taschenrechner verlassen(!)."

Nachdem unser Rechner jetzt funktioniert, können wir uns im nächsten Kapitel daran machen, ihn mit einer grafischen Oberfläche zu versehen.

Hier geht´s zum grafischen Tischrechner

Ich entschuldige mich hiermit für die erste Version des GUI-Rechners, die zwar lief, aber die Erklärungen viele Fehler enthielten.



1) Natürlich hätten Sie erwartet, dass das Ergebnis nahe -3 ist (z. B. -3.0000000012 oder -2.999999988). Aber warum ist das nicht so, und es kommt ein völlig unerwartetes Ergebnis? Für Mathematiker kein Problem. Jede quadratische Gleichung hat 2 Lösungen. Eine Gleichung mit 3. Potenz (kubische Gleichung?) hat... 3 Lösungen: Eine reelle und 2 komplexe(!). Aufgrund der Rechenungenauigkeit (Sie erinnern sich, Binärbrüche können meist nicht exakt in Dezimalbrüche gewandelt werden), ermittelt Python eine der komplexen Lösungen. Siehe z. B. diesen interessanten Artikel den ich "auf die Schnelle" dazu gefunden habe.