Search code examples
pythonstatic-methodsread-eval-print-loopclass-method

Calling @classmethod decorated method throws a TypeError


I am trying to figure out difference between @staticmethod and @classmethod. The latter is passed a cls instance.

When I tried to call the @classmethod, it is giving me an error.

How should I call a @classmethod (to_c() and to_f()) decorated method in REPL?

Here is the REPL calls

>>> from temperature_converter import *
>>> c = TemperatureConverter(41)
>>> TemperatureConverter.to_f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Repos\Python\module-3\temperature_converter.py", line 21, in to_f
    return cls.c_to_f(cls.temperature)
  File "C:\Repos\Python\module-3\temperature_converter.py", line 25, in c_to_f
    return celsius * 9 / 5 + 32
TypeError: unsupported operand type(s) for *: 'property' and 'int'

Here is the class, TemperatureConverter

class TemperatureConverter:
    _temperature = 0

    @property
    def temperature(self):
        return self._temperature

    @temperature.setter
    def temperature(self, value):
        self._temperature = value

    def __init__(self, temperature):
        self.temperature = temperature

    @classmethod
    def to_c(cls):
        return cls.f_to_c(cls.temperature)

    @classmethod
    def to_f(cls):
        return cls.c_to_f(cls.temperature)

    @staticmethod
    def c_to_f(celsius):
        return celsius * 9 / 5 + 32

    @staticmethod
    def f_to_c(fahrenheit):
        return (fahrenheit - 32) * 5/9

Solution

  • Problem: You trying to access the temperature property from the class and expecting the same result as an instance, in this case an int value of 41. However, attributes are called differently in instances and classes. Compare the __get__ method for each:

    Description

    # Reassignments for illustration
    C = TemperatureConverter                                   # class  
    i = c                                                      # instance 
    attr = "temperature"                                       # attribute    
    
    # Call via Instance binding, `c.temperature`
    C.__dict__[attr].__get__(i, C)                     
    # 41
    
    # Call via Class binding, `C.temperature`
    C.__dict__[attr].__get__(None, C)                      
    # <property at 0x4ab9458>
    

    Here is more information on the signature of the __get__ method and a SO post on how descriptors work. In this case, the signature is could be seen as C.__get__(self, inst, cls). In short, when getting from a property, unlike an instance calls, calls from a class passes None for the instance argument.

    As shown above, the property object is returned if bound to the class:

    C.temperature
    # <property at 0x4ab9458>
    

    Can we still "get" the value from the property object? Let us call __get__ on the property object:

    C.temperature.__get__(i, C)
    # 41
    

    The latter shows it is possible to get the property value while bound to a class. While it may be tempting to implement the __get__ method on cls.temperature inside your class method, you still need to pass in an instance (e.g. i) to access the value of the property. Instances are not accessible in a class method. Therefore, we see why the property object is returned in your code, which is trying to multiply with an int and raises the error you observe.

    This is one explanation for your problem, in particular describing why you cannot access your property value cls.temperature within a class method.