Kan iemand mij de betekenis uitleggen van @classmethod
en @staticmethod
in python? Ik moet het verschil en de betekenis weten.
Voor zover ik begrijp, vertelt @classmethod
een klasse dat het een methode is die moet worden overgeërfd in subklassen, of... iets. Maar wat's daar het nut van? Waarom niet gewoon de klasse methode definiëren zonder @classmethod
of @staticmethod
of enige @
definities toe te voegen?
tl;dr: wanneer moet ik ze gebruiken, waarom moet ik ze gebruiken, en hoe moet ik ze gebruiken?
Ik ben redelijk gevorderd met C++, dus het gebruik van meer geavanceerde programmeerconcepten zou geen probleem moeten zijn. Geef me gerust een bijbehorend C++ voorbeeld indien mogelijk.
Hoewel classmethod
en staticmethod
veel op elkaar lijken, is er toch een klein verschil in gebruik voor beide entiteiten: classmethod
moet een verwijzing naar een klasse-object als eerste parameter hebben, terwijl staticmethod
helemaal geen parameters kan hebben.
class Date(object):
def __init__(self, day=0, month=0, year=0):
self.day = day
self.month = month
self.year = year
@classmethod
def from_string(cls, date_as_string):
day, month, year = map(int, date_as_string.split('-'))
date1 = cls(day, month, year)
return date1
@staticmethod
def is_date_valid(date_as_string):
day, month, year = map(int, date_as_string.split('-'))
return day <= 31 and month <= 12 and year <= 3999
date2 = Date.from_string('11-09-2012')
is_date = Date.is_date_valid('11-09-2012')
Laten we uitgaan van een voorbeeld van een klasse, die zich bezighoudt met datuminformatie (dit zal onze boilerplate zijn):
class Date(object):
def __init__(self, day=0, month=0, year=0):
self.day = day
self.month = month
self.year = year
Deze klasse kan uiteraard worden gebruikt om informatie over bepaalde datums op te slaan (zonder informatie over de tijdzone; laten we aannemen dat alle datums in UTC worden weergegeven).
Hier hebben we __init__
, een typische initialisator van Python klasse-instanties, die argumenten ontvangt als een typische instancemethod
, met als eerste niet-optionele argument (self
) een verwijzing naar een nieuw aangemaakte instantie.
Klassenmethode
We hebben een aantal taken die mooi gedaan kunnen worden met classmethod
s.
Let's veronderstellen dat we een heleboel Date
klasse instanties willen maken met datum informatie afkomstig van een externe bron gecodeerd als een string met formaat 'dd-mm-yyyy'. Stel dat we dit op verschillende plaatsen in de broncode van ons project moeten doen.
Dus wat we hier moeten doen is:
Datum
door deze waarden door te geven aan de initialisatie-aanroep.Dit ziet er als volgt uit:
day, month, year = map(int, string_date.split('-'))
date1 = Date(day, month, year)
Voor dit doel kan C++ een dergelijke functie implementeren met overloading, maar Python ontbeert deze overloading. In plaats daarvan kunnen we classmethod
gebruiken. Laten's nog een "constructor" maken.
@classmethod
def from_string(cls, date_as_string):
day, month, year = map(int, date_as_string.split('-'))
date1 = cls(day, month, year)
return date1
date2 = Date.from_string('11-09-2012')
Laten's wat zorgvuldiger naar de bovenstaande implementatie kijken, en bekijken welke voordelen we hier hebben:
cls
is een object dat de klasse zelf bevat, niet een instantie van de klasse. Het is best cool, want als we onze Datum
klasse erven, zullen alle kinderen from_string
ook gedefinieerd hebben.Statische methode
Hoe zit het met staticmethod
? Het is vrij gelijkaardig aan classmethod
maar neemt geen verplichte parameters (zoals een class method of instance method doet).
Laten we eens kijken naar het volgende use-case.
We hebben een datum string die we op een of andere manier willen valideren. Deze taak is ook logisch gebonden aan de Datum
klasse die we tot nu toe hebben gebruikt, maar vereist geen instantiatie ervan.
Hier is waar staticmethod
nuttig kan zijn. Laten we eens kijken naar het volgende stukje code:
@staticmethod
def is_date_valid(date_as_string):
day, month, year = map(int, date_as_string.split('-'))
return day <= 31 and month <= 12 and year <= 3999
# usage:
is_date = Date.is_date_valid('11-09-2012')
Dus, zoals we kunnen zien aan het gebruik van staticmethod
, hebben we geen toegang tot wat de klasse is - het is in feite gewoon een functie, syntactisch opgeroepen als een methode, maar zonder toegang tot het object en zijn internals (velden en andere methoden), terwijl classmethod dat wel heeft.
Rostyslav Dzinko's antwoord is zeer toepasselijk. Ik dacht dat ik nog een andere reden kon benadrukken waarom je @classmethod
zou moeten kiezen boven @staticmethod
als je een extra constructor maakt.
In het voorbeeld hierboven, gebruikte Rostyslav de @classmethod
from_string
als een Factory om Date
objecten te maken van anders onacceptabele parameters. Hetzelfde kan gedaan worden met @staticmethod
zoals in de code hieronder staat:
class Date:
def __init__(self, month, day, year):
self.month = month
self.day = day
self.year = year
def display(self):
return "{0}-{1}-{2}".format(self.month, self.day, self.year)
@staticmethod
def millenium(month, day):
return Date(month, day, 2000)
new_year = Date(1, 1, 2013) # Creates a new Date object
millenium_new_year = Date.millenium(1, 1) # also creates a Date object.
# Proof:
new_year.display() # "1-1-2013"
millenium_new_year.display() # "1-1-2000"
isinstance(new_year, Date) # True
isinstance(millenium_new_year, Date) # True
Dus zowel new_year
als millenium_new_year
zijn instanties van de klasse Date
.
Maar, als je goed kijkt, het Factory proces is hard gecodeerd om Date
objecten te maken, wat er ook gebeurt. Wat dit betekent is dat zelfs als de Datum
klasse wordt ondergeclassificeerd, de subklassen nog steeds gewone Datum
objecten zullen maken (zonder eigenschappen van de subklasse). Zie dat in het voorbeeld hieronder:
class DateTime(Date):
def display(self):
return "{0}-{1}-{2} - 00:00:00PM".format(self.month, self.day, self.year)
datetime1 = DateTime(10, 10, 1990)
datetime2 = DateTime.millenium(10, 10)
isinstance(datetime1, DateTime) # True
isinstance(datetime2, DateTime) # False
datetime1.display() # returns "10-10-1990 - 00:00:00PM"
datetime2.display() # returns "10-10-2000" because it's not a DateTime object but a Date object. Check the implementation of the millenium method on the Date class for more details.
datetime2
is geen instantie van DateTime
? WTF? Nou, dat komt door de @staticmethod
decorator die gebruikt is.
In de meeste gevallen is dat ongewenst. Als je een Factory methode wilt die zich bewust is van de klasse die hem heeft aangeroepen, dan is @classmethod
wat je nodig hebt.
Herschrijven van Date.millenium
als (dat'is het enige deel van de bovenstaande code dat verandert):
@classmethod
def millenium(cls, month, day):
return cls(month, day, 2000)
zorgt ervoor dat de klasse
niet hard-coded is maar geleerd. cls
kan elke subklasse zijn. Het resulterende object
zal terecht een instantie van cls
zijn.
Laten we dat eens uittesten:
datetime1 = DateTime(10, 10, 1990)
datetime2 = DateTime.millenium(10, 10)
isinstance(datetime1, DateTime) # True
isinstance(datetime2, DateTime) # True
datetime1.display() # "10-10-1990 - 00:00:00PM"
datetime2.display() # "10-10-2000 - 00:00:00PM"
De reden is, zoals je nu wel weet, dat @classmethod
werd gebruikt in plaats van @staticmethod
@classmethod
betekent: wanneer deze methode wordt aangeroepen, geven we de klasse als eerste argument door in plaats van de instantie van die klasse (zoals we normaal doen met methodes). Dit betekent dat je de klasse en haar eigenschappen kunt gebruiken in die methode in plaats van een bepaalde instantie.
@staticmethod
betekent: wanneer deze methode wordt aangeroepen, geven we er geen instantie van de klasse aan door (zoals we normaal met methodes doen). Dit betekent dat je een functie binnen een klasse kunt plaatsen, maar dat je geen toegang hebt tot de instantie van die klasse (dit is nuttig wanneer je methode geen instantie gebruikt).