Python: Was ist der Unterschied zwischen @staticmethod und @classmethod?

Der mechanische Unterschied ist eine Zeile: Eine classmethod bekommt die Klasse, eine staticmethod bekommt nichts. Warum das wichtig ist, sieht man erst, wenn jemand von deiner Klasse ableitet — und dort verbringt dieser Artikel seine Zeit.

Was die beiden bekommen

class Demo:
    def instanzmethode(self, x): ...
    @classmethod
    def klassenmethode(cls, x): ...
    @staticmethod
    def statische_methode(x): ...
instance_method got self=Demo, x=1
class_method    got cls=Demo, x=1
static_method   got nothing but x=1

classmethod und staticmethod lassen sich beide direkt auf der Klasse aufrufen. Eine Instanzmethode nicht:

Demo.instance_method(2) -> TypeError: Demo.instance_method() missing 1 required positional argument: 'x'

Denn self wurde nie mitgegeben — Demo.instance_method ist bloß eine Funktion, und du müsstest selbst eine Instanz übergeben.

Das ganze Argument in zwei Zeilen

Hier ist der Grund, warum die Wahl zählt. Zwei Fabrikmethoden an einer Basisklasse, von jeder Sorte eine:

class Basis:
    @classmethod
    def mit_cls(cls):
        return cls()
    @staticmethod
    def mit_static():
        return Basis()

class Kind(Basis):
    pass
Child.make_cls()    -> Child
Child.make_static() -> Base   <- hardcoded Base

cls ist die Klasse, über die aufgerufen wurde, die classmethod baut also ein Kind. Die staticmethod nennt Basis in ihrem Rumpf und liefert deshalb immer eine Basis zurück, ganz gleich wer sie aufruft.

Das ist der gesamte Grund für classmethod, und deshalb müssen alternative Konstruktoren classmethods sein:

class Punkt:
    @classmethod
    def aus_string(cls, text):
        x, y = (int(t) for t in text.split(","))
        return cls(x, y)
Point.from_string('1,2')   : Point(1, 2)
Point3D.from_string('3,4') : Point3D(3, 4)   <- correct subclass

Schreib return Punkt(x, y) statt return cls(x, y) und jede abgeleitete Klasse bekommt stillschweigend den falschen Typ zurück. Es ist derselbe Fehler wie new self() gegenüber new static() in PHP, falls dir der schon einmal begegnet ist.

cls übernimmt auch überschriebene Attribute

Nicht nur den Typ, sondern alles was die abgeleitete Klasse geändert hat:

class Tier:
    laut = "..."
    @classmethod
    def beschreibe(cls):
        return f"{cls.__name__} macht {cls.laut}"
Dog.describe()        : Dog says woof
Dog.describe_static() : Animal says ...   <- still the base class

Eine classmethod ist also immer dann die richtige Wahl, wenn sich das Verhalten nach Einstellungen auf Klassenebene richten soll, die abgeleitete Klassen überschreiben dürfen.

Wann eine staticmethod richtig ist

Wenn die Funktion tatsächlich weder das eine noch das andere braucht. Sie ist ein Helfer, der der Lesbarkeit und der Einordnung wegen zur Klasse gehört, und sonst nichts:

class Temperatur:
    @staticmethod
    def c_nach_f(celsius):
        return celsius * 9 / 5 + 32

Kein self, kein cls, keine Klassenattribute. Wenn du eines davon willst, wolltest du eine classmethod.

Die ehrliche Alternative ist eine Funktion auf Modulebene. Eine staticmethod bringt dir die Gruppierung und die Möglichkeit, sie in einer Ableitung zu überschreiben; ist dir beides egal, ist die einfache Funktion einfacher.

Was diese Dinge tatsächlich sind

type(Demo.static_method)   : function
type(Demo.class_method)    : method
type(Demo.instance_method) : function
type(d.instance_method)    : method   <- bound

Eine staticmethod ist eine ganz gewöhnliche Funktion, die zufällig im Namensraum einer Klasse liegt — genau das steht oben. Eine classmethod ist bereits an die Klasse gebunden und zeigt sich deshalb auch über die Klasse als method. Und eine Instanzmethode ist an der Klasse eine function, an einer Instanz aber eine gebundene method — das ist das Deskriptor-Protokoll bei der Arbeit.

Seit Python 3.10 sind staticmethod-Objekte direkt aufrufbar, du kannst eine also schon im Klassenrumpf während der Definition verwenden. Das war vorher eine kleine Ärgerlichkeit.

Der Fehler, der überlebt

Vergiss den Dekorator und der Fehlschlag kommt verzögert:

class Ups:
    def helfer(x):
        return x * 2
Oops.helper(5)   : 10   <- works when called on the CLASS
Oops().helper(5) -> TypeError: Oops.helper() takes 1 positional argument but 2 were given

Auf der Klasse aufgerufen funktioniert es, weil nichts gebunden wird und die 5 in x landet. Auf einer Instanz aufgerufen wird die Instanz als erstes Argument übergeben, x wird zum Objekt, und dein eigentliches Argument hat keinen Platz mehr.

Die Fehlermeldung verwirrt beim ersten Mal — du hast ein Argument übergeben und sie sagt, es wären zwei. Das unsichtbare ist self.

Ein praktischer Hinweis

@staticmethod und @classmethod gehören ganz nach außen, also über alle anderen Dekoratoren:

class Geordnet:
    @staticmethod
    def richtig():
        return "ok"

Sie verpacken die Funktion in einen Deskriptor. Alles, was darüber steht, würde also den Deskriptor dekorieren und nicht die Funktion — und das tut fast nie das Gewünschte.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).