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?
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})