4

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) 
user145214
  • 43
  • 2
  • 1
    A regular method has to take `self` as the first parameter. – Barmar May 28 '20 at 19:27
  • 1
    https://stackoverflow.com/questions/12179271/meaning-of-classmethod-and-staticmethod-for-beginner – notacorn May 28 '20 at 19:27
  • It's most common way to create alternative constructors in python. Main benefit is that you can call method using class name. – Olvin Roght May 28 '20 at 19:28
  • Because the second code doesn’t work. Try it. – Mark Tolonen May 28 '20 at 19:29
  • @MarkTolonen It *kind of* works; accessing a function via a class (rather than an instance of the class) produces the function itself, not a method. A proper static method would produce the underlying function whether accessed from a class or an instance. – chepner May 28 '20 at 19:37
  • @chepner True but works only in some situations is still broken. – Mark Tolonen May 28 '20 at 19:52
  • fromBirthYear can only return a Person object. A derived class would want to return the an instance of the derived object not the parent class. See my answer. In addition you now have to repeat the word Person twice which is prone to errors. – shrewmouse May 28 '20 at 19:56

3 Answers3

3

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.

shrewmouse
  • 5,338
  • 3
  • 38
  • 43
1

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'>
chepner
  • 497,756
  • 71
  • 530
  • 681
1

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.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • Strictly speaking, `_count = 0` isn't necessary in `Derived` in order for `Derived` to get a separate instance count - the `+= 1` logic will create one in `Derived` if it doesn't exist. However, it would be created by adding one to the value found in `Base` in that situation. Thus, if `Base` instances were created before the first `Derived` instances, they would be reflected in the `Derived` count. – Karl Knechtel Jul 29 '22 at 03:13