maciekcube
maciekcube

Reputation: 71

Why does modifying a class attribute doesn't modify an object attribute in Python?

So as illustrated in the code snippet below, I can't fully understand how Python is accessing class variables.

class Test():
    var = 1

obj = Test()
obj2 = Test()
print(Test.var, '\t',obj.var, '\t', obj2.var)

obj.var += 1
print(Test.var, '\t',obj.var, '\t', obj2.var)

Test.var += 5
print(Test.var, '\t',obj.var, '\t', obj2.var)

Gives the following output:

1        1       1  # Makes sense
1        2       1  # Also makes sense
6        2       6  # I would expect obj.var to become '7'

The obj.var stays the same. Does that mean, that modifying obj.var created an object specific variable that is now independent of the class attribute var?

Upvotes: 2

Views: 1112

Answers (1)

Uri Granta
Uri Granta

Reputation: 1904

The confusion is probably in the obj.var += 1 instruction.

This is equivalent to obj.var = obj.var + 1. The right hand side can't find var on the object so delegates to the class. The left hand side, however, stores the result on the object. From that point onward, looking up var on the object will no longer delegate to the class attribute.

>>> class Test():
>>>    var = 1
>>> Test.__dict__
mappingproxy({'__module__': '__main__',
              'var': 1,
              '__dict__': <attribute '__dict__' of 'Test' objects>,
              '__weakref__': <attribute '__weakref__' of 'Test' objects>,
              '__doc__': None})
>>> obj = Test()
>>> obj.__dict__
{}
>>> obj.var += 1
>>> obj.__dict__
{'var': 2}

If you delete the object attribute then the class attribute is exposed again:

>>> Test.var += 5
>>> obj.var
2
>>> del obj.var
>>> obj.var
6

Relevant bit from the docs:

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.


Regarding your follow-on question, here is one way to do what you want using data descriptors, though I don't think it's very Pythonic. In particular, it's unusual to have data descriptors on classes (and may break things if you're not careful).

class VarDescriptorMetaclass(type):
    """Metaclass to allow data descriptor setters to work on types."""
    def __setattr__(self, name, value):
        setter = getattr(self.__dict__.get(name), '__set__')
        return setter(None, value) if setter else super().__setattr__(name, value)

class VarDescriptor(object):
    """Data descriptor class to support updating property via both class and instance."""
    def __init__(self, initial_value):
        self.value = initial_value
    def __get__(self, obj, objtype):
        if obj and hasattr(obj, '_value'):
            return self.value + obj._value
        return self.value
    def __set__(self, obj, value):
        if obj:
            obj._value = value - self.value
        else:
            self.value = value
    def __delete__(self, obj):
        if obj and hasattr(obj, '_value'):
            del obj._value

class Test(metaclass=VarDescriptorMetaclass):
    var = VarDescriptor(initial_value=1)

This seems to do what you want:

>>> obj = Test()
>>> obj.var
1
>>> obj.var += 1
>>> obj.var
2
>>> Test.var += 5
>>> Test.var
6
>>> obj.var
7

Upvotes: 1

Related Questions