why should i use @classmethod when i can call the constructor in the without the annotation?
Question:
I was looking into the advantages of @classmethods and figured that we can directly call the constructor from any method, in that case, why do we need a class method. Are there some advantages which i have missed.
Why this code, what are the advantages?
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year - year)
and not this code :-
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def fromBirthYear(name, year):
return Person(name, date.today().year - year)
Answers:
Because if you derive from Person, fromBirthYear will always return a Person object and not the derived class.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def fromBirthYear(name, year):
return Person(name, year)
class Fred(Person):
pass
print(Fred.fromBirthYear('bob', 2019))
Output:
<__main__.Person object at 0x6ffffcd7c88>
You would want Fred.fromBirthYear
to return a Fred object.
In the end the language will let you do a lot of things that you shouldn’t do.
Given
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def fromBirthYear(name, year):
return Person(name, date.today().year - year)
def __repr__(self):
return f"Person('{self.name}', {self.age})"
your code works find, as long as you don’t access fromBirthYear
via an instance of Person
:
>>> Person("bob", 2010)
Person('bob', 10)
However, invoking it from an instance of Person
will not:
>>> Person("bob", 2010).fromBirthYear("bob again", 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fromBirthYear() takes 2 positional arguments but 3 were given
This is due to how the function
type implements the descriptor protocol: access through an instance calls its __get__
method (which returns the method
object that “prepasses” the instance to the underlying function), while access through the class returns the function itself.
To make things more consistent, you can define fromBirthYear
as a static method, which always gives access to the underlying function whether accessed from the class or an instance:
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@staticmethod
def fromBirthYear(name, year):
return Person(name, date.today().year - year)
def __repr__(self):
return f"Person('{self.name}', {self.age})"
>>> Person.fromBirthYear("bob", 2010)
Person('bob', 10)
>>> Person.fromBirthYear("bob", 2010).fromBirthYear("bob again", 2015)
Person('bob again', 5)
Finally, a class method behaves somewhat like a static method, being consistent in the arguments received whether invoked from the class or an instance of the class. But, like an instance method, it does receive one implicit argument: the class itself, rather than the instance of the class. The benefit here is that the instance returned by the class method can be determined at runtime. Say you have a subclass of Person
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year - year)
def __repr__(self):
return f"Person('{self.name}', {self.age})"
class DifferentPerson(Person):
pass
Both classes can be used to call fromBirthYear
, but the return value now depends on the class which calls it.
>>> type(Person.fromBirthYear("bob", 2010))
<class '__main__.Person'>
>>> type(DifferentPerson.fromBirthYear("other bog", 2010))
<class '__main__.DifferentPerson'>
Using the @classmethod
decorator has the following effects:
-
The method is neatly documented as being intended for use this way.
-
Calling the method from an instance works:
>>> p = Person('Jayna', 43)
>>> p.fromBirthYear('Scott', 2003)
<__main__.Person object at 0x7f1c44e6aa60>
Whereas the other version will break:
>>> p = Person('Jayna', 43)
>>> p.fromBirthYear('Scott', 2003)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fromBirthYear() takes 2 positional arguments but 3 were given
>>> p.fromBirthYear(2003) # Jayna herself became the `name` argument
<__main__.Person object at 0x7f1c44e6a3a0>
>>> p.fromBirthYear(2003).name # this should be a string!
<__main__.Person object at 0x7f1c44e6a610>
- The class itself is passed as a parameter (this is the difference between
@classmethod
and @staticmethod
). This allows for various polymorphism tricks:
>>> class Base:
... _count = 0
... @classmethod
... def factory(cls):
... cls._count += 1
... print(f'{cls._count} {cls.__name__} instance(s) created via factory so far')
... return cls()
...
>>> class Derived(Base):
... _count = 0 # if we shadow the count here, it will be found by the `+=`
... @classmethod
... def factory(cls):
... print('making a derived instance')
... # super() not available in a `@staticmethod`
... return super().factory()
...
>>> Base.factory()
1 Base instance(s) created via factory so far
<__main__.Base object at 0x7f1c44e6a4f0>
>>>
>>> Derived.factory()
making a derived instance
1 Derived instance(s) created via factory so far
<__main__.Derived object at 0x7f1c44e63e20>
>>>
>>> Base().factory()
2 Base instance(s) created via factory so far
<__main__.Base object at 0x7f1c44e6a520>
>>>
>>> Derived().factory()
making a derived instance
2 Derived instance(s) created via factory so far
<__main__.Derived object at 0x7f1c44e63e20>
Note that Derived.factory
and Derived().factory
would create Derived
rather than Base
instances even if it weren’t overridden:
>>> class Derived(Base):
... _count = 0
... pass
...
>>> Derived.factory()
1 Derived instance(s) created via factory so far
<__main__.Derived object at 0x7f1c44e63e20>
This is only possible using @classmethod
, since otherwise there is no variable cls
to call and we are stuck with a hard-coded Base
. We would have to override the method to return Derived
explicitly, even if we didn’t want to change any other logic.
I was looking into the advantages of @classmethods and figured that we can directly call the constructor from any method, in that case, why do we need a class method. Are there some advantages which i have missed.
Why this code, what are the advantages?
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year - year)
and not this code :-
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def fromBirthYear(name, year):
return Person(name, date.today().year - year)
Because if you derive from Person, fromBirthYear will always return a Person object and not the derived class.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def fromBirthYear(name, year):
return Person(name, year)
class Fred(Person):
pass
print(Fred.fromBirthYear('bob', 2019))
Output:
<__main__.Person object at 0x6ffffcd7c88>
You would want Fred.fromBirthYear
to return a Fred object.
In the end the language will let you do a lot of things that you shouldn’t do.
Given
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def fromBirthYear(name, year):
return Person(name, date.today().year - year)
def __repr__(self):
return f"Person('{self.name}', {self.age})"
your code works find, as long as you don’t access fromBirthYear
via an instance of Person
:
>>> Person("bob", 2010)
Person('bob', 10)
However, invoking it from an instance of Person
will not:
>>> Person("bob", 2010).fromBirthYear("bob again", 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fromBirthYear() takes 2 positional arguments but 3 were given
This is due to how the function
type implements the descriptor protocol: access through an instance calls its __get__
method (which returns the method
object that “prepasses” the instance to the underlying function), while access through the class returns the function itself.
To make things more consistent, you can define fromBirthYear
as a static method, which always gives access to the underlying function whether accessed from the class or an instance:
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@staticmethod
def fromBirthYear(name, year):
return Person(name, date.today().year - year)
def __repr__(self):
return f"Person('{self.name}', {self.age})"
>>> Person.fromBirthYear("bob", 2010)
Person('bob', 10)
>>> Person.fromBirthYear("bob", 2010).fromBirthYear("bob again", 2015)
Person('bob again', 5)
Finally, a class method behaves somewhat like a static method, being consistent in the arguments received whether invoked from the class or an instance of the class. But, like an instance method, it does receive one implicit argument: the class itself, rather than the instance of the class. The benefit here is that the instance returned by the class method can be determined at runtime. Say you have a subclass of Person
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year - year)
def __repr__(self):
return f"Person('{self.name}', {self.age})"
class DifferentPerson(Person):
pass
Both classes can be used to call fromBirthYear
, but the return value now depends on the class which calls it.
>>> type(Person.fromBirthYear("bob", 2010))
<class '__main__.Person'>
>>> type(DifferentPerson.fromBirthYear("other bog", 2010))
<class '__main__.DifferentPerson'>
Using the @classmethod
decorator has the following effects:
-
The method is neatly documented as being intended for use this way.
-
Calling the method from an instance works:
>>> p = Person('Jayna', 43)
>>> p.fromBirthYear('Scott', 2003)
<__main__.Person object at 0x7f1c44e6aa60>
Whereas the other version will break:
>>> p = Person('Jayna', 43)
>>> p.fromBirthYear('Scott', 2003)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fromBirthYear() takes 2 positional arguments but 3 were given
>>> p.fromBirthYear(2003) # Jayna herself became the `name` argument
<__main__.Person object at 0x7f1c44e6a3a0>
>>> p.fromBirthYear(2003).name # this should be a string!
<__main__.Person object at 0x7f1c44e6a610>
- The class itself is passed as a parameter (this is the difference between
@classmethod
and@staticmethod
). This allows for various polymorphism tricks:
>>> class Base:
... _count = 0
... @classmethod
... def factory(cls):
... cls._count += 1
... print(f'{cls._count} {cls.__name__} instance(s) created via factory so far')
... return cls()
...
>>> class Derived(Base):
... _count = 0 # if we shadow the count here, it will be found by the `+=`
... @classmethod
... def factory(cls):
... print('making a derived instance')
... # super() not available in a `@staticmethod`
... return super().factory()
...
>>> Base.factory()
1 Base instance(s) created via factory so far
<__main__.Base object at 0x7f1c44e6a4f0>
>>>
>>> Derived.factory()
making a derived instance
1 Derived instance(s) created via factory so far
<__main__.Derived object at 0x7f1c44e63e20>
>>>
>>> Base().factory()
2 Base instance(s) created via factory so far
<__main__.Base object at 0x7f1c44e6a520>
>>>
>>> Derived().factory()
making a derived instance
2 Derived instance(s) created via factory so far
<__main__.Derived object at 0x7f1c44e63e20>
Note that Derived.factory
and Derived().factory
would create Derived
rather than Base
instances even if it weren’t overridden:
>>> class Derived(Base):
... _count = 0
... pass
...
>>> Derived.factory()
1 Derived instance(s) created via factory so far
<__main__.Derived object at 0x7f1c44e63e20>
This is only possible using @classmethod
, since otherwise there is no variable cls
to call and we are stuck with a hard-coded Base
. We would have to override the method to return Derived
explicitly, even if we didn’t want to change any other logic.