1

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?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
cabbi
  • 393
  • 2
  • 12
  • 2
    If you don't tell us what surprised you, it will be very hard to help you out. – DocDriven Jan 14 '19 at 22:50
  • 1
    Possible duplicate of [What is the difference between @staticmethod and @classmethod?](https://stackoverflow.com/questions/136097/what-is-the-difference-between-staticmethod-and-classmethod) – Jared Smith Jan 14 '19 at 22:50
  • Sorry but I did not find any answer to my question on the proposed duplicate thread – cabbi Jan 14 '19 at 22:56
  • The point of a duplicate is that the same answers will be given. Asking a duplicate question is not the best way to get the answer to a question that's already been asked; bounties are. – Pika Supports Ukraine Jan 14 '19 at 22:59
  • 1
    The point of my question is about the class variable, not the class & static methods – cabbi Jan 14 '19 at 23:06

2 Answers2

2

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})
Ramon
  • 1,169
  • 11
  • 25
  • what makes me crazy is: if I write "@classmethod def inc_class(cls): cls._var_boom += 1" I got an error, so python is NOT creating a new '_var_boom' variable for that class instance, but for '_var' it is! – cabbi Jan 15 '19 at 00:06
  • 1
    @cabbi I'm looking into it, but it seems inherited classes take the base's member if you try to use an undefined member of the same name. Note that `+=` expects an existing variable so it should indeed give an error, except with `_var` it picks it up from `Base`. – Ramon Jan 15 '19 at 00:11
  • 1
    [This](https://docs.python.org/3.4/library/operator.html#inplace-operators) explains it. `cls._var += 1` is equivalent to `cls._var = cls._var; cls._var += 1`. The read of `cls._var` will find `Base._var` and assign its value to a newly defined `A._var`, then the `+=` works fine. I can't find the documentation now for how Python does name lookup, but it probably goes up in scope until (if) it finds it, which is why `Base._var` is picked up before `A._var` is defined. – Ramon Jan 15 '19 at 00:25
  • with your above comment you FULLY explained me the "magic" python is doing with those variables – cabbi Jan 15 '19 at 00:29
  • Your last comment should definitely be part of your answer – cabbi Jan 15 '19 at 00:31
  • @cabbi Glad to be of help. Updated the answer as per your suggestion. – Ramon Jan 15 '19 at 00:42
1

Even though the two classes inherit from the Base class, they are completely different objects. Through the instantiation of a and b, you have two objects that belong to two separate classes. When you call

a.inc_class()
b.inc_class()

you increment the _var attribute of class A once, and then you do the same for class B. Even though they share the same name, they are different objects. If you had a second instance of class A, say a2, and you would call the function again, then both calls would manipulate the same variable. This explains how you get your first two outputs.

The third output refers to the Base class object. Again, even though it is the same name, it is a different object. You increment the 3rd object twice, therefore you get 2 as the answer.

DocDriven
  • 3,726
  • 6
  • 24
  • 53