Objektorientierte Programmierung OOP
Einführung
In den ersten Zeiten der Programmierung herrschte noch der sogenannte Spaghetticode vor, der nur vordergründig leichter zu lesen war. Um eine Strukturierung des Quellcodes zu erreichen, werden bei der objektorientierten Programmierung sogenannte Klassen erzeugt, die jeweils bestimmte Eigenschaften („Attribute“) und Funktionen („Methoden“) besitzen. Klassen werden in Python allgemein durch einzelne Zeichenketten mit großen Anfangsbuchstaben gekennzeichnet. Auch bei Klassen und Funktionsnamen gibt es gewisse Konvetionen, beispielsweise die so genannte Camel-Case-Variante. Grundsätzlich ist man hier aber frei, sollte sich selbst aber an eine Regel halten, um Quellcode lesbarer zu gestalten.
Objekte in Python
In Python, everything is an object!!!
print(isinstance(1, object)) print(isinstance(list(), object)) print(isinstance(True, object)) def foo(): pass print(isinstance(foo, object)) print("Ergebnis: Alles ist ein Objekt!!!")
Dieser Code zeigt, dass alles in Python in der Tat ein Objekt ist. Jedes Objekt enthält mindestens drei Daten:
- Referenzzähler
- Typ
- Wert
- Der Referenzzähler dient der Speicherverwaltung und ist für den reinen Anwender nicht weiter von Interesse.
- Der Typ wird auf der CPython-Ebene verwendet, um die Typsicherheit während der Laufzeit zu gewährleisten.
- Wert enthält den tatsächlich zugewiesenen Wert, welcher dem Objekt zugeordnet ist.
Nicht alle Objekte sind gleich. Es gibt eine weitere wichtige Unterscheidung: unveränderliche (immutuble) vs. veränderliche (mutuble) Objekte.
Unveränderliche vs. veränderliche Objekte
In Python gibt es zwei Arten von Objekten:
- Unveränderliche Objekte können nicht geändert werden.
- Veränderbare Objekte können geändert werden.
Typ | Immutable? |
---|---|
int | JA |
float | JA |
bool | JA |
complex | JA |
tuple | JA |
frozenset | JA |
str | JA |
list | Nein |
set | Nein |
dict | Nein |
Die Überprüfung kann mit der Funktion id() vorgenommen werden, die die Speicheradresse des Objekts zurückgibt. Die Funktion is überprüft, ob zwei Objekte auf dieselbe Speicheradresse verweisen:
x = 5 print(hex(id(x)),"\n")# durch hex() weniger Ziffern (16-er System) x += 1 print(x) print(hex(id(x)))# Neue Speicheradresse, also neues Objekt print() s = "Python in der ZEDAT" print(s) print(hex(id(s))) s += " rocks" print(s) print(hex(id(s)))# Neues Objekt
Der Versuch, die Zeichenfolge direkt zu mutieren, führt zu einem Fehler:
s = "Python in der ZEDAT" s[0] = "R"
Der Type str is immutable! Wir vergleichen es mit einem veränderbaren Objekt, wie list:
my_list = [1, 2, 3] print(my_list) print(hex(id(my_list))) my_list.append(4) print(my_list) print(hex(id(my_list)))# Dieselbe Speicheradresse!!
Im Gegensatz zum Beispiel am Anfang gibt es hier einen wesentlichen Unterschied. Auch nach Addition des Elements 4 hat die Liste dieselbe Objektadresse, sie ist bezüglich ihres Inhalts veränderbar (mutable)
my_list = [1, 2, 3] print(my_list) print(hex(id(my_list))) my_list[0] = 0 print(my_list) print(hex(id(my_list)))
listA = [1, 2, 3] print(listA) print(hex(id(listA))) listB = listA print(listB) print(hex(id(listB)))
Einfacher:
x = 5 print(x) print("x",hex(id(x))) y = x print(y) print("y",hex(id(y))) print(y is x) x += 1 print(x) print("x",hex(id(x))) print(y) print("y",hex(id(y))) print(y is x)
Man sieht, dass kein neues Python-Objekt erstellt wurde, nur ein neuer Name, der auf dasselbe Objekt verweist. Außerdem hat sich die Referenzzählung des Objekts um eins erhöht.
x = 1000 y = 1000 print(x is y)
Wie oben sind x und y beide Namen, die auf dasselbe Python-Objekt verweisen. Das Python-Objekt, das den Wert 1000 enthält, ist jedoch nicht immer auf derselben Speicheradresse zu finden. Würde man zwei Zahlen addieren, um 1000 zu erhalten, würde man am Ende eine andere Speicheradresse erhalten:
x = 1000 y = 1000 print(x is y) z = 499 + 501 print(x is z) z = 1000 - 1 + 1 print(x is z)
Der Ablauf ist wie folgt:
Hinweis: Die oben genannten Schritte werden nur ausgeführt, wenn dieser Code innerhalb eines Editors (REPL: Read-Eval-Print Loop) ausgeführt wird. Wenn das obige Beispiel in einer Datei gespeichert und dann ausgeführt wird, ergibt sich „x is y“ ist wahr (true)
# Hinweis voss >python Python 3.11.7 (main, Dec 4 2023, 18:10:11) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> x = 1000 >>> y = 501 + 499 >>> x is y False >>> z = 1000 + 1 - 1 >>> x is z False
Das gleiche Verhalten mit anderen Zahlen:
x = 20 y = 19 + 1 print(x is y)
Python erstellt intern bereits eine Anzahl an Objekten, die dann nicht neu kreiert werden müssen (bessere Performance)
- Integer von -5 bis 256
- Zeichenfolgen, die nur ASCII-Buchstaben, Ziffern oder Unterstriche enthalten
s1 = "Python" print(id(s1)) s2 = "Python" print(id(s2)) print(s1 is s2) s3 = "Python in der ZEDAT" print(id(s3)) s4 = "Python in der ZEDAT" print(id(s4)) print(s3 is s4)
s1 und s2 zeigen auf denselben Speicherplatz und sind daher beide gleich. s3 und s4 enthalten aber Leerzeichen, sodass für s4 ein neuer Speicherplatz reserviert wird.
Klassen in Python
Eine neue Klasse wird in Python mit dem Schlüsselwort class
, gefolgt vom Klassennamen
und einem Doppelpunkt eingeleitet. Alle darauf folgenden Definitionen von Eigenschaften
und Funktionen, die zur Klasse gehören, werden wie üblich eingerückt, üblicherweise um vier
Leerzeichen.
Python verwendet den Objektbegriff etwas unterschiedlich; man kann sagen: alles ist ein Objekt. Es gibt hier sowohl Klassen-, als auch Instanzobjekte, die sich stark von anderen OOP-Sprachen unterscheiden. Klassenobjekte bieten ein definiertes Standardverhalten und dienen als Basis zum Generieren von Instanzobjekten. Diese sind die realen Objekte, die von einer Anwendung erzeugt werden. Ein Instanzobjekt hat einen eigenen Namensraum und kopiert alle Namen aus dem Klassenobjekt, aus dem heraus es erstellt wurde.
Definition und Initialisierung eigener Klassen
Ebenso wichtig wie der Begriff einer Klasse ist der Begriff der Instanz einer Klasse. Wäh- rend beispielsweise die Klasse „Wecker“ einen Objekttyp eines meist unhöflich Lärm er- zeugenden Gerätes darstellt, so ist ein einzelner neben einem Bett stehender Wecker ein konkreter Vertreter dieser Klasse. Eine solche Instanz hat, zumindest in der Programmie- rung, stets alle in der Klasse definierten Eigenschaften und Funktionen, allerdings mit möglicher unterschiedlicher Ausprägung (beispielsweise Farbe oder Klingelton).
In Python könnte die Implementierung einer Beispielklasse etwa so aussehen, wie im folgenden Beispiel. Zu beachten ist, dass der Kopf einer Klasse unterschiedlich sein kann:
# Hinweis class Temperatur(object): # Python 2.x class Temperatur(): # Python 3.x class Temperatur: # Python 3.x (Alternative)
Alle Varianten können für Python 3 genutzt werden, aber nicht umgekehrt alle für Python 2
class Temperatur: # Klassenkopf """ Diese Klasse erlaubt das Speichern, Umrechnen und Ausgeben von Temperaturen! """ def __init__(self, celsius): # Initialisierungsmethode self.temperatur = celsius # temperatur -> Attribut def get_temperatur(self): # Objekt- und Klassenmethoden return self.temperatur def set_temperatur(self, celsius): # Methodenkopf self.temperatur = celsius def celsius_to_fahrenheit(self): # Methodenkopf fahrenheit = (1.8 * self.temperatur) + 32 return fahrenheit temp = Temperatur(-3) # temp ist eine Instanz der Klasse Temperatur print("Es sind gerade ",temp.get_temperatur(),"°C",sep="") temp.set_temperatur(-5) print("Es sind gerade ",temp.get_temperatur(),"°C",sep="")
Im obigen Beispiel wurde zunächst die Klasse Temperatur
mit folgendem docString definiert:
# Hinweis """ Diese Klasse erlaubt das Speichern, Umrechnen und Ausgeben von Temperaturen! """
Dieser liefert eine Beschreibung, falls die Klasse mit help(Temperatur)
aufgerufen wird, was
hier allerdings praktisch nicht funktioniert, da Python-Online auf dem Server in einem temporären Verzeichnis ausgeführt wird.
Laden Sie die Datei Temperatur.py herunter und testen das folgende kleine Programm auf
Ihrem Rechner mit idle3 oder einem anderen Editor.
# Hinweis: benötigt Temperatur.py import Temperatur help(Temperatur)
ergibt als Ausgabe, wenn Temperatur.py
im gleichen Verzeichnis wie das kleine Beispielprogramm
liegt:
# Hinweis: Ausgabe von help(Temperatur) >>> import Temperatur >>> help(Temperatur) Help on module Temperatur: NAME Temperatur CLASSES builtins.object Temperatur class Temperatur(builtins.object) | Temperatur(celsius) | | Diese Klasse erlaubt das Speichern, Umrechnen und Ausgeben von Temperaturen! | | Methods defined here: | | __init__(self, celsius) | Initialize self. See help(type(self)) for accurate signature. | | celsius_to_fahrenheit(self) | | get_temperatur(self) | | set_temperatur(self, celsius) | | ---------------------------------------------------------------------- | Data descriptors defined here: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined) FILE /Users/voss/Python/FU-Python/tutorials/10/Temperatur.py :
Da die Datei Temperatur.py
sowohl als eigenständiges Programm als auch als Modul
genutzt wird, muss intern eine Abfrage erfolgen, ob der Code als Modul oder Programm genutzt wird.
Deswegen findet man häufig am Ende von Python Programmen die folgenden Zeilen:
# Hinweis: Test, ob als Modul oder Programm aufgerufen if __name__ == "__main__": temp = Temperatur(-3) # temp ist eine Instanz der Klasse Temperatur print("Es sind gerade ",temp.get_temperatur(),"°C",sep="") temp.set_temperatur(-5) print("Es sind gerade ",temp.get_temperatur(),"°C",sep="")
Die Abfrage if __name__ == "__main__":
ergibt eine wahre Aussage, falls Der Code als
Hauptprogramm (main) genutzt wird, sodass die folgenden Zeilen in dem Fall auszuführen sind. Anderenfalls wäre
die interne Variable __name__
nicht gleich "__main__"
und würden dann ignoriert werden.
In der Funktion __init__()
wird anschließend festgelegt, wie eine neue Instanz der Klasse
erzeugt wird. Die angegebenen Argumente werden dabei als Eigenschaften der Instanz,
die in Python mit
Die Methode __init__()
wird automatisch aufgerufen, wenn man den Namen der Klasse
als Funktion aufruft. Im Folgenden Beispiel gleich zweimal:
class Temperatur: # Klassenkopf """ Diese Klasse erlaubt das Speichern, Umrechnen und Ausgeben von Temperaturen! """ def __init__(self, celsius): # Initialisierungsmethode self.temperatur = celsius # temperatur -> Attribut def get_temperatur(self): # Objekt- und Klassenmethoden return self.temperatur def set_temperatur(self, celsius): # Methodenkopf self.temperatur = celsius def celsius_to_fahrenheit(self): # Methodenkopf fahrenheit = (1.8 * self.temperatur) + 32 return fahrenheit temp = Temperatur(-3) # temp ist eine Instanz der Klasse Temperatur print("Es sind gerade ",temp.get_temperatur(),"°C",sep="") temp.set_temperatur(-5) print("Es sind gerade ",temp.get_temperatur(),"°C",sep="")
Eine neue Instanz einer Klasse lässt sich im Fall des obigen Beispiels
also folgendermaßen erzeugen: temp = Temperatur(-3)
. Es wird nur eine einzige Variable
erwartet: Der Temperaturwert, der in Celsius erwartet wird. Man sollte sinnvollerweise ein optionales Argument
zulassen, welches bei anderen Einheiten als Celsius automatisch die Umnrechnung vornimmt. Wird das optionale
Argument nicht angegeben, wird es automatisch durch unit="c"
auf Celsius gesetzt.
class Temperatur: # Klassenkopf """ Diese Klasse erlaubt das Speichern, Umrechnen und Ausgeben von Temperaturen! """ def __init__(self, degrees, unit="c"): # Initialisierungsmethode if unit.upper() == "C": self.temperatur = degrees # temperatur -> Attribut elif unit.upper() == "F": self.temperatur = self.fahrenheit_to_celsius(degrees) else: self.temperatur = self.kelvin_to_celsius(degrees) def get_temperatur(self): # Objekt- und Klassenmethoden return self.temperatur def set_temperatur(self, degrees, unit="c"): # Methodenkopf if unit.upper == "C": self.temperatur = degrees # temperatur -> Attribut elif unit.upper == "F": self.temperatur = self.fahrenheit_to_celsius(degrees) else: self.temperatur = self.kelvin_to_celsius(degrees) def fahrenheit_to_celsius(self, degrees): # Methodenkopf return (degrees - 32) / 1.8 def kelvin_to_celsius(self, kelvin): # Methodenkopf return (kelvin - 273) temp = Temperatur(-3) # temp ist eine Instanz der Klasse Temperatur print(type(temp)) print("Es sind gerade ",temp.get_temperatur(),"°C",sep="") temp.set_temperatur(300, unit="K") print("Es sind gerade ",temp.get_temperatur(),"°C",sep="") print() print('Typ von "temp" ist ',type(temp)) if isinstance(temp, Temperatur): print('"temp" ist eine Instanz der Klasse "Temperatur"') else: print('"temp" ist keine Instanz der Klasse "Temperatur"') Temp = "dummy" if isinstance(Temp, Temperatur): print('"Temp" ist eine Instanz der Klasse "Temperatur"') else: print('"Temp" ist keine Instanz der Klasse "Temperatur"')
Die Bedeutung der jeweiligen Funktionen (Methoden) kann man sich jeweils ausgeben lassen, insofern der Programmentwickler für entsprechende Texte gesorgt hat. In der zweiten Hälfte des folgenden Beispiels finden Kursteilnehmer mit Vorkenntnissen weitere Möglichkeiten der Ausgabe von Interna des Programmes:
""" circle.py: The circle module, which defines a Circle class. """ from math import pi class Circle: """A Circle instance models a circle with a radius""" def __init__(self, radius=1.0): """Initializer with default radius of 1.0""" self.radius = radius # Create an instance variable radius def __str__(self): """Return a descriptive string for this instance, invoked by print() and str()""" return 'This is a circle with radius of {:.2f}'.format(self.radius) def __repr__(self): """Return a formal string that can be used to re-create this instance, invoked by repr()""" return 'Circle(radius={})'.format(self.radius) def get_area(self): """Return the area of this Circle instance""" return self.radius * self.radius * pi # If this module is run under Python interpreter, __name__ is '__main__'. # If this module is imported into another module, __name__ is 'circle' (the module name). if __name__ == '__main__': c1 = Circle(2.1) # Construct an instance print("c1: ", c1) # Invoke __str__(): This is a circle with radius of 2.10 print("c1.get_area(): ", c1.get_area()) # 13.854423602330987 print("c1.radius: ", c1.radius) # 2.1 print("str(c1): ", str(c1)) # Invoke __str__(): This is a circle with radius of 2.10 print("repr(c1): ", repr(c1), "\n") # Invoke __repr__(): Circle(radius=2.1), see below for c3 c2 = Circle() # Default radius print("c2: ", c2) print("c2.get_area(): ", c2.get_area(), " square units\n") # Invoke member method c2.color = 'red' # Create a new attribute for this instance via assignment print("c2.color: ", c2.color, "\n") #print(c1.color) # Error - c1 has no attribute color c3 = repr(c1) # Use definition of c1 print("c3: ", c3, "\n") # Test doc-strings print("__doc__: ", __doc__) # This module print("Circle.__doc__: ", Circle.__doc__) # Circle class print("Circle.get_area.__doc__: ", Circle.get_area.__doc__, "\n") # get_area() method print(isinstance(c1, Circle)) # True print(isinstance(c2, Circle)) # True print(isinstance(c1, str)) # False print() print(dir()) # Return the list of names in the current local scope print(__name__) print(dir(c1)) # List all attributes including built-ins print(vars(c1)) # Return a dictionary of instance variables kept in __dict__ print(c1.__dict__) # Same as vars(c1) print(c1.__class__) # print(type(c1)) # Same as c1.__class__ print(c1.__doc__) print(c1.__module__) print(c1.__init__) print(c1.__str__) print(c1.__str__()) # or str(c1), or print(c1) print(c1.__repr__()) # or repr(c1) print(c1) # same as c1.__repr__() print(Circle(radius=2.100000)) print(c1.radius) print(c1.get_area) print(c1.get_area()) # Same as Circle.get_area(c1) # Inspect "instance" object c2 print(dir(c2)) print(type(c2)) # or c2.__class__ print(vars(c2)) # or c2.__dict__ print(c2.radius) print(c2.color) print(c2.__init__) print() # Inspect the "class" object Circle print(dir(Circle)) # List all attributes for Circle object print(help(Circle)) # Show documentation print(Circle.__class__) print(Circle.__dict__) # or vars(Circle) print(Circle.__doc__) print(Circle.__init__) print(Circle.__str__) print(Circle.__str__(c1)) # Same as c1.__str__() or str(c1) or print(c1) print(Circle.get_area) print(Circle.get_area(c1)) # Same as c1.get_area()
Durch type(objektname)
kann allgemein angezeigt werden, zu welcher Klasse ein beliebiges
Python-Objekt gehört; ebenso kann mit isinstance(objektname, klassenname)
geprüft werden, ob ein Objekt eine Instanz der angegebenen Klasse ist. Der Objektname muss hierbei
existieren.
Möchte man eine konkrete Instanz wieder löschen, so ist dies allgemein mittels
del(name_der_instanz)
möglich, im obigen Fall also mittels del(temp)
.
Allerdings erfolgt die Löschung nur dann, wenn auch die letzte Referenz auf
die Instanz gelöscht wird. Sind beispielsweise tempA=tempB=Temperatur(10,unit="F"))
zwei Referenzen auf die gleiche
Instanz erzeugt worden, so wird mit del(tempA)
nur die erste Referenz gelöscht; die Instanz
selbst bleibt weiter bestehen, da die Variable tempB
immer noch darauf verweist.
Beim Löschen der letzten Referenz auf wird automatisch Pythons „Garbage Collector“ aktiv
und übernimmt die Aufräumarbeiten, indem die entsprechende Methode __del__()
aufgerufen wird und der zugehörige Arbeitsspeicher freigegeben wird.
Sollen bei der Löschung einer Instanz weitere Aufgaben abgearbeitet werden, so können
diese in einer zu definierenden Funktion __del__()
innerhalb der Klasse festgelegt werden.
Geschützte und private Attribute und Methoden
In manchen Fällen möchte der Anwender verhindern, dass interne Funtionen oder Attribute durch den Anwender einer Klasse verändernt werden können. Python erlaubt für diesen Zweck sowohl geschützte („protected“) als auch private („private“) Attribute und Methoden:
class foo: def bar(x,y): return x**y + foo.__baz(x,y)# innerer Aufruf, daher kein Fehler def __baz(x,y): # kein Aufruf von außen möglich! return x-y print(foo.bar(7,3)) # 7^3 + 7-3 #print(foo.__baz(7,3)) # ist eine private Methode von foo und ergibt daher von außen einen Fehler (Ausprobieren!)
Attribute sollten beispielsweise dann als geschützt oder privat gekennzeichnet werden, wenn sie nur bestimmte Werte annehmen sollen. In diesem Fall werden zusätzlich so genannte „Getter“ und „Setter“-Methoden definiert, deren Aufgabe es ist, nach einer Prüfung auf Korrektheit den Wert des entsprechenden Attributs auszugeben oder ihm einen neuen Wert zuzuweisen.
Setter- und Getter-Methoden werden bevorzugt als so genannte „Properties“ definiert; dazu wird folgende Syntax verwendet:
# Hinweis class MyClass: def __init__(self): self._myattr = None def get_myattr(self): return self._myattr def set_myattr(self, value): # todo: check if value is valid self._myattr = value myattr = property(get_myattr, set_myattr)
Durch die property()
-Funktion wird die Setter- und Getter-Methode eines geschützten
oder privaten Attributs dem gleichen, nicht-geschützten Attributnamen zugewiesen; myattr
ist daher
identisch zu _myattr
. Von
außerhalb der Klasse ist dieses gezielte Handling also nicht sichtbar, das entsprechende Attribut erscheint von außen also wie ein gewöhnliches Attribut. Dieses Prinzip der
Kapselung von Aufgaben ist typisch für objektorientierte Programmierung: Wichtig ist
es, die Aufgabe eines Objekts klar zu definieren sowie seine „Schnittstellen“, also seine
von außerhalb zugänglichen Attribute und Methoden, festzulegen. Solange das Objekt als
einzelner Baustein seine Aufgabe damit erfüllt, braucht man sich als Entwickler um die
Interna dieses Bausteins nicht weiter Gedanken zu machen.
Klassenmethoden, Instanzmethoden und statische Methoden
Eine Klassenmethode gehört zur Klasse, in der sie definiert wurde und ist daher eine Funktion der Klasse. Sie wird
mit dem sogenannten Dekorator @classmethod
definiert.
class MyClass: @classmethod def hello(cls): # cls -> Klasse print('Hello from', cls.__name__) MyClass.hello() # Standard myinstance1 = MyClass() # Erstellen einer Instanz myinstance1.hello() # Aufrufen der Klassenmethode über die Instanz
Instanzmethoden sind der häufigste Anwendungsfall, wobei sie von der Instanz (und keinem Klassenobjekt) aufgerufen werden. Das erste Argument ist immer die Instanz selbst (self):
class MyClass: def hello(self): print('Hello from', self.__class__.__name__) myinstance1 = MyClass() myinstance1.hello() #MyClass.hello() # Nicht möglich, gibt folgenden Fehler. Ausprobieren!! ## TypeError: hello() missing 1 required positional argument: 'self' MyClass.hello(myinstance1) # Ein Instanzobjekt als PArameter ist möglich
Eine statische Methode wird mit dem Dekorator @staticmethod
definiert. Sie kennt selbst nicht die Klasse, in der sie definiert wurde
und befindet sich dort aus rein organisatorischen Gründen.
Eimne statische Methode kann über ein Klassen- oder Instanzobjekt aufgerufen werden:
class MyClass: @staticmethod def hello(): print('Hello, world') myinstance1 = MyClass() myinstance1.hello() MyClass.hello() # Braucht kein Argument
Intern definierte Methoden
Der folgende Abschnitt ist nur für Fortgeschrittene; für andere geht es hier mit der VErerbung weiter!
Die Konstruktor- __init__()
und Dekonstruktormethode __del__()
einzelner Instanzen
wurden bereits erwähnt. Die Sonderbehandlung ergibt sich schon dadurch, dass mit dem Aufruf
von MyClass()
implizit der Aufruf MyClass.__init__()
erfolgt, um eine neue Instanz einer Klasse
zu erzeugen.
> Folgende intern definierte Methoden erlauben eine Sonderbehandlung:
__str()__
definiert eine Zeichenkette, die beim Aufruf von str(MyClass) als Objektbeschreibung ausgegeben
wird.__repr()
ist so definiert, dass Python-Code als Ergebnis zurückgegeben wird, bei dessen Ausführung eine neue Instanz der jeweiligen Klasse erzeugt wird.__call__()
kann als Instanz einer
Klasse wie eine Funktion aufgerufen werden, um Code auszuführen.
class MyCallable: def __init__(self, value): self.value = value def __call__(self): return 'Der gespeicherte Wert ist %s' % self.value if __name__ == '__main__': obj = MyCallable(4711) # Erstelle eine Instanz print(obj()) # Aufruf dieser Instanz, die __call__() ausführt
Ausschließlich für Zugriff auf Attribute vorgesehen sind:
__dict__
listet in Form eines dict
welche Attribute und zugehörigen Werte die jeweilige Instanz der Klasse aktuell beinhaltet.__slots__
(statisch) definiert eine Liste, welche Attributnamen die Instanzen einer Klasse haben.
Wird versucht, mit del(instanzname.attributname)
ein Attribut einer Instanz zu löschen, dessen Name in der __slots__
-Liste
enthalten ist, so gibt es einen AttributeError
. Umgekehrt gibt es auch einen FEhler,
wenn ein neuer Wert für ein Attribut einer Instanz fesetgelegt werden soll, dessen Name
nicht in der __slots__
-Liste enthalten ist.__getattr__()
definiert, wie sich die Klasse zu verhalten
hat, wenn ein angegebenes Attribut abgefragt wird, aber nicht existiert. Üblicherweise wird ein
AttributeError
ausgelöst.__getattribute__()
gibt das angegebene Attribut aus, sofern es existiert.
Andernfalls kann -- wie bei __getattr__()
-– wahlweise ein
Standard-Wert zurückgegeben oder ein AttributeError ausgelöst werden. __setattr__()
setzt ein Attribut auf den angegebenen Wert. Als Argumente werden der
Name des Attributs und der zuzuweisende Wert erwartet.__delattr__()
bestimmt, wie sich eine Instanz beim Aufruf
von del(instanzname.attributname)
verhalten soll.
Die standardmäßig definierten Funktionen sind getattr()
, setattr()
, hasattr()
und delattr()
:>/p>
class MyClass: """This class contains an instance variable called myvar""" def __init__(self, myvar): self.myvar = myvar myinstance = MyClass(8) print(myinstance.myvar) # 8 print(getattr(myinstance, 'myvar')) # 8 print(getattr(myinstance, 'no_var', 'default')) # default attr_name = 'myvar' print(getattr(myinstance, attr_name)) # Using a variable setattr(myinstance, 'myvar', 9) # Same as myinstance.myvar = 9 print(getattr(myinstance, 'myvar')) # 9 print(hasattr(myinstance, 'myvar')) # True delattr(myinstance, 'myvar') print(hasattr(myinstance, 'myvar')) # False>
Folgende Funktionen sind für den Vergleich zweier Objekte vorgesehen:
__eq__()
(„equal“), __ne__()
(„not equal“),
__gt__()
(„greater than“) und __ge__()
(„greater equal“) bestimmen, nach welchen Kriterien zwei Instanzen verglichen werden.__hash__()
gint einen zur angegebenen Instanz gehörenden Hashwert aus, der die Instanz als Objekt eindeutig identifiziert.Logische Operationen:
__bool__()
legt fest, in welchen Fällen eine Instanz den Wahrheitswert True oder False zurückgeben soll,
wenn bool(instanz)
aufgerufen wirdNumerische Operationen vorgesehen:
__int__()
, __oct__()
, __hex__()
, __float__()
, __long__()
und __complex__()
wird festgelegt, wie das Objekt durch einen Aufruf
von int(instanz)
, oct(instanz)
usw. in den jeweiligen numerischen Datentyp
umzuwandeln ist.__pos__()
und __neg__()
wird festgelegt, welche Ergebnisse die unären Operatoren +instanz
,
beziehungsweise -instanz
liefern sollen__abs__()
(Absolutbetrag) und __round__()
(Rundung) funktionieren wie die einfachen Funktionen, hier nur auf
eine Instanz bezogen.__add__()
, __sub__()
, __mul__()
und __truediv__()
wird
festgelegt, welches Ergebnis sich bei der Verknüpfung zweier Instanzen mit den
vier Grundrechenarten ergeben soll.
class myClass: def __init__(self,a): self.a = a def __add__(self,b): return self.a + b.a def __mul__(self,b): return self.a * b.a def __sub__(self,b): return self.a - b.a def __floordiv__(self,b): # needed by truediv return int(self.a / b.a) def __truediv__(self,b): return float(self.a) / float(b.a) def get(self): return self.a ob1 = myClass(11) ob2 = myClass(22) print(ob1.get(), ob2.get()) print(ob1+ob2) print(ob1*ob2) print(ob1-ob2) print(22/11,ob2/ob1,ob2//ob1)
Insbesondere bei der Definition einer Klasse, die Punkte definiert, macht es Sinn die Methoden der Addition, Subtraktion und Multiplikation neu zu definieren (zu überladen):
""" point.py: The point module, which defines the Point class """ class Point: """A Point instance models a 2D point with x and y coordinates""" def __init__(self, x = 0, y = 0): """Initializer, which creates the instance variables x and y with default of (0, 0)""" self.x = x self.y = y def __str__(self): """Return a descriptive string for this instance""" return '({}, {})'.format(self.x, self.y) def __repr__(self): """Return a command string to re-create this instance""" return 'Point(x={}, y={})'.format(self.x, self.y) def __add__(self, right): """Override the '+' operator: create and return a new instance""" p = Point(self.x + right.x, self.y + right.y) return p def __sub__(self, right): """Override the '+' operator: create and return a new instance""" p = Point(self.x + right.x, self.y + right.y) return p def __mul__(self, factor): """Override the '*' operator: modify and return this instance""" self.x *= factor self.y *= factor return self def __truediv__(self, denominator): """Override the '/' operator: modify and return this instance""" return self.__mul__(1/denominator) # Test if __name__ == '__main__': p1 = p3 = Point() print("1: ",p1,p3) # (0, 0) (0, 0) p1.x = 5 p1.y = 6 print("2: ",p1) # (5, 6) changed p2 = Point(3, 4) print("3: ",p2) # (3, 4) print("4: ",p1 + p2) # (8, 10) Same as p1.__add__(p2) p3 = p1 + p2 print("5: ",p3) print("6: ",p1) # (5, 6) No change print("7: ",p1 - p2) # (8, 10) Same as p1.__sub__(p2) print("8: ",p1) # (5, 6) No change print("9: ",p2 * 3) # (9, 12) Same as p2.__mul__(3) print("10: ",p2) # (9, 12) Changed print("11: ",p2 / 3) # (3.0, 4.0) Same as p2.__truediv__(3) print("12: ",p2) # (3.0, 4.0) Changed
__floordiv__()
definiert (instanz_1 // instanz_2) und
__mod__()
(Modulo-Rechnung) die Bedeutung von (instanz_1 % instanz_2),
analog zu den normalen FUnktionen.Wertzuweisungen
__iadd__()
, __isub__()
, __imul__()
und __itruediv__()
beschreiben die kombinierten Wertzuweisung zweier Instanzen aus den vier Grundrechenarten,
beispielsweise instanz_1 += instanz_2
__ifloordiv__()
definiert instanz_1 //= instanz_2
und
__imod__()
(Modulo-Rechnung) definiert instanz_1 %= instanz_2
Container, Slicings und Iteratoren
__len__()
, __contains__()
, __getitem__()
, __setitem__()
, __delitem__()
arbeiten allesamt wie ihre normalen Entsprechungen, hier nur bezogen auf Instanzen.Verwendung des Objekts innerhalb von with
-Konstrukten
__enter__()
definiert Anweisungen, die einmalig ausgeführt werden sollen, wenn eine Instanz der Klasse in eine with
-Umgebung geladen
wird.__exit__()
definiert Anweisungen, die einmalig ausgeführt werden sollen, wenn eine with
-Umgebung
um eine Instanz der Klasse wieder
verlassen wird.Vererbung
Mit dem Begriff „Vererbung“ wird ein in der Objekt-orientierten Programmierung sehr wichtiges Konzept bezeichnet, das es ermöglicht, bei der Definition von fein spezifizierten Objekte auf allgemeinere Basis-Klassen zurückzugreifen; die Basis-Klasse „vererbt“ dabei ihre Attribute und Methoden an die abgeleitete Sub-Klasse, wobei in dieser weitere Ei- genschaften hinzukommen oder die geerbten Eigenschaften angepasst werden können. Aus mathematischer Sicht ist die Basis-Klasse eine echte Teilmenge der daraus abgeleiteten Klasse, da diese alle Eigenschaften der ursprünglichen Klasse (und gegebenenfalls noch weitere) enthält.
Um die Eigenschaften einer Basis-Klasse in einer neuen Klasse zu übernehmen, muss diese bei der Klassendefinition in runden Klammern angegeben werden:
# Hinweis class SubClass(MyClass): pass
Bis auf diese Besonderheit werden alle Attribute und Methoden in einer abgeleiteten Klasse ebenso definiert wie in einer Klasse, die keine Basis-Klasse aufweist. In Python ist es prinzipiell möglich, dass eine Klasse auch mehrere Basis-Klassen aufweist; in diesem Fall werden die einzelnen Klassennamen bei der Definition der neuen Klasse durch Kommata getrennt angegeben. Die links stehende Klasse hat dabei beim Vererben der Eigenschaften die höchste Priorität, gleichnamige Attribute oder Methoden werden durch die weiteren Basis-Klassen also nicht überschrieben, sondern nur ergänzt. Da durch Mehrfach-Vererbungen allerdings keine eindeutige Baumstruktur mehr vorliegt, also nicht mehr auf den ersten Blick erkennbar ist, aus welcher Klasse die abgeleiteten Attribute und Methoden ursprünglich stammen, sollte Mehrfach-Vererbung nur in Ausnahmefällen und mit Vorsicht eingesetzt werden.
Das folgende Beispiel definiert die Unterklasse Zylinder, die die Eigenschaften von Circle erben soll, denn Deckel und Boden des ZTylinders sind Kreise.
"""cylinder.py: The cylinder module, which defines the Cylinder class""" from circle import Circle # Using the Circle class from the circle module circle.py class Cylinder(Circle): """The Cylinder class is a subclass of Circle""" def __init__(self, radius = 1.0, height = 1.0): """Initializer""" super().__init__(radius) # Invoke superclass' initializer (Python 3 syntax) self.height = height def __str__(self): """Self Description for print() and str()""" # If __str__ is missing in the subclass, print() will invoke the superclass version! return 'Cylinder(radius={},height={})'.format(self.radius, self.height) def __repr__(self): """Formal Description for repr()""" # If __repr__ is missing in the subclass, repr() will invoke the superclass version! return self.__str__() # re-direct to __str__() (not recommended) def get_volume(self): """Return the volume of the cylinder""" return self.get_area() * self.height # Inherited get_area() # For testing if __name__ == '__main__': cy1 = Cylinder(1.1, 2.2) # Output: Cylinder(radius=1.10,height=2.20) print(cy1) # Invoke __str__() print(cy1.get_area()) # Use inherited superclass' method print(cy1.get_volume()) # Invoke its method print(cy1.radius) print(cy1.height) print(str(cy1)) # Invoke __str__() print(repr(cy1)) # Invoke __repr__() cy2 = Cylinder() # Default radius and height print(cy2) # Output: Cylinder(radius=1.00,height=1.00) print(cy2.get_area()) print(cy2.get_volume()) print(dir(cy1)) # ['get_area', 'get_volume', 'height', 'radius', ...] print(Cylinder.get_area) # Inherited from the superclass print(Circle.get_area) print(issubclass(Cylinder, Circle)) # True print(issubclass(Circle, Cylinder)) # False print(isinstance(cy1, Cylinder)) # True print(isinstance(cy1, Circle)) # True (A subclass object is also a superclass object) print(Cylinder.__base__) # Show superclass:print(Circle.__subclasses__()) # Show a list of subclasses: [ ] c1 = Circle(3.3) print(c1) # Output: This is a circle with radius of 3.30 print(isinstance(c1, Circle)) # True print(isinstance(c1, Cylinder)) # False (A superclass object is NOT a subclass object)
Dekoratoren
Dekoratoren werden in Python als Kurzschreibweise verwendet, um bestimmte, innerhalb
einer Klasse definierte Methoden mit zusätzlichen Methoden zu „umhüllen“.
Der wohl wichtigste Dekorator ist @property :
Mit Hilfe dieses Dekorators kann eine get-Methode zu einem „Attribut“ gemacht werden, dessen Wert nicht statisch in einer
Variablen abgelegt ist, sondern dynamisch mittels der dekorierten get-Methode abgefragt wird.
Die grundlegende Funktionsweise ist folgende:
# Beispiel-Klasse definieren: class C: # Variable, die nur "intern" verwendet werden soll: _counter = 0 # get-Methode, mittels derer ein Wert ausgegeben werden soll: def get_counter(self): return self._counter # Markierung der get-Methode als Property counter = property(get_counter)
Anhand des obigen Beispiels kann man gut erkennen, dass in der Beispiel-Klasse C neben
der als nur zur internen Verwendung vorgesehenen Variablen _counter
auch noch ein
zweites Attribut counter
definiert wird, und zwar explizit als Aufruf von property()
.
Wird via c = C()
eine neue Instanz der Klasse erzeugt, so wird mittels c.counter
die
get_counter()
-Funktion aufgerufen. Die für einen Methoden-Aufruf typischen runden
Klammern entfallen also, von außen hat es den Anschein, als würde auf ein gewöhnli-
ches Attribut zugegriffen. Intern hingegen wird die _counter
-Variable, die womöglich an
anderen Stellen innerhalb der Klasse verändert wird, ausgegeben.
Als Kurzschreibweise für ähnliche Methoden-„Wrapper“ gibt es in Python folgende Syntax:
# Hinweis class C: _counter = 0 @property def get_counter(self): return self._counter
Die Zeile @property
wird dabei „Dekorator“ genannt. Diese Kurzschreibweise hat den
gleichen Effekt wie das obige, explizite Wrapping der zugehörigen Methode.
Generatoren und Iteratoren
Generatoren sind Objekte, die über eine __next__()
-Funktion verfügen, also bei jedem
Aufruf von next(generator_object)
einen neuen Rückgabewert liefern. Man kann sich
einen Generator also vorstellen wie eine Funktion, die mit jedem Aufruf das nächste
Element einer Liste zurückgibt. Kann der Generator keinen weiteren Rückgabewert liefern,
wird ein StopIteration-Error
ausgelöst. In anderen PRogrammiersprachen wird auch von Pointern (Zeigern)
gesprochen, die sowohl nach vorne als auch nach hinten verkettet werden können.
Der Vorteil von Generatoren gegenüber Listen liegt darin, dass auch bei sehr großen
Datenmengen kein großer Speicherbedarf nötig ist, da immer nur das jeweils aktuell zu-
rückgegebene Objekt im Speicher existiert. Beispielsweise handelt es sich auch bei
File-Objekten um Generatoren, die beim Aufruf von next(fileobject)
jeweils die nächste
Zeile der geöffneten ausgeben. Auf diese Weise kann der Inhalt einer (beliebig) großen
Datei beispielsweise mittels einer for
-Schleife abgearbeitet werden, ohne dass der Inhalt
der Datei auf einmal eingelesen und als Liste gespeichert werden muss.
Generatoren werden mit einer Syntax erzeugt, die der von "List Comprehensions" sehr ähn-
lich ist; es wird lediglich runde Klammern anstelle der eckigen Klammern zur Begrenzung
des Ausdrucks verwendet:
# List Comprehension: mylist = [i**2 for i in range(1,10)] print(mylist) # Ergebnis: [1, 4, 9, 16, 25, 36, 49, 64, 81] # Generator: mygen = (i**2 for i in range(1,10)) print(next(mygen)) # Ergebnis: 1 print(next(mygen)) # Ergebnis: 4 def generateValueList(): yield(11) yield(22) yield(33) g1 = generateValueList() print(g1) print(next(g1)) print(next(g1)) print(next(g1)) #print(next(g1)) # gibt einen Fehler, da kein Wert mehr in der Liste ist (Ausprobieren!) for item in generateValueList(): print(item, end=' ')
Generatoren können auch verschachtelt auftreten; bestehende Generatoren können also zur Konstruktion neuer Generatoren verwendet werden:
mygen = (i**2 for i in range(1,10)) # [1, 4, 9, 16, 25, 36, 49, 64, 81] # Neuen Generator mittels eines existierenden erzeugen: mygen2 = (i**2 for i in mygen) # [1, 16, 81, ... ] print(next(mygen2)) # Ergebnis: 1 print(next(mygen2)) # Ergebnis: 16 for item in mygen2: print(item, end=' ') print()
Python kann diese Art von verschachtelten Generatoren sehr schnell auswerten, so dass Generatoren allgemein sehr gut für die Auswertung großer Datenströme geeignet sind. Zu beachten ist lediglich, dass der Generator bei jedem Aufruf – ähnlich wie eine Funktion – einen Rückgabewert liefert, der von sich aus nicht im Speicher verbleibt, sondern entweder unmittelbar weiter verarbeitet oder manuell gespeichert werden muss.
Beispiele
Einfaches Spiel
# Hinweis class Spieler(): """ Die Regeln: Schere schneidet Papier. Papier wickelt Stein ein. Stein zerstört Schere. """ def __init__(self, name): self.name = name self.punkte = 0 self.wahl = "" def choose(self): self.wahl = input("{name}: Wähle Stein, Schere oder Papier: ".format(name= self.name)) print("{name} Wählt {wahl}".format(name=self.name, wahl = self.wahl)) def toNumericalChoice(self): switcher = { "Stein" : 0, "Papier": 1, "Schere": 2, } return switcher[self.wahl] def incrementPoint(self): self.punkte += 1 class SpielRunde(): def __init__(self, p1, p2): self.rules = [ [0, -1, 1], [1, 0, -1], [-1, 1, 0] ] p1.choose() p2.choose() ergebnis = self.compareChoices(p1,p2) print("Runde ergibt {ergebnis}".format(ergebnis = self.getResultAsString(ergebnis) )) if ergebnis > 0: p1.incrementPoint() elif ergebnis < 0: p2.incrementPoint() else: print("Unentschieden, kein Gewinner!") def compareChoices(self, p1, p2): return self.rules[p1.toNumericalChoice()][p2.toNumericalChoice()] def awardPoints(self): print("implement") def getResultAsString(self, ergebnis): res = { 0: "Unentschieden", 1: "Gewonnen", -1: "Verloren" } return res[ergebnis] class Spiel(): def __init__(self): self.endeSpiel = False self.Spieler1 = Spieler("Paul") self.Spieler2 = Spieler("Erna") def start(self): while not self.endeSpiel: SpielRunde(self.Spieler1, self.Spieler2) self.checkEndCondition() def checkEndCondition(self): answer = input("Weiterspielen j/n: ") if answer == 'j': SpielRunde(self.Spieler1, self.Spieler2) self.checkEndCondition() else: print("Spiele beendet. {p1name} hat {p1punkte} und {p2name} hat {p2punkte}".format( p1name = self.Spieler1.name, p1punkte= self.Spieler1.punkte, p2name=self.Spieler2.name, p2punkte=self.Spieler2.punkte)) self.bestimmeGewinner() self.endeSpiel = True def bestimmeGewinner(self): ergebnis_str = "Unentschieden" if self.Spieler1.punkte > self.Spieler2.punkte: ergebnis_str = "Gewinner ist {name}".format(name=self.Spieler1.name) elif self.Spieler1.punkte < self.Spieler2.punkte: ergebnis_str = "Gewinner ist {name}".format(name=self.Spieler2.name) print(ergebnis_str) spiel = Spiel() spiel.start()
Nächste Einheit: 11 Anwendungen III