Search code examples
pythonstatic-methodsclass-methodclass-variables

Class variable scope for static vs class methods


I discovered a weird behaviour (at least weird for me) on python class variables.

class Base(object):
    _var = 0

    @classmethod
    def inc_class(cls):
        cls._var += 1

    @staticmethod
    def inc_static():
        Base._var += 1

class A(Base):
    pass

class B(Base):
    pass

a = A()
b = B()

a.inc_class()
b.inc_class()
a.inc_static()
b.inc_static()

print(a._var)
print(b._var)
print(Base._var)

The output is 1 1 2.

This is surprising me (I was expecting 4 4 4) and I'm wondering why?


Solution

  • When decorated with @classmethod the first argument cls to inc_class(cls) is, well, the class. <class '__main__.A'> and <class '__main__.B'> respectively for A and B. So cls._var refers to A's _var, and similarly for B. In inc_static, decorated with @staticmethod there is no argument, you're explicitly referring to <class '__main__.Base'>, a different _var.

    Note the '_var': 0 attribute in Base's and A's __dict__. @classmethod is doing what you'd expect it to do, binding members to classes, in this case A and B.

    >>> Base.__dict__
    mappingproxy({'__module__': '__main__', '_var': 0, 'inc_class': <classmethod 
    object at 0x7f23037a8b38>, 'inc_static': <staticmethod object at 
    0x7f23037a8c18>, '__dict__': <attribute '__dict__' of 'Base' objects>, 
    '__weakref__': <attribute '__weakref__' of 'Base' objects>, '__doc__': None})
    
    >>> A.__dict__
    mappingproxy({'__module__': '__main__', '__doc__': None})`
    

    After calling Base.inc_static():

    >>> Base.__dict__
    mappingproxy({'__module__': '__main__', '_var': 1, 'inc_class': 
    <classmethod object at 0x7f23037a8b38>, 'inc_static': <staticmethod 
    object at 0x7f23037a8c18>, '__dict__': <attribute '__dict__' of 'Base' 
    objects>, '__weakref__': <attribute '__weakref__' of 'Base' objects>, 
    '__doc__': None})
    
    >>> A.__dict__
    mappingproxy({'__module__': '__main__', '__doc__': None})
    

    After calling A.inc_class():

    >>> Base.__dict__
    mappingproxy({'__module__': '__main__', '_var': 1, 'inc_class': 
    <classmethod object at 0x7f23037a8b38>, 'inc_static': <staticmethod 
    object at 0x7f23037a8c18>, '__dict__': <attribute '__dict__' of 'Base' 
    objects>, '__weakref__': <attribute '__weakref__' of 'Base' objects>, 
    '__doc__': None})
    
    >>> A.__dict__
    mappingproxy({'__module__': '__main__', '__doc__': None, '_var': 1})
    

    What's interesting is how A's _var is initialised. Note that you do cls._var += 1 before cls._var has been defined. As explained here, cls._var += 1 is equivalent to cls._var = cls._var; cls._var += 1. Because of the way python does lookup the first read of cls._var will fail in A and continue to find it in Base. At the assignment _var is added to A's __dict__ with the value of Base._var, and then all is fine.

    >>> class Base(object):
    ...     _var = 10
    ...     @classmethod
    ...     def inc_class(cls):
    ...         cls._var += 1
    ... 
    >>> class A(Base):
    ...     pass
    ... 
    >>> A.__dict__
    mappingproxy({'__module__': '__main__', '__doc__': None})
    >>> A.inc_class()
    >>> A.__dict__
    mappingproxy({'__module__': '__main__', '__doc__': None, '_var': 11})