Programmieren mit Python 3 – Klasse Expression Parser

Parser Klasse mit Variablen (Klasse ExpressionParser)

Zunächst stelle ich wieder den Quellcode für das ganze Modul (sie müssen es unter dem Namen exprpars.py speichern) vor, danach folgen die Erläuterungen.

"""Expression Parser Class, Copyright (c) 2017, Peter Sulzer, Fuerth/Bay""" from math import * # For a real calculator we need all mathematical functions from cmath import * # ... and all complex mathematical functions import sys # needed for maximum and minnimum floating point values import re # This module is required if we want to use regular expressions # save some important inbuilt functions under another name: _eval__=eval _exec__=exec _round__=round class ExpressionParser: """Parser class to evaluate mathematical expressions""" # Copyright 2017, Peter Sulzer, Fuerth - all rights reserved # Program is released under the GNU General Public License version 1 ore above # First we must compile the pattern for our regular expression: __assignpattern=re.compile("(?P<assign>^[A-Za-z][A-Za-z0-9]*\s*=)") # __assignpattern is a private (can only be accessed from inside our # class) "Class Variable". All instances (objects) of this class have # the same assign pattern # Next we need a (private) class variable with the list of all math # functions and constants and other Python reserved names: __reservedNames = [ "ceil","copysign","fabs","factorial","floor","fmod","frexp", "fsum","gcd","in","isclose","isfinite","isinf","isnan","ldexp", "modf","trunc","exp","expm1","log","log1p","log2","log10", "pow","sqrt","acos","asin","atan","atan2","cos","hypot","sin", "tan","degrees","radians","round","acosh","asinh","atanh","cosh", "sinh","tanh","erf","erfc","gamma","lgamma","pi","e","tau", "inf","nan","None","none","True","true","False","false", ] # Following defines that evaluateExpression is a Class Method which can @classmethod # be called even if no instance of ExpressionParser exists def evaluateExpression(cls,expression): # implements expression parser with error checking expression=expression.strip() # remove leading and trailing whitespace (e.g. spaces) assignement=None # Assume no assignement # Now search if user wants to assign to a variable: if expression.find("=") >= 0: # OK, an assignement, but then the # The part before the "=" must match our pattern: assignement=cls.__assignpattern.search(expression) if not assignement: # if not we report a bad variable name: _err__='Bad variable name! Syntax: [A-Za-z][A-Za-z0-9]*' _rslt__=sys.float_info.min return (_rslt__,_err__) _err__="" # Assume no error at first if assignement: assign=assignement.group("assign") varname=assign[:-1].strip() # If variable name is a reserved name abort with error: if (cls.isReservedName(varname)): _rslt__=sys.float_info.min _err__="Reserved names (e.g. function names) not allowed for variable names" return (_rslt__,_err__) assignLength=len(assign) # Now evaluate the expression after the "=": _rslt__,_err__=cls.__evaluate(expression[assignLength:]) if (_err__): _err__="In assignement: "+_err__ else: try: # Now assign the result to the variable before the "=" # with Pythons inbuild "exec" function: _exec__(expression,globals()) except: _err__="Invalid assignement" _rslt__=sys.float_info.min else: # Calculate the expression with our private method __evaluate(): _rslt__,_err__=cls.__evaluate(expression) return (_rslt__,_err__) # we return a tupel (2 values at once :-)) @classmethod def formatResult(cls,_rslt__): r=_round__(_rslt__.real,14) i=_round__(_rslt__.imag,14) if (r != 0.0): if (i == 0.0): return "{}".format(r) return "({}+{}j)".format(r,i) if (i == 0.0): return "0" return "(0+{}j)".format(i) # Private method (can only be called from code inside our class) to # evaluate an expression: @classmethod def __evaluate(cls,expression): _err__="" try: _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__) @classmethod def isReservedName(cls,name): if (name in cls.__reservedNames): return True else: return False # Below you can define your own functions: # Example: # Important: Add the "sind" function name to the private # _mathFuncs__ class variable of the class ExpressionParser(!): ExpressionParser._ExpressionParser__reservedNames.append("sind") # We must use _ExpressionParser__reservedNames above, to be able to access # the (pseudo) private variable __reservedNames from outside the class def sind(angled): # sinus function which assumes angle in degrees instead of radians return sin(radians(angled))

Zunächst importieren wir die benötigten Module, hier ist das Modul „re” für Reguläre Ausdrücke hinzugekommen. Danach sichern wir einige Funktionsnamen, die wir benötigen unter einem anderen Namen.

Unsere Klasse Klasse heißt „ExpressionParser” Die Strings (z. B. in der ersten Zeile) zwischen den 3-fachen Anführungszeichen (siehe im Kapitel „Ein-/Ausgabe auf Bildschirm”) sind sogenannte „Dokumentationsstrings” (Docstring). Diese müssen immer als erste „Anweisung” eines Moduls, einer Funktion, Klasse oder Methodendefinition stehen. Auf diesen String kann man mittels .__doc__ zugreifen. Z. B. kann man den Dokumentationsstring für die Klasse „ExpressionParser” mittels:

print(ExpressionParser.__doc__)

ausgeben. Es gibt auch Tools mit denen man so direkt aus dem Quellcode eine rudimentäre Dokumentation eines Programms erstellen kann. Sie sollten es sich angewöhnen, in allen Modulen, Klassen, Funktionen, Methoden, ... Dokumentationsstrings zu verwenden (habe ich in dieser Klasse aus Übersichtlichkeitsgründen nicht für jede Methode gemacht).

Mit folgender Anweisung wird das Muster für den regulären Ausdruck für die Variablennamen festgelegt, den unser Parser akzeptiert (siehe Kapitel Reguläre Ausdrücke):

__assignpattern=re.compile("(?P<assign>^[A-Za-z][A-Za-z0-9]*\s*=)")

Da dieses Attribut (so nennt man Variablen in einer Klasse) ohne „self.” vor dem Variablennamen angelegt wurde, ist es eine Klassenvariable. Da ihr Name mit doppeltem Unterstrich beginnt, ist es eine private Klassenvariable, auf die wir nur innerhalb der Klasse Zugriff haben. Sie existiert also bereits, bevor wir eine Instanz dieser Klasse erzeugt haben. Innerhalb der Klasse können wir also mit Klasse.__assignpattern auf sie zugreifen. Alternativ können Sie in einer Instanzmethode der Klasse auch die Form: type(self).__assignpattern nutzen. Auch self.__assignpattern funktioniert, ist aber gefährlich: Falls ein Instanzattribut __assignpattern existiert, nutzt letztere Form die Instanzvariable und nicht die Klassenvariable(!). Das „\s” in dem Muster steht für beliebige Whitespacezeichen (siehe unten), auch mehrere hintereinander.

Als nächstes reservieren wir einige Namen, die vom Benutzer unseres Rechners nicht als Variablennamen verwendet werden können, da es Namen für mathematische Funktionen (wie z. B. sin für die Sinus-Funktion) sind, die der Benutzer verwenden können soll, und die er nicht versehentlich durch einen Variablennamen überschreiben können soll. Dazu verwenden wir die private Liste __reservedNames.

Die Zeile @classmethod #... ist ein „Decorator” (Dekorator), die mit Python 2.4 eingeführt wurden. Ein Decorator beginnt immer mit dem „@” direkt gefolgt vom Namen. Einen Decorator kann man auch selbst definieren (und er kann sogar Argumente haben), hier benutzen wir aber nur den von Python vorgegeben Decorator @classmethod, der angibt, dass die folgende Methode (bei uns „evaluateExpression”) eine Klassenmethode ist. Klassenmethoden existieren (ähnlich wie Klassenvariablen) unabhängig von der Instanz einer Klasse. Sie können mit Klassenname.Methodenname(...) aufgerufen werden, ohne dass vorher ein Objekt dieser Klasse instanziert wurde.

Eine Klassenmethode kann man alternativ auch über ein Objekt (Instanz) der Klasse aufrufen, z. B. mit instanz=ExpressionParser() und anschließend z. B. rslt,err=instanz.evaluateExpression(expr). Wie Sie sehen, benötigt eine Klasse keinen Konstruktor (__init__ Methode), um sie instanzieren zu können. Wenn der Konstruktor fehlt, stellt Python einen Default-Konstruktor bereit. Beim Aufruf einer Methode über ein Objekt, wird aber (siehe oben) als erster Parameter die Instanz (normalerweise self genannt) automatisch übergeben (auch wenn diese beim Aufruf nicht angegeben wird), weshalb unsere Klassenmethode dieses Argument bei der Definition in:

def evaluateExpression(cls,expression): # ...

als erstes Argument enthalten muss. Bei einer Klassenmethode verwendet man aber i. allg. per Konvention nicht self sondern einen anderen Namen, häufig cls für class (class dürfen Sie aber nicht als Argumentnamen verwenden, da class ein Python-Schlüsselwort [zur Deklaration von Klassen] ist).

Die obige Methode „evaluateExpression” ist unsere (erweiterte) Funktion calcExpr__ aus unserem ursprünglichen Parser-Modul calcpars.py. (Da diese jetzt in einer eigenen Klasse liegt, müssen wir ihren Namen nicht sichern.)

Da gibt es einige Änderungen:

Als erstes entfernen wir sämtliche sämtlichen „Whitespace” vor und hinter dem übergebenen Ausdruck mittels der „.strip()” Methode:

expression=expression.strip() # ...

Beispiel: "  Ein String ".strip() ergibt: „Ein String”. Whitespace sind alle Zeichen, die „Leerraum” (also Leerzeichen, Tabulatorzeichen, Zeilentrennzeichen, ...) erzeugen.

Danach nehmen wir zunächst an, das der eingegebene Ausdruck keine Zuweisung ist: (assignement=None). None ist kein Schlüsselwort von Python, sondern eine der wenigen eingebauten Konstanten. Konstanten beginnen in Python mit einem Großbuchstaben, danach kommen Kleinbuchstaben. Weitere Konstanten sind z. B. True und False.

Solche Konstantennamen dürfen Sie genau so wie Schlüsselworte nicht als Variablennamen verwenden(!). None bedeutet, dass diese Variable keinen Wert hat. None bedeutet wirklich „Variable hat keinen Wert”(!). Z. B.

a=None; b=""; a == b

gibt in Idle False aus, wenn sie es im interaktiven Modus von Idle testen(!). Wie Sie sehen, dürfen Sie mehrere Python-Anweisungen innerhalb einer Zeile schreiben, wenn Sie die Anweisungen durch Semikolon trennen.

Jetzt kommt die eigentliche Logik, um eine Variablen-Zuweisung zu finden. Zunächst testen wir mittels der „.find” Methode, ob in dem Ausdruck ein „=” vorkommt (falls nicht, ist es keine Zuweisung). Wenn ja müssen wir testen, ob der erste Teil der Zuweisung unserem Muster aus dem regulären Ausdruck (siehe oben) entspricht:

assignement=cls.__assignpattern.search(expression)

Die .search(...)-Methode gibt die Konstante None zurück, wenn sie das Muster (Buchstabe optional gefolgt von Buchstaben und/oder Ziffern Whitespace und ein „=” Zeichen) nicht gefunden hat, ansonsten ein Match Object (assignement). Ein Match Object hat die Eigenschaft, dass es immer den boolschen Wert „True” hat. Da search() (und auch match()) None zurückgibt, wenn das Muster nicht gefunden wurde, kann man also mittels einer einfachen if-Anweisung testen, ob das Muster enthalten ist.

In der folgenden if-Anweisung testen wir ob das Muster nicht gefunden wurde. Falls es nicht gefunden wurde, geben wir, ähnlich wie die Fehlerrückgabe in unserer Funktion calcExpr__(), einen Fehler zurück, weshalb ich mir eine Erklärung spare. Solchen Code müssen Sie mittlerweile verstehen, falls nicht, arbeiten Sie die vorhergehenden Kapitel noch mal durch.

Falls wir das Muster für eine Zuweisung gefunden haben, nehmen wir zunächst an, dass kein Fehler auftreten wird (err=""). Danach testen wir, ob es sich bei dem eingegebenen Ausdruck um eine Zuweisung handelt (if (assignement)). Mittels unseres Match Objects „assignement” und der group()-Methode ermitteln wir zunächst den gefundenen String, der unserem Muster entspricht:

assign=assignement.group("assign")

Dies ist aber noch nicht der vom Benutzer eingegebene Variablenname, da das letzte Zeichen unseres Musters das Gleichheitszeichen (=) ist. Dieses können wir mittels folgender Anweisung entfernen, um den Variablennamen zu erhalten:

varname=assign[:-1].strip()

Das [:-1] ist ein „Slicing” Ausdruck. Dies wird gleich unten erklärt. Der Slicing-Ausdruck assign[:-1] ergibt den String in Assign ohne das letzte Zeichen. Im folgenden if-Block sehen wir nach, ob der vom Benutzer eingegebene Variablenname ein reservierter Name (siehe oben) ist, falls ja, geben wir einen Fehler zurück. Die Methode (isReservedName) mit der wir dies testen, wird weiter unten erklärt.

Mittels folgenden Zeile ermitteln wir zunächst die Länge unseres gefundenen Musters (also den Teil bis einschließlich des „=” Zeichens):

assignLength=len(assign)

Danach ermitteln wir mit unserer statischen, privaten __evaluate()-Methode (Beschreibung siehe unten) den Ausdruck, der nach dem „=” folgt. Um diesen Teilstring aus dem übergebenen Parameter „expression” zu ermitteln verwenden wir (wie schon oben) Slicing (Scheibenbildung). Slicing kann man in Python außer für Strings auch für andere Typen (z. B. Listen und Tupel) verwenden. Am besten erklärt man dies mit einigen Beispielen:

"0123456"[4:6] ergibt "45"
"0123456"[4:] ergibt "456"
"0123456"[4] ergibt "4"
"0123456"[:3] ergibt "012"
"0123456"[-3:-1] ergibt "45"

Zu beachten ist, dass das letzte Zeichen (siehe erstes Beispiel) nicht in dem Teilstring enthalten ist(!). Wie man am letzten Beispiel sieht, zählen negative Zahlen von hinten aus. Das Slicing in Python kann noch viel mehr, wenn Sie alle Möglichkeiten wissen wollen, lesen Sie selbst in der Python-Dokumentation nach. In unserem Fall übergeben wir ab der Länge des gefundenen Musters (nach dem „=”) den Rest des Parameters.

Falls beim Auswerten des Ausdrucks nach dem „=” ein Fehler aufgetreten ist, fügen wir vor dem zurückgegebenen Fehler-String (err) noch den Hinweis ein, dass der Fehler in einer Zuweisung auftrat. Ansonsten (else-Zweig) kommt jetzt die „große Schweinerei” mit Hilfe der anderen "Zauberfunktion" von Python, der exec()-Funktion, die eine komplette Python-Anweisung ausführt und somit noch viel gefährlicher als die eval()-Funktion ist. (Daher mussten wir auch die vielen obigen Überprüfungen einbauen, bevor wir die exec Funktion verwenden können.) Mit

_exec__(expression,globals())

führen wir die Benutzereingabe aus, das zweite Argument globals() gibt an, dass wir die Anweisung im globalen Namespace (Namensraum) ausführen wollen. Diese Anweisung bewirkt jetzt, dass die Variable mit dem Namen vor dem „=” in unserem Programm hinzugefügt wird. Die haben also nicht Sie, der Programmierer, sondern der Benutzer hinzugefügt, während Ihr Programm läuft. Dies nennt man „selbstverändernden Code” und so etwas macht man normalerweise nicht(!). Danach ist diese Variable (von der der Programmierer überhaupt nichts weiß) im Programm gesetzt, und der Benutzer kann diese in folgenden Ausdrücken verwenden.

Da wir hier nur ein relativ kleines Programm haben und vor allem den Teil vor und nach dem Zuweisungszeichen (=) geprüft haben, kann man das hier machen. In großen Programmen sollten Sie so etwas nicht machen, es wird dann schnell sehr unübersichtlich und daraus resultierende Fehler lassen sich nur schwer aufspüren. Nochmal die Warnung: Verwenden Sie die exec() und eval() Funktionen niemals in Programmen, auf die man vom Internet aus Zugriff hat! Z. B. wenn Sie Python als Skript-Sprache für Ihre Webseite nutzen. Trotz aller Prüfungen kann man nicht ausschließen, dass man einen speziellen Fall vergessen hat. Wenn diesen ein Angreifer findet, kann er mit der exec()-Funktion beliebigen Code auf Ihrem Webserver ausführen.

Jetzt wird Ihnen auch hoffentlich klar, warum ich alle Variablennamen so „komisch” benannt habe und die eingebauten Python-Funktionsnamen unter einem „komischen Namen” gesichert (umbenannt) habe: Da die alle mit Underscores (_) beginnen und enden und unser Muster (regulärer Ausdruck) keine Variablennamen mit Underscore erlaubt, kann der Benutzer keinen Namen für eigene Variablen verwenden, die unser Programm „kompromittieren” (zum Absturz bringen) kann - einige eingebaute Namen habe ich ziemlich sicher vergessen ;-)

Die exec()-Funktion haben wir natürlich in einen „try: except”-Block eingeschlossen. Sollte trotz unserer vorhergehenden Prüfungen noch ein Fehler aufgetreten sein, setzen wir einfach err und rslt (wie in unserem bisherigen Parser-Modul).

In dem nachfolgenden else-Teil (keine Zuweisung ) berechnen wir einfach den Ausdruck mit der __evaluate()-Methode (wie oben). Die return-Anweisung gibt einfach das Ergebnis und den Fehler, die in einem der obigen Zweige gesetzt wurden, zurück.

Die private Klassen-Methode __evaluate() entspricht unserer Funktion _calcExpr__() aus dem alten Parser-Modul (lediglich bei der Deklaration ist das zusätzliche Argument cls vorhanden, das aber beim Aufruf nicht angegeben wird). Hier hat sich aber nichts geändert. Sehen Sie sich daher gegebenenfalls noch mal die Erklärung im Kapitel Ein Tischrechner (2) an.

Fehlt noch die Formatierungsroutine für die Ausgabe des Ergebnisses. Hier gibt es eine bessere Möglichkeit, als wir in der Methode formatResult__() in unserem vorherigen Parser-Modul verwendet haben, die eingebaute Funktion round(). Diese Funktion ruft die Methode __round__() einer Klasse auf. Da in Python alles eine Klasse ist, auch Typen, können wir diese Funktion jeweils auf den Real- und Imaginärteil des Ergebnisses anwenden. Der Komplexe-Zahlen-Typ hat die Methode __round__() nicht implementiert, weshalb wir round() nicht direkt auf das Ergebnis (das komplex sein kann) anwenden können. Daher runden wir zunächst den Real- und Imaginärteil des Ergebnisses (rslt) und können dann direkt auf 0 (bzw. 0.0) testen. Ansonsten funktionert unsere öffentliche Klassenmethode formatResult() genau so wie die alte formatResult__()-Funktion. Allerdings werden jetzt alle Ergebnisse gerundet (nicht nur Werte nahe bei 0), was häufig zu „hübscheren” Ergebnissen führt.

Fehlt noch die oben verwendete Methode „isReservedName”. Auch dies ist eine Klassenmethode. In dieser testen wir einfach mittels des „in” Operators, ob der übergebene Parameter (name) in der Liste unserer reservierten Namen (__reservedNames, diese Liste ist ebenfalls eine Klassenvariable auf die wir mittels cls.__reservedNames zugreifen können.) enthalten ist. Falls ja geben wir True (wahr) zurück, sonst False (falsch).

Zum Schluss habe ich Ihnen noch ein Beispiel angegeben, wie Sie unseren Parser selbst um eigene Funktionen erweitern können. Die Funktion „sind(Winkel_in_Grad)” ermittelt den Sinus, wenn der Winkel in Grad (englisch degrees) angegeben wurde. Da es in Python die Funktion „radians” gibt, die einen Winkelwert von Grad ins Bogenmaß umwandelt, ist die Implementation trivial. Wichtig ist nur, dass Sie den neuen Funktionsnamen mittels der „.append” Methode zur Liste der reservierten Namen hinzufügen. Dazu müssen Sie jetzt aber den „mangled Name” (umgewandelten Namen) verwenden, da per Konvention Namen, die mit 2 Unterstrichen beginnen, als privat gelten. Wie unter objektorientierter Programmierung schon geschrieben, gibt es in Python nicht wirklich private Variablen. Einem Namen in einer Klasse, der mit zwei Unterstrichen beginnt, wird ein Unterstrich plus der Klassenname vorangestellt. Aus unserem __reservedNames wird also _ExpressionParser__reservedNames. Bitte gewöhnen Sie es sich für normale Programme nicht an, von außen auf als privat deklarierte Variablen zuzugreifen. Ich habe es hier nur gemacht, damit man den Parser um eigene Funktionen erweitern kann. Außerdem wird im selben Modul, in dem die Klasse definiert ist, auf die private Variable zugegriffen. Der Grund ist, dass wir keinen selbst geschriebenen Parser implementieren wollen (und in einer solchen Einführung auch nicht können), und wie geschrieben, was ich hier mache ist eine „große Schweinerei”.

Da der Parser jetzt in eine eigene Klasse ausgelagert wurde, können wir nicht mehr unser bisheriges GUI-Programm verwenden. Damit man die Parser-Klasse zunächst testen kann, habe ich folgendes kurzes Kommandozeilen-Programm geschrieben:

from exprpars import ExpressionParser as EP while (True): _expr__=input(">") if _expr__ == "": break _rslt__,_err__=EP.evaluateExpression(_expr__) # Alternatively you may use: #_parser__=EP() #_rslt__,_err__=_parser__.evaluateExpression(_expr__) if (_err__): print("Error: "+_err__) else: print(EP.formatResult(_rslt__))

Wie Sie sehen, ist dieses Programm dank unserer Parser-Klasse jetzt sehr kurz. Interessant ist hier die import-Anweisung. Mittels des „... as EP” benennen wir die Klasse ExpressionParser (nur in diesem Modul) in EP um, wodurch wir sie jetzt mit z. B. EP.evaluateExpression(_expr__) statt mit ExpressionParser.evaluateExpression(_expr__) aufrufen können (und müssen!). Zum Beenden drücken Sie nach dem Eingabeprompt (>) einfach die Eingabetaste.

Im nächsten Kapitel folgt dann noch die Version für die grafische Oberfläche. Mit wahlweiser Option das Dezimal-Komma statt des Dezimal-Punkts zu verwenden. Außerdem wird das Ergebnis (wahlweise) automatisch in die Zwischenablage gestellt.