Klassen- und Instanzattribute



Klassenattribute

Gehorche Asimovs Gesetzen Bisher hatte jede Instanz einer Klasse ihre eigenen Attribute, die sich von denen anderer Instanzen unterschieden. Man bezeichnet dies als ,,nicht-statisch'' oder ,,dynamisch'', da sie für jede Instanz einer Klasse dynamisch erstellt werden. So hatten wir beispielsweise den Namen eines Roboters mit Hilfe des Instanzattribut self.__name gespeichert. Instanzattribute sind Attribute, die für jede Instanz in der Regel einen verschiedenen Wert annehmen, so wie ja jeder Roboter sinnvoller Weise einen anderen Namen haben sollte.

Wie kann man jedoch Informationen speichern, die sich nicht auf ein bestimmtes Objekt beziehen, sondern für die ganze Klasse relevant sind? Also Attribute, die für alle Instanzen gleich sind. Solche Attribute könnten für unsere Roboterklasse beispielsweise der Name des Herstellers, die Anzahl aller erzeugten Roboter oder wie in unserem folgenden Beispiel die Asimowschen Gesetze1 sein. Diese Attribute bezeichnet man als statische Attribute. Sie existieren unabhängig von den Instanzen, oder anders ausgedrückt, sie sind für alle Instanzen gleich. Statische Attribute werden auch als Klassenattribute bezeichnet, weil sie wie bereits gesagt, Eigenschaften bezeichnen, die für die ganze Klasse gelten und nicht nur für einzelne Objekte der Klasse. Ein Klassenattribut existiert pro Klasse nur einmal, wird also nur einmal angelegt. Instanzattribute werden für jedes Objekt angelegt. Statische Attribute werden außerhalb der Instanzmethoden direkt im class-Block definiert. Außerdem muss jedem Klassenattribut ein Initialwert zugewiesen werden. Es ist Usus, die statischen Member direkt unterhalb der class-Anweisung zu positionieren.



Wir könnten beispielsweise die Asimowschen Gesetze als Klassenattribut in unserer Roboterklasse formulieren, denn diese Gesetze gelten für jede Instanz, also für jeden Roboter, gleichermaßen. Wir benutzen für die Implementierung ein Tupel mit Strings, d.h. jedes Element des Tupels entspricht einem Gesetz:

class Roboter:
    Gesetze = (
"""Ein Roboter darf kein menschliches Wesen wissentlich
verletzen oder durch Untätigkeit gestatten, dass
einem menschlichen Wesen wissentlich Schaden zugefügt wird.""",
"""Ein Roboter muss den ihm von einem Menschen gegebenen
Befehlen gehorchen - es sei denn, ein solcher Befehl 
würde mit Regel eins kollidieren.""",
"""Ein Roboter muss seine Existenz beschützen, so lange
dieser Schutz nicht mit Regel eins oder zwei 
kollidiert.""")
    def __init__(self, name, baujahr):
        self.__name = name
        self.__baujahr = baujahr
    # weitere Definitionen wie gehabt

Auf die Robotergesetze können wir entweder direkt über den Klassennamen mit print(Roboter.Gesetze) oder über eine Instanz eines Roboters zugreifen:

for nummer, text in enumerate(Roboter.Gesetze):
    print(str(nummer+1) + ":\n" + text)
1:
Ein Roboter darf kein menschliches Wesen wissentlich
verletzen oder durch Untätigkeit gestatten, dass
einem menschlichen Wesen wissentlich Schaden zugefügt wird.
2:
Ein Roboter muss den ihm von einem Menschen gegebenen
Befehlen gehorchen - es sei denn, ein solcher Befehl 
würde mit Regel eins kollidieren.
3:
Ein Roboter muss seine Existenz beschützen, so lange
dieser Schutz nicht mit Regel eins oder zwei 
kollidiert.

Im folgenden Beispiel zeigen wir, wie man Instanzen mittels einer Klassenvariablen zählen kann. Dazu erhöhen wir die Variable counter bei der Initialisierung jeder neuen Instanz. Wird eine Instanz gelöscht, wird die Methode __del__ aufgerufen, in der in unserem Beispiel die Klassenvariable counter um 1 vermindert wird:

class C: 
    counter = 0
    
    def __init__(self): 
        type(self).counter += 1
    def __del__(self):
        type(self).counter -= 1
x = C()
print("Anzahl der Instanzen: " + str(C.counter))
y = C()
print("Anzahl der Instanzen: " + str(C.counter))
del x
print("Anzahl der Instanzen: " + str(C.counter))
del y
print("Anzahl der Instanzen: " + str(C.counter))
Anzahl der Instanzen: 1
Anzahl der Instanzen: 2
Anzahl der Instanzen: 1
Anzahl der Instanzen: 0

Im Prinzip hätten wir auch C.counter statt type(self).counter schreiben können, denn type(self) wird zu ,,C'' ausgewertet. Aber wir werden später sehen, dass type(self) bei der Vererbung wesentlich wird.

Statische Methoden

Im vorigen Abschnitt hatten wir Klassenattribute als öffentliche Attribute verwendet. Selbstverständlich können und sollten wir auch Klassenattribute als private Attribute definieren können, also mit doppeltem vorangestellten Unterstrich. In diesem Fall brauchen wir aber eine Möglichkeit über Methoden die Werte zu lesen bzw. zu manipulieren. Man könnte dazu auch Instanzmethoden benutzen:

class Roboter:
    __counter = 0
    
    def __init__(self):
        # type(self) liefert als Typ "Roboter"
        type(self).__counter += 1
        
    def AnzahlRoboter(self):
        return Roboter.__counter
        
x = Roboter()
print(x.AnzahlRoboter())
y = Roboter()
print(x.AnzahlRoboter())
1
2

Dies ist nicht brauchbar, da zum einen die Anzahl der Roboter nichts mit einer einzelnen Instanz zu tun hat und zum anderen, weil wir die Anzahl nicht abfragen können, solange keine Instanzen bestehen. Versucht man über den Klassennamen auf die Methode zuzugreifen, erhält man die Fehlermeldung:

print(Roboter.AnzahlRoboter())
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-1e6d18d985c3> in <module>
----> 1 print(Roboter.AnzahlRoboter())
TypeError: AnzahlRoboter() missing 1 required positional argument: 'self'

Wie wäre es, wenn wir einfach das self in der Methode AnzahlRoboter weglassen? Um dies auszuprobieren, speichern wir einfach das folgende Skript unter robots_static_methods1.py ab:

class Roboter:
    __counter = 0
    
    def __init__(self):
        # type(self) liefert als Typ "Roboter"
        type(self).__counter += 1
        
 
    def AnzahlRoboter():
        return Roboter.__counter
    
    
print(Roboter.AnzahlRoboter())
0

Über den Klassennamen können wir nun zugreifen, aber nicht über Instanzen, wie wir im Folgenden sehen:

x = Roboter()
x.AnzahlRoboter()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-53b12e075a7b> in <module>
      1 x = Roboter()
----> 2 x.AnzahlRoboter()
TypeError: AnzahlRoboter() takes 0 positional arguments but 1 was given

Versuchen wir über eine Instanz zuzugreifen, betrachtet Python die Methode als Instanzmethode und Instanzmethoden müssen immer als erstes Argument eine Referenz auf die Instanz haben.

Um eine Methode der obigen Art so zu implementieren, dass man sowohl über Instanzen als auch über den Klassennamen zugreifen kann, benötigen wir statische Methoden.

Weil eine statische Methode nicht an eine Instanz gebunden ist, benötigt sie keinen Parameter self wie die Instanzmethoden. Steht in der Zeile vor einem Methodenheader @staticmethod wird die Methode zu einer statischen Methode. Man bezeichnet @staticmethod auch als einen Dekorator. Wir sehen im folgenden Beispiel, dass der Aufruf nun über den Klassennamen erfolgt und dass wir nun die Anzahl auch erfragen können, bevor eine erste Instanz angelegt worden ist:

class Roboter:
    counter = 0
    
    def __init__(self):
        # type(self) liefert als Typ "Roboter"
        type(self).counter += 1
        
    @staticmethod    
    def AnzahlRoboter():
        return Roboter.counter
print(Roboter.AnzahlRoboter())
x = Roboter()
print(Roboter.AnzahlRoboter())
print(x.AnzahlRoboter())
0
1
1

Klassenmethoden

Statische Methoden darf man nicht mit Klassenmethoden verwechseln. Klassenmethoden sind auch nicht an Instanzen gebunden, aber anders als statische Methoden, sind Klassenmethoden an eine Klasse gebunden. Das erste Argument einer Klassenmethode ist eine Referenz auf die Klasse, d.h. das Klassenobjekt. Aufrufen kann man sie über den Klassennamen oder eine Instanz.

class Roboter:
    counter = 0
   
    def __init__(self):
        type(self).counter += 1
        
    @classmethod    
    def AnzahlRoboter(cls):
        return cls, Roboter.counter
print(Roboter.AnzahlRoboter())
x = Roboter()
print(Roboter.AnzahlRoboter())
print(x.AnzahlRoboter())
(<class '__main__.Roboter'>, 0)
(<class '__main__.Roboter'>, 1)
(<class '__main__.Roboter'>, 1)


Für Klassenmethoden gibt es zwei Einsatzgebiete. Zum einen die sogenannte Fabrikmethoden, -Methoden, die neue Instanzen generieren -, auf die wir hier nicht näher eingehen, und zum anderen in den Fällen, in denen aus statischen Methoden auf andere statische Methoden zugegriffen werden muss. Dann müsste man den Klassennamen fest kodieren, was insbesondere bei der Vererbung ein Nachteil sein kann.

In der folgenden Bruchklasse benutzen wir eine statische Methode ggT, die den größten gemeinsamen Teiler von zwei Zahlen berechnet, und eine Klassenmethode kuerze, die zwei Zahlen in gekürzter Form zurückgibt. Bevor wir in init die Werte für Zaehler und Nenner abspeichern, berechnen wir die gekürzte Form von beiden durch den Aufruf self.kuerze(z,n). Hätten wir kuerze als statisiche Methode definiert oder, wie man normalerweise sagt mit @staticmethod dekoriert, dann müssten wir die Zeile g = cls.ggT(zaehler, nenner) durch g = Bruch.ggT(zaehler, nenner) austauschen:

class Bruch(object):
    def __init__(self,z,n):
        self.__z, self.__n = self.kuerze(z,n)
        
    def __str__(self):
        return str(self.__z)+'/'+str(self.__n)
    @staticmethod
    def ggT(a,b):
        while b != 0:
            a,b = b,a%b
        return a
    @classmethod
    def kuerze(cls, zaehler, nenner):
        g = cls.ggT(zaehler, nenner)
        return (zaehler // g, nenner // g)
x = Bruch(8,24)
print(x)
1/3

In unserem nächsten Beispiel wollen wir den Vorteil der Klassenmethoden bei der Verarbeitung demonstrieren. Wir definieren eine Klasse "Pets" mit einer Methode "about". Die Klassen "Dogs" und "Cats" erben von dieser Klasse. Sie erben auch die Methode "about". In unserer ersten Implementierung dekorieren wir die "about"-Methode als "staticmethod", um die Nachteile dieses Vorgehens zu zeigen:

class Pet:
    name = "pet animals"
    @staticmethod
    def about():
        print("This class is about {}!".format(Pet.name))   
    
class Dog(Pet):
    name = "'man's best friends' (Frederick II)"
class Cat(Pet):
    name = "cats"
p = Pet()
p.about()
d = Dog()
d.about()
c = Cat()
c.about()
This class is about pet animals!
This class is about pet animals!
This class is about pet animals!

Insbesondere im Fall von c.about() und d.about() hätten wir aussagekräftigere Sätze erwartet oder uns gewünscht.

Das "Problem" ist, dass die Methode "about" nicht weiß, dass sie von einer Instanz der Klasse Dogs oder Cats aufgerufen wird.


Im folgenden Code dekorieren wir die Methode nun mit dem "classmethod"-Dekorator:

class Pet:
    name = "pet animals"
    @classmethod
    def about(cls):
        print("This class is about {}!".format(cls.name))
    
class Dog(Pet):
    name = "'man's best friends' (Frederick II)"
class Cat(Pet):
    name = "cats"
p = Pet()
p.about()
d = Dog()
d.about()
c = Cat()
c.about()
This class is about pet animals!
This class is about 'man's best friends' (Frederick II)!
This class is about cats!

Fußnoten:

1 Isaac Asimov formulierte in seiner Kurzgeschichte ,,Runaround'' (Astounding, 1942) die Robotergesetze (englisch Three Laws of Robotics). Sie werden deshalb nach ihm auch als ,,Asimowsche Gesetze'' bezeichnet. Sie beschreiben die "Grundregeln des Roboterdienstes".