Search code examples
pythonclassmethodsinstantiationclass-method

Why are classmethods blind to normal methods when instantiated?


Questions

  1. Why are classmethods blind to normal methods when instantiated?
  2. Is there a way to pass self to allow classmethods see normal methods when instantiated?

Observations

Uninstantiated classmethods

  • When using a class method, the modification of any attribute will persist.
  • I expected each use of a class function to only temporarily store the attributes whilst the classmethod ran

Instantiated classmethods

  • Classmethods cannot use any variable that is set from a normal method
  • Even if the class is instantiated, the classmethods are still totally blind to anything that is done in normal methods!
  • This is unexpected behavior. I would expect classmethods to behave as normal methods once instantiated!

Example Code

def fmt_atts(self, att):
    """A simple formatter"""
    if hasattr(self, att): print '\t self.{:15s} = {}'.format(att, getattr(self, att))
    else: print "\t self.{:15s} does not exist".format(att)

def _report_vals(func):
    """This just reports the current state of values"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        self = args[0]
        print '> calling func: {} :: {}'.format(func.__name__, func.__doc__)
        res = func(*args, **kwargs)
        for att in self.all_attrs:
            fmt_atts(self, att)
        print '> end func: {}'.format(func.__name__)
        return res
    return wrapper

class Example(object):
    all_attrs = ['att_class', 'att_clsFnSet', 'att_initSet']
    att_class = 'set_in_class'

    @_report_vals
    def __init__(self):
        """setting an attribute and calling self.test2"""
        self.att_initSet = 'set_in_init'
        self.set_atts_in_classmethod()

    @classmethod
    @_report_vals
    def set_atts_in_classmethod(cls):
        """Sets attributes from within a classmethod"""
        cls.att_class = 'set_in_classmethod'
        cls.att_clsFnSet = 'set_in_classmethod'

    @classmethod
    @_report_vals
    def view_atts_from_classmethod(cls):
        """View attributes from a classmethod"""
        pass

    @_report_vals
    def view_atts_from_method(self):
        """View attributes from a normal method"""
        pass

    @_report_vals
    def set_atts_in_method(self):
        """Sets attributes from within a normal method"""
        self.att_class = 'set_in_method'
        self.att_clsFnSet = 'set_in_method'

if __name__ == '__main__':
    print '__without init'
    print '> calling `Example.att_class` directly'
    fmt_atts(Example, 'att_class')
    print '# comment: notice that `A.att_class` has a persisting value'
    Example.set_atts_in_classmethod()
    Example.view_atts_from_classmethod()

    print '\n__ post init: ex = Example()'
    ex = Example()
    print '# comment: notice that `self.att_initSet` has been set but not accessible from classmethod'
    ex.set_atts_in_classmethod()
    ex.view_atts_from_classmethod()
    print '# comment: notice that `self.att_initSet` was set in __init__ but not avialable!'
    print '# comment: however, `self.att_class` was set in another classmethod but *IS* accessible'
    ex.view_atts_from_method()
    print '# comment: notice that `self.att_initSet` is accessible from a normal method'
    ex.set_atts_in_method()
    ex.view_atts_from_classmethod()
    print '# comment: It appears that classmethods can only access attributes set by other classmethods'
    print '# comment: even when instanciated'

Output Example

__without init
> calling `Example.att_class` directly
        self.att_class       = set_in_class
# comment: notice that `A.att_class` has a persisting value
> calling func: set_atts_in_classmethod :: Sets attributes from within a classmethod
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     does not exist
> end func: set_atts_in_classmethod
> calling func: view_atts_from_classmethod :: View attributes from a classmethod
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     does not exist
> end func: view_atts_from_classmethod

__ post init: ex = Example()
> calling func: __init__ :: setting an attribute and calling self.test2
> calling func: set_atts_in_classmethod :: Sets attributes from within a classmethod
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     does not exist
> end func: set_atts_in_classmethod
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     = set_in_init
> end func: __init__
# comment: notice that `self.att_initSet` has been set but not accessible from classmethod
> calling func: set_atts_in_classmethod :: Sets attributes from within a classmethod
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     does not exist
> end func: set_atts_in_classmethod
> calling func: view_atts_from_classmethod :: View attributes from a classmethod
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     does not exist
> end func: view_atts_from_classmethod
# comment: notice that `self.att_initSet` was set in __init__ but not avialable!
# comment: however, `self.att_class` was set in another classmethod but *IS* accessible
> calling func: view_atts_from_method :: View attributes from a normal method
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     = set_in_init
> end func: view_atts_from_method
# comment: notice that `self.att_initSet` is accessible from a normal method
> calling func: set_atts_in_method :: Sets attributes from within a normal method
        self.att_class       = set_in_method
        self.att_clsFnSet    = set_in_method
        self.att_initSet     = set_in_init
> end func: set_atts_in_method
> calling func: view_atts_from_classmethod :: View attributes from a classmethod
        self.att_class       = set_in_classmethod
        self.att_clsFnSet    = set_in_classmethod
        self.att_initSet     does not exist
> end func: view_atts_from_classmethod
# comment: It appears that classmethods can only access attributes set by other classmethods
# comment: even when instantiated

Solution

  • Why are classmethods blind to normal methods when instantiated?

    Class (or static) methods are not meant to act on instances. They are methods that are common to all the instances of the class. Therefore, it is not logical for a class method to act on one specific instance.

    These are generally utilitary functions. For example, a Vector class could implement a dot_product as a class method, rather than an instance method. This would make sense, because such an operation involves more than one instance.

    Is there a way to pass self to allow classmethods see normal methods when instantiated?

    Yes, there is, though it does not really make sense.

    First off, a class method is able to see instance methods. For example:

    class Foo:
        def speak(self):
            print("Hello")
    
        @classmethod
        def make_speak(cls, instance):
            instance.speak()
    
    foo = Foo()
    Foo.make_speak(foo)
    

    Output:

    Hello
    

    Let's go further, and change a little the make_speak method:

    @classmethod
    def make_speak(cls):
        print(cls.speak)
    
    Foo.make_speak()
    

    Output:

    <function Foo.speak at 0x000001E58331EF28>
    

    What this output reveals, is that the speak method referred to by cls.speak actually corresponds to a speak method that has not been attached to any instance. This can be seen even clearer when calling that function:

    @classmethod
    def make_speak(cls):
        cls.speak()
    
    Foo.make_speak()
    

    Output:

    TypeError: speak() missing 1 required positional argument: 'self'
    

    An TypeError is raised, because the speak method from the Foo class needs one argument, which is not passed.

    Workaround

    Disclaimer: The following is not a good solution.

    The speak method needs an instance as argument to be run. Then, just give it:

    @classmethod
    def make_speak(cls, instance):
        cls.speak(instance)
    
    foo = Foo()
    Foo.make_speak(foo)
    

    Output:

    Hello
    

    But by doing this, you want the make_speak method to act on a specific Foo instance, so as to call its speak method.

    This is exactly tantamount to directly calling foo.speak.


    This example might seem obvious and dull. However, it highlights the fact that if a class method takes exactly one instance as argument (and potentially other things that are not instances of this class), then it is equivalent to an instance method.