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) 
Asked By: user145214

||

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.

Answered By: shrewmouse

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'>
Answered By: chepner

Using the @classmethod decorator has the following effects:

  1. The method is neatly documented as being intended for use this way.

  2. 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>
  1. 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.

Answered By: Karl Knechtel
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.