quarkpol
quarkpol

Reputation: 495

How python adds atributes to class in this code?

How python adds attributes val1 and val2 to class. Does python internally invoke something like b1.__shared_state['val1'] = 'Jaga Gola!!!'?

# Borg (monostate pattern) lets a class have as many instances as one likes,
# but ensures that they all share the same state


class Borg:
    __shared_state = {}

    def __init__(self):
        self.__dict__ = self.__shared_state



b1 = Borg()
b2 = Borg()

print(b1 == b2)
b1.val1 = 'Jaga Gola!!!'
b1.val2 = 'BOOOM!!!'
print(b2.val1, b1.val2)

And why if I delete _shared_state and self.__dict__ = self.__shared_state I can't add attribute to class and get error: AttributeError: 'Borg' object has no attribute 'val1'?

class Borg:

    def __init__(self):
        pass



b1 = Borg()
b2 = Borg()

print(b1 == b2)
b1.val1 = 'Jaga Gola!!!'
b1.val2 = 'BOOOM!!!'
print(b2.val1, b1.val2)

Upvotes: 1

Views: 106

Answers (2)

Cblopez
Cblopez

Reputation: 586

Its an interesting thing what you are doing there, and its based on mutability:

The initial __shared_state that you declared is created before any of your code is execute. That dictionary is known as Class Attribute, because it is linked to the class, not an instance (does not use self for declaration). This means that __shared_state is shared between b1 and b2 because it is created before them, and since it is a dict, it is mutable.

What does it mean that it is mutable?

It means that one dictionary assigned to two different instances, will reference to same memory address, and even if we change the dicttonary, the memory address will remain the same. Here is a probe:

class Example:

    __shared_state = {1: 1}

    def __init__(self):
        self.__dict__ = self.__shared_state
        print(self.__shared_state)

ex1 = Example()
ex2 = Example()

print(id(ex1.__dict__), id(ex2.__dict__))

# Prints
# {1: 1}
# {1: 1}
# 140704387518944 140704387518944

Notice how they have the same id? That's because they are refering to the same object, and since the dictionary type is mutable, altering the dictionary in one object, means that you are changing it for both, because they are the same:

# Executing this
ex1.val1 = 2
# Equals this
ex1.__dict__['val1'] = 2
# Which also equals this
Example.__shared_state['val1'] = 2 

This does not happen with integers, which are immutable:

class Example:

    __shared_state = 2

    def __init__(self):
        self.a = self.__shared_state
        print(self.__shared_state)

ex1 = Example()
ex2 = Example()

ex2.a = 3

print(id(ex1.a), id(ex2.a))
# Prints
# 2
# 2
# 9302176 9302208
# Notice that once we change ex2.a, its ID changes!

When you delete your __shared_state, the moment you assign b1.val1 = 'Jaga Gola!!!' and b1.val2 = 'BOOOM!!!', it is only assigning to the dictionary from b1, thats why when you try to print b2.val1 and b2.val2 it raises an Error.

Upvotes: 0

Arthur Tacca
Arthur Tacca

Reputation: 10008

In this code:

class Borg:
    __shared_state = {}

    def __init__(self):
        self.__dict__ = self.__shared_state
  • The __shared_state = {} line occurs at class level, so it is added once to the class Borg, not to every individual object of type Borg. It is the same as writing Borg.__shared_state = {} afterwards.
  • The self.__dict__ = self.__shared_state is confusing because it uses self. twice but has very different effects:
    • When assigning to self.something, that something is set in the object self. No surprise there.
    • But when reading from self.something, first something is looked for in the self object, and if it's not found there then it's looked for in the object's class. That mind sound weird but you actually use that all the time: that's how methods normally work. For example, in s = "foo"; b = s.startswith("f"), the object s doesn't have an attribute startswith, but its class str does and that's what is used when you call the method.

This line:

b1.val1 = 'Jaga Gola!!!'

Ends up translating to:

b1.__dict__['val1'] = 'Jaga Gola!!!'

But we know that b1.__dict__ is equal to Borg.__shared_state, so it's assigned to that. Then:

print(b2.val1, ...

translates to:

print(b2.__dict__['val1'])

and again we know that b2.__dict__ is equal to the same Borg.__shared_state so val1 is found.

If you remove the stuff about __shared_state at the beginning then b1 and b2 get their own __dict__ objects so putting val1 into the dict of b1 has no effect on b2, and that's how you get the error you mentioned.


This is all fine for playing around with to understand what's happening, but you should realise that this code isn't guaranteed to work and might break e.g. in a future version of Python or another implementation such as PyPy. The Python documentation for __dict__ describes it as a "read-only attribute" so you shouldn't be assigning to it at all. Don't do this in code that anybody else might run!

In fact, the idea that a.foo is just a.__dict__['foo'] is a huge simplification. For a start, we already encountered that sometimes it's followed by a.__class__.__dict__['foo'] when reading. Another example is that a.__dict__ is clearly not a.__dict__['__dict__'], otherwise how would it ever it end!? The process is somewhat complicated and documented in the Data Model docs.

The supported way to get this behaviour is to use the special __setattr__ and __getattr__ methods (also described in those Data Model docs), like this:

class Borg:
    __shared_state = {}

    def __getattr__(self, name):
        try:
            return Borg.__shared_state[name]
        except KeyError:
            raise AttributeError

    def __setattr__(self, name, value):
        Borg.__shared_state[name] = value

Upvotes: 2

Related Questions