The following code shows precisely what happens:
class A():
i = 0
def add_i(self):
print '\n-----In add_i function-----'
print 'BEFORE: a.__dict__ :',a.__dict__
print 'BEFORE: id(self) : %d\n' % id(self.i)
self.i = self.i + 1
print 'self.i = self.i + 1 done\n'
print 'AFTER: a.__dict__ :',a.__dict__
print 'AFTER: id(self) : %d' % id(self.i)
print '-----end of add_i function-----\n'
a = A()
print '\nA.i ==',A.i
print 'id(A.i) ==',id(A.i)
print 'A.__dict__.keys() :',A.__dict__.keys()
print '\na.i ==',a.i
print 'id(a.i) ==',id(a.i)
print 'a.__dict__.keys() :',a.__dict__.keys()
a.add_i()
print '\nA.i ==',A.i
print 'id(A.i) ==',id(A.i)
print 'A.__dict__.keys() :',A.__dict__.keys()
print '\na.i ==',a.i
print 'id(a.i) ==',id(a.i)
print 'a.__dict__.keys() :',a.__dict__.keys()
result
A.i == 0
id(A.i) == 10021948
A.__dict__.keys() : ['i', 'add_i', '__module__', '__doc__']
a.i == 0
id(a.i) == 10021948
a.__dict__.keys() : []
-----In add_i function-----
BEFORE: a.__dict__ : {}
BEFORE: id(self) : 10021948
self.i = self.i + 1 done
AFTER: a.__dict__ : {'i': 1}
AFTER: id(self) : 10021936
-----end of add_i function-----
A.i == 0
id(A.i) == 10021948
A.__dict__.keys() : ['i', 'add_i', '__module__', '__doc__']
a.i == 1
id(a.i) == 10021936
a.__dict__.keys() : ['i']
The __dict__ of an object exposes the namespace of the object, that is to say a mapping from names to objects holding its attributes.
Just after the class A and its instancea have been created, only A has an attribute i,
a doesn't.
In fact at this moment, the namespace of a is void, and when the intepreter encounters a.i it does that:
A class instance has a namespace implemented as a dictionary which is
the first place in which attribute references are searched. When an
attribute is not found there, and the instance’s class has an
attribute by that name, the search continues with the class
attributes. see 3 Data model 3.2 The standard type hierarchy
in the Python Language Reference
That is to say, as i isn't an a instance's attribute, the interpreter goes and search in the namespace of A: there it finds an attribute i and it returns this attribute as the result of the call of a.i
.
Now, when a.add_i() is executed, the instruction self.i = self.i + 1 is processed like follows:
firstly, self.i is searched: as i isn't an attribute of self (which is a), the returned object is in fact A.i.
secondly, self.i + 1 then creates a new object whose value is the incremented value of A.i. But now this object is DIFFERENT from the object of name A.i: it has a different identity, that is to say a different localisation in the memory.
thirdly, there's an assignement: self.i = .....
It means that the name i is created in the namespace of self (which is a) and binded to the just newly created object with incremented value.
So between the beginning of the add_i() function and its end, the meaning of self.i has changed.
Consequently, the meaning of a.i after the instruction a.add_i() is no more the same as before.
.
In-depth comprehension of Python processes requires to consider the games that identifiers (the names) and objects are playing in namespaces, there is no other way than that.