In [ ]:
    
%%HTML
<img src="https://imgs.xkcd.com/comics/bun_alert.png" width=500></img>
    
In [ ]:
    
%%HTML
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Pay no mind.... <a href="https://t.co/mnIPHJXE1h">pic.twitter.com/mnIPHJXE1h</a></p>— David Beazley (@dabeaz) <a href="https://twitter.com/dabeaz/status/890634046958477312">July 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
    
In [ ]:
    
# let's reproduce it
class A():
    pass
A.__dict__ is A.__dict__
    
In [ ]:
    
# ... and more robustly...
a = A()
a.__class__.__dict__ is a.__class__.__dict__
    
The code in question involves class objects and instances, the is operator, and attribute access via the dot notation. Let's explore how those objects and operations work.
Out of scope:
...but you will run into these concepts if you investigate beyond this tutorial.
In [ ]:
    
class B():
    pass
    
In [ ]:
    
C = type('C',(),dict())
    
In [ ]:
    
D = type('C',(),dict())
    
In [ ]:
    
D
    
Takeaways:
Reminder:
object, including objects that are class definitions
In [ ]:
    
# Start with the equivalence operator (==)
# --> remember that this will be defined by the ".__eq__()" method of the argument on the left
    
In [ ]:
    
B == B
    
In [ ]:
    
B == C
    
In [ ]:
    
C == D
    
In [ ]:
    
B == D
    
In [ ]:
    
# check the directory of the object's attributes (more about this later)
vars(B)
    
In [ ]:
    
vars(B) == vars(B)
    
In [ ]:
    
vars(B) == vars(C)
    
In [ ]:
    
vars(C) == vars(D)
    
In [ ]:
    
# let's cast it to a real 'dict'
dict(vars(D))
    
In [ ]:
    
dict(vars(B)) == dict(vars(C))
    
In [ ]:
    
dict(vars(C)) == dict(vars(D))
    
In [ ]:
    
# check the directory of attributes (more about this later)
dir(B)
    
In [ ]:
    
dir(B) == dir(B)
    
In [ ]:
    
dir(B) == dir(C)
    
In [ ]:
    
dir(C) == dir(D)
    
Takeaways:
vars, dir, etc.) are equivalent for self-comparison
In [ ]:
    
# instance and type
isinstance(B,type)
    
In [ ]:
    
isinstance(B,object)
    
In [ ]:
    
type(B)
    
In [ ]:
    
B.__class__
    
In [ ]:
    
B.__base__
    
In [ ]:
    
B.__bases__
    
In [ ]:
    
id(B)
    
In [ ]:
    
# the 'is' operator compares the result of the 'id' function's application to the arguments
B is B
    
In [ ]:
    
id(B) == id(B)
    
In [ ]:
    
# now use B's callability to create an instance of it
b = B()
    
In [ ]:
    
isinstance(b,B)
    
In [ ]:
    
type(b).__bases__
    
In [ ]:
    
# FWIW
type(type)
    
In [ ]:
    
type.__bases__
    
Takeaways:
WTF?
In addition to various notions of identity, we also need to investigate attribute access.
Apart from the problem we're investigating, Python places a lot of importance on interfaces, in which an object is described and classified in terms of its function and attributes, rather than its identity or inheritance properties.
In [ ]:
    
# set some attributes of some objects
setattr(b,'an_instance_attr',1)
setattr(B,'a_class_attr',2)
setattr(B,'a_class_method',lambda x: 3)
    
In [ ]:
    
vars(b)
    
In [ ]:
    
b.__dict__
    
In [ ]:
    
vars(B)
    
Conclusion: __dict__ / vars() returns an instance's attributes.
Let iterate through b's inheritance tree, and look at the instance attributes.
In [ ]:
    
vars(type)
    
In [ ]:
    
vars(object)
    
In [ ]:
    
# collect all the instance attributes of the inheritance tree (don't include type)
attribute_keys = set( list(vars(b).keys()) + list(vars(B).keys()) + list(vars(object).keys()))
    
In [ ]:
    
for attribute_key in attribute_keys:
    print('{} : {}'.format(attribute_key,getattr(b,attribute_key)))
    
In [ ]:
    
# our manual attributes collection should match that from 'dir'
attribute_keys - set(dir(b))
    
NOTE: dir is not always reliable.
Take-aways:
__dict__ attribute lists the instance attributes of an object
In [ ]:
    
b.an_instance_attr
    
In [ ]:
    
B.an_instance_attr
    
In [ ]:
    
B.a_class_attr
    
In [ ]:
    
b.a_class_attr
    
In [ ]:
    
b.a_class_method
    
In [ ]:
    
b.a_class_method()
    
Take-aways:
Out of scope:
In [ ]:
    
B.mro()
    
In [ ]:
    
# Python's MRO invokes a smart algorithm that accounts for circularity in the inheritance tree
# https://en.wikipedia.org/wiki/C3_linearization
class X():
    a = 1
class Y():
    b = 2
class Z(X,Y):
    c = 3
Z.mro()
    
In [ ]:
    
Z.c
    
In [ ]:
    
Z.b
    
In [ ]:
    
Z.a
    
In [ ]:
    
# get an attribute defined only by the base class
Z.__repr__
    
To locate the attribute named my_attr, Python:
__dict__ attribute of the instance for key my_attr__dict__ attributes of all the objects in the MRO__getattr__ method, and calls object.__getattr__('my_attr')Take-aways:
In [ ]:
    
# let's start with the instance-level attribute dictionary
b.__dict__['an_attr'] = 'value'
b.__dict__
    
In [ ]:
    
# I don't know why anyone would want to do this, but we'll allow it at the level of instance objects. 
# Any hashable object can be a key in an ordinary dictionary.
b.__dict__[1] = [3,4]
    
In [ ]:
    
# what happens if we do the same to `b`'s class?
b.__class__.__dict__[1] = [3,4]
    
In [ ]:
    
# right, we've seen this "mappingproxy" before
b.__class__.__dict__
    
In [ ]:
    
# also equivalent
B.__dict__
    
The MappingProxyType type is a read-only view of a mapping (dictionary). So we can't set instance attributes via this attribute. This requires that attributes be set with setattr, which calls __setattr__.
In [ ]:
    
# turns out, it's a method of 'object'
B.__setattr__
    
In [ ]:
    
setattr(B,1,2)
    
Take-away:
object.__setattr__, thus speeding up attribute lookup.
In [ ]:
    
%%HTML
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Pay no mind.... <a href="https://t.co/mnIPHJXE1h">pic.twitter.com/mnIPHJXE1h</a></p>— David Beazley (@dabeaz) <a href="https://twitter.com/dabeaz/status/890634046958477312">July 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
    
In [ ]:
    
# the example
A.__dict__ is A.__dict__
    
In [ ]:
    
# run this a few times
id(A.__dict__)
    
Takeaway: a new mappingproxy object is created for every call to __dict__, and since two objects can't share the same memory address at the same time, this form of comparison will never be true. The reason that a new mappingproxy is created for each call to __dict__ is, unfortunately, out of scope.
Bonus questions below:
In [ ]:
    
# what about this?
id(A.__dict__) == id(A.__dict__)
    
In [ ]:
    
# or this?
x = id(A.__dict__)
y = id(A.__dict__)
x == y
    
In [ ]:
    
# or this?
x = A.__dict__
y = A.__dict__
id(x) == id(y)
    
Remember: the return value of the id builtin function "is an integer which is guaranteed to be unique and constant for this object during its lifetime."
In [ ]: