5

As illustrated in the code below, why can't I use __setattr__ to set values on a dict that is part of the class that overloads the method? I expected that b.hello would not exist.

class MyClass():

    datastore = {}

    def __init__(self):
        self.datastore = {}

    def __getattr__(self, key):
        return self.datastore[key]

    def __setattr__(self, key, value):
        self.datastore[key] = value

a = MyClass()
b = MyClass()

a.hello = "err"

print a.hello # err
print b.hello # err
sowa
  • 1,249
  • 16
  • 29

2 Answers2

4

b.hello prints your string "err" because datastore is an attribute of the class itself, not of objects of the class. Therefore, when you initialize it in a, b can also access it.

Therefore, remove the datastore = {} from the class.

Furthermore, from the Python docs:

if __setattr__() wants to assign to an instance attribute, it should not simply execute self.name = value — this would cause a recursive call to itself. Instead, it should insert the value in the dictionary of instance attributes, e.g., self.__dict__[name] = value. For new-style classes, rather than accessing the instance dictionary, it should call the base class method with the same name, for example, object.__setattr__(self, name, value).

So, change your code to:

class MyClass(object): # Use new style classes
    def __init__(self):
        object.__setattr__(self, 'datastore', {}) # This prevents infinite recursion when setting attributes

    def __getattr__(self, key):
        return self.datastore[key]

    def __setattr__(self, key, value):
        self.datastore[key] = value

a = MyClass()
b = MyClass()

a.hello = "err"

print a.hello # Works
print b.hello # Gives an error
Dhara
  • 6,587
  • 2
  • 31
  • 46
3

Let me first explain why this occurs:

class MyClass():

    datastore = {}

    def __init__(self):
        self.datastore = {} # calls __setattr__

As you can see, your first variable definition in __init__ ends up calling __setattr__

    def __setattr__(self, key, value):
        self.datastore[key] = value

The instance does not yet have the datastore attribute because it is still in the process of being defined. So this line self.datastore[key] = value first trys to look up datastore in the instance's __dict__ but can't find it! Then it looks up one level in the class tree, where it does find it, as a class attribute!

Remember this:

class MyClass():

    datastore = {}

This is pretty confusing to begin with, since you have both an instance variable and a class variable with the same name, you should not have both.

So you can change your __init__ to this:

object.__setattr__(self, 'datastore', {})

Like @Dhara suggested, or you can use the more general approach which I would recommend:

super(MyClass, self).__setattr__('datastore', {})

Where the latter option only works for new-style classes (which are better in every way!) which you should be using! Just add object as a superclass

class MyClass(object): # In Py3k you don't need to since all classes are newstyle

One thing to note:

    def __setattr__(self, key, value):
        self.datastore[key] = value

Only works because you are setting the key of a dictionary and not an attribute of the instance. Be careful not to do things like

    def __setattr__(self, key, value):
        self.datastore = {} # for example

because that will result in infinite recursion, if you ever want to do something similar in __setattr__ use the same trick from before:

    def __setattr__(self, key, value):
        super(MyClass, self).__setattr__('datastore', {})

The final result should look like:

class MyClass(object):

    def __init__(self):
        super(MyClass, self).__setattr__('datastore', {})

    def __getattr__(self, key):
        return self.datastore[key]

    def __setattr__(self, key, value):
        self.datastore[key] = value


a = MyClass()
b = MyClass()
a.hello = "err"
print a.hello
print b.hello 
jamylak
  • 128,818
  • 30
  • 231
  • 230
  • Thank you for your perfect explanation. Can you explain this code you have wrote more: def __setattr__(self, key, value): super(MyClass, self).__setattr__('datastore', {}) I mean why you use super.setattr...? – Shahryar Mar 31 '20 at 19:20
  • @Shahryar Since `__setattrr__` is using `self.datastore` which doesn't exist yet – jamylak Apr 02 '20 at 02:29
  • so it will call the __setattr__ of the super class? why we need this? i understand all the codes here and in every other answers. The only thing i don't get is this call to super. Why would we need this? – Shahryar Apr 02 '20 at 06:30
  • 1
    If you want to find out why., simply remove the `super(MyClass, self).__setattr__('datastore', {})` and change it to `self.datastore = {}` and try run `a = MyClass()` and see what happens – jamylak Apr 02 '20 at 07:23
  • why can't we simply say self[name] = value? is it wrong? @jamylak – Shahryar Apr 07 '20 at 14:03