Reputation: 495
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
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.
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
Reputation: 10008
In this code:
class Borg:
__shared_state = {}
def __init__(self):
self.__dict__ = self.__shared_state
__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.self.__dict__ = self.__shared_state
is confusing because it uses self.
twice but has very different effects:
self.something
, that something
is set in the object self
. No surprise there.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