Programmieren mit Python

Expression Parser als Klasse

Damit unser Tischrechner Variablen unterstützen kann, müssen wir das Parser-Modul ändern. Dies habe ich zum Anlass genommen, aus dem Parser eine Klasse zu machen, und hier eine rudimentäre Einführung in die objektorientierte Programmierung zu bringen.

Objektorientierte Programmierung mit Klassen

Klassen bestehen im Allgemeinen aus Attributen und Methoden. Vereinfacht sind Attribute Variable und Methoden Funktionen. Beide gelten aber nur innerhalb der Klasse, bzw. Objekten (Instanzen) dieser Klasse. Ein Beispiel:

class Kontakt: # Ein Kontakteintrag counter=0 # Zähler, eine Klassenvariable. Diese # existiert nur einmal für alle Instanzen dieser Klasse def __init__(self,name,vorname="",telefon=""): # Dies ist der "Konstruktor", der # immer automatisch aufgerufen wird, wenn ein neues Objekt (eine Instanz) # dieser Klasse erzeugt (angelegt) wird. type(self).counter += 1 # Zaehler (die Klassenvariable "counter") erhoehen und... self.__id = type(self).counter; # ...dem Identifikationsattribut der Instanz zuweisen self.__name = name # das Namens-Attribut (muss beim erzeugen angegeben werden) zuweisen self.vorname = vorname # das Vornamens-Attribut zuweisen self.telefon = telefon # Das Telefonnummern-Attribut zuweisen def __str__(self): # Objekt dieser Klasse als String zurueckgeben return "{}: {}, {}, {}".format(self.__id,self.__name,self.vorname,self.telefon) def name(self): # Name der Instanz (des Objekts) zurueckgeben; z. B.: return self.__name # print(KontaktObjekt.name()) # Klammern beachten! def setName(self,name): # Name (z. B. nach Heirat) aendern if (len(name) >= 2): # Name muss mindestens 2 Zeichen lang sein self.__name=name # z. B.: KontaktObjekt.setName("Meier") # Name zu Meier aendern #else: # Folgender Code (ich habe ihn daher auskommentiert, ist nicht # getestet (EINSCHLIESSLICH DES else)! #raise Exception('Name muss mindestens 2 Zeichen lang sein!') ## Das Programm funktioniert zwar auch ohne den "else"-Zweig (siehe ## das auskommentierte else nach dem if oben), aber Sie sollten, ## wenn Sie einen Fehler festgestellt haben, den Anwender (und ## somit auch den Programmierer) auf seinen Fehler explizit ## hinweisen, und nicht einfach (wie hier), die Aktion einfach ## stillschweigend nicht ausführen.

"counter" ist eine Klassenvariable, die für alle Objekte (Instanzen) dieser Klassen nur ein einziges mal existiert (d. h. deren Wert ist zu einem bestimmten Zeitpunkt für alle Instanzen der selbe).

Danach kann man jetzt diese Klasse "instanzieren" (ein Objekt dieser Klasse erzeugen):

k1 = Kontakt("Mustermann")

k2 = Kontakt("Conda", "Anna", "+49-911-1234567")

Dadurch wird sowohl für das Objekt k1, als auch k2 der Konstruktor der Klasse "Kontakt" (die von Python vorgegebnene Methode __init__) aufgerufen und danach ist k1 (mit Identifikationsnummer 1) jetzt das Objekt für die Person "Mustermann", Vorname und Telefonnummer sind nicht gesetzt (leerer String). Beim Aufruf wird der Parameter für das erste Argument (self) der Methodendefinition nicht angegeben. Dies wird erreicht, da in der Methodendefinition (unserem Konstruktor für die Klasse Kontakt) die Argumente "vorname" und "telefon" sogenannte Defaultparameter sind, indem ihnen mittels „ ="" ” ein Wert (hier leerer String) zugewiesen wird, der immer verwendet wird, wenn beim Aufruf dieser Parameter nicht angegeben wird. „k2” (mit Identifikationsnummer 2) ist jetzt das Objekt für die Person mit Nachnamen "Conda", Vornamen "Anna" und der Telefonnummer "+49-911-1234567". Da wir die __str__-Methode (Funktion) implementiert haben, können wir diese beiden Objekte jetzt einfach mit z. B. print(k2) ausgeben:

2: Conda, Anna, +49-911-1234567

Python ruft für viele Funktionen die Methode (Funktion) __str__() einer Klasse auf, wenn sie ein Objekt dieser Klasse übergeben, z. B. auch wenn Sie die str()-Funktion (die kennen Sie schon!) verwenden. Am besten testen Sie das mal in Idle.

Falls Sie die Telefonnummer von Anna Conda ändern wollen, so erreichen Sie das mit:

k2.telefon = "+49-9131-9876543"

Den Namen (z. B. weil Anna geheiratet hat) können Sie dagegen so nicht ändern. Probieren Sie es aus mit k2.__name="Bolika". Es kommt kein Fehler, aber geben Sie k2 mal mit der print()-Funktion aus. Es wird nach wie vor "Conda" als Name ausgegeben. Der Grund ist, dass __name ein privates Attribut der Klasse Kontakt ist (erkennbar an den beiden Unterstrichen vor dem Namen). Auf private Elemente (es gibt auch private Methoden) kann man nur von innerhalb der Klasse (also Methoden der Klasse) aus zugreifen. Damit versteckt man Elemente, auf die der Benutzer der Klasse nicht zugreifen können soll. Aber warum kam kein Fehler, als wir oben "Bolika" zugewiesen haben: In Python ist es (im Gegensatz zu compilierten Sprachen wie C++, C#, Java, ...) möglich Attribute zu einem bereits existierenden Objekt hinzuzufügen. Dies ist nicht schön, ließ sich aber wohl bei einer Interpretersprache nicht einfach vermeiden (vermutlich aus Performance-Gründen). Denken Sie also daran! k2 hat jetzt im Gegensatz zu k1 das zusätzliche öffentliche Attribut __name. Testen Sie es mit z. B. print(k2.__name)

Intern funktioniert es so, dass der Name "__name" innerhalb der Klasse automatisch bei jeder Verwendung umbenannt (mangled) wird. Wie er umbenannt wird, und wie sie dadurch auch Zugriff auf private Attribute/Methoden bekommen können, können Sie selbst nachlesen. Ich "verrate" es hier absichtlich nicht, da sie es sich angewöhnen sollten, von außen nicht auf privat deklarierte Namen zuzugreifen (in compilierten Sprachen ist dies gar nicht erst möglich) – der Autor einer Klasse hat sich schon etwas dabei gedacht, warum er ein Attribut/eine Methode als privat deklariert hat.

Aber natürlich müssen (sollten) wir den Namen ändern können (Heirat). Das können wir über die Methode setName() erreichen, die wir folgendermaßen aufrufen müssen:

setName("Bolika")

Der erste Parameter aus der Argumentliste der Methoden-Definition (self) muss (darf!) also NICHT angegeben werden. Bei der Definition der Methode in der Klasse MUSS er dagegen angegeben werden. Jetzt wird Ihnen vielleicht auch klar, warum wir das Namens-Attribut als privat deklariert haben (__name). Wir wollen sicherstellen, dass der Name immer mindestens 2 Zeichen lang ist. Dadurch dass wir nur über die setName()-Methode auf den Namen zugreifen können, ist dies sichergestellt. Dies (private Attribute und Zugriff nur über öffentliche Methoden) nennt man auch Kapselung. (In Produktionscode würde man natürlich auch im Konstruktor prüfen, ob der Name mindestens 2 Zeichen lang ist und falls nicht, z. B. eine Ausnahme mit dem Schlüsselwort raise werfen.)

Auch wenn wir auf den Namen eines Kontakt-Objekts zugreifen wollen, können wir das nicht direkt über KontaktObjekt.__name machen, sondern müssen die Methode .name() verwenden, z. B.:print(k1.name()) – beachten Sie die (leeren) Klammern nach dem Aufruf der Methode ".name"(!).

Jetzt noch einige Erklärungen zur Syntax bei Klassen. Auf Klassenvariablen greift man innerhalb der Klassendefinition über das Konstrukt type(self).variablenname zu. „type(self)” innerhalb der Klassendefinition bedeutet: Ermittle den Typ dieses (self) Objekts (ergibt hier die Klasse Kontakt) und verwende in diesem Typ (dieser Klasse) das Attribut (den Namen) "variablenname". Alternativ können Sie daher auch über Kontakt.counter auf den Zähler zugreifen. Aber Sie sollten es vermeiden, den Klassennamen hart zu codieren. Wenn Sie den Klassennamen nachträglich ändern, müssten Sie sonst den Klassennamen an vielen Stellen ändern, was fehlerträchtig ist. Im übrigen existiert eine Klassenvariable unabhängig von einer Instanz der Klasse. Wenn Sie z. B. feststellen wollen, wie viele Instanzen bisher von der Klasse Kontakt erzeugt wurden, können Sie das z. B. folgendermaßen ermitteln:

print(Kontakt.counter) # von außerhalb der Klasse können Sie natürlich nur den hart codierten Klassennamen verwenden. Natürlich sollten Sie in Produktionscode den Zähler (counter) ebenfalls als privat (__counter) deklarieren, damit er von außen nicht verändert werden kann (sonst kann es vorkommen, dass sie verschiedene Objekte mit gleicher Identifikationsnummer [__id] haben).

Dies funktioniert sogar, bevor überhaupt ein einziges Objekt der Klasse Kontakt angelegt wurde(!). D. h. Klassenattribute/Klassenvariablen existieren unabhängig von Instanzen/Objekten einer Klasse.

Auf Instanzvariablen/Attribute (die also zu einer Instanz/Objekt der Klasse gehören) greifen Sie innerhalb der Klassendefinition mittels des Konstrukts self.Attributnamen zu. Siehe oben den Konstruktor (die __init__()-Methode). Auf Methoden analog mit self.Methodenname. Von außerhalb der Klassendefinition stellen sie einfach den Objektnamen voran, wie z. .B. oben bei "k2.telefon".

Da dies mit den Klassenmethoden / Instanzmethoden etwas verwirrend ist, habe ich unten noch mal ein Beispiel (für die Kommandozeile) angegeben, mit dem (hoffentlich) klar wird, was man zu beachten hat.

Dies soll zunächst als rudimentäre Einführung in die objektorientierte Programmierung genügen, zumal wir das meiste für unsere Parser-Klasse gar nicht benötigen. Unsere Parser-Klasse benötigt nämlich weder Instanz-Attribute noch Instanz-Methoden (die nur über eine Instanz der Klasse aufgerufen werden können), sondern nur Klassenmethoden und Klassenvariablen, die auch aufgerufen werden können, wenn keine Instanz dieser Klasse existiert.

Im nächsten Kapitel kommt dann endlich der Expression Parser als Klasse mit der „großen Schweinerei”.


Beispielklasse für Unterschied Klassen-/Instanzmethoden (Sie sollten dieses Beispielprogramm herunterladen und in Idle ausprobieren!):

class MyClass: __cnt=0 def __init__(self): type(self).__cnt += 1 self.__id = type(self).__cnt #self.__cnt = -1 # uncomment, to see the fault in nextId2(self) below # Following (commented with "##") is WRONG! This code creates an # instance variable self.__cnt which is different from the class # variable MyClass.__cnt (or type(self).__cnt) and increments it # by 1. As default value of integer is 0, it is set to 1: ## def __init__(self): ## self.__cnt += 1 ## self.__id = self.__cnt def __str__(self): return "MyClass-Object with id: {}".format(self.__id) @classmethod def noOfObjects(cls): return cls.__cnt # here you must use cls.__cnt, as this # method has the @classmethod decorator. #return type(cls).__cnt # This is WRONG! Results in error: # AttributeError: type object 'type' has no attribute '_MyClass__cnt' # I. e. in a class method like this, you may AND MUST(!) always # use the simpler form cls.__cnt, even if the method is called # via an object instance (see below) def nextId(self): return type(self).__cnt+1 def nextId2(self): return self.__cnt+1 # this works but is not recommended! To see # why remove the comment character in "#self.__cnt = -1" in the # constructor (__init__(self) method) above print("No. of objects={}".format(MyClass.noOfObjects())) m1=MyClass() print(m1) # You can call a class method via an instance (object) or the class: print("No. of objects={}".format(m1.noOfObjects())) print("No. of objects (class)={}".format(MyClass.noOfObjects())) print("Next id for a {} object={}".format(type(m1),m1.nextId())) # Following gives different result if there is an instance variable .__cnt(!): print("Next id for a {} object={}".format(type(m1),m1.nextId2()))