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.

TypImmutable?
intJA
floatJA
boolJA
complexJA
tupleJA
frozensetJA
strJA
listNein
set Nein
dictNein

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:

  • Python-Objekt erstellen (1000)
  • x dem Objekt zuordnen
  • Python-Objekt erstellen (499)
  • Python-Objekt erstellen (501)
  • Zusammenfügen der beiden Objekte (rechte Seite)
  • Neues Python-Objekt erstellen (1000 -- linke Seite)
  • Assign the name y dem Objekt zuordnen

    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 self bezeichnet wird, gespeichert. Nach der Initialisierung stehen dann die angegebenen Funktionen (Methoden) zur Verfügung, die die Objektvariable speichern oder ausgeben.

    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:

  • Geschützte Attribute und Methoden werden durch einen einfach Unterstrich vor dem jeweiligen Namen gekennzeichnet. Auf derartige Attribute oder Methoden kann weiterhin von außerhalb der Klasse zugegriffen werden; der Unterstrich soll jedoch ein Hinweis sein, dass es sich prmär um eine interne Funktion handelt.
  • Private Attribute und Methoden werden durch einen doppelten Unterstrich vor dem jeweiligen Namen gekennzeichnet. Auf derartige Attribute kann von außerhalb der Klasse weder lesend noch schreibend zugegriffen werden; sie sind ausschließlich lokal zur Klasse.
  • 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 wird
  • Numerische 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