In [1]:
%%HTML
<img src="https://imgs.xkcd.com/comics/bun_alert.png" width=500></img>
In [2]:
%%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 [3]:
# let's reproduce it
class A():
pass
A.__dict__ is A.__dict__
Out[3]:
In [4]:
# ... and more robustly...
a = A()
a.__class__.__dict__ is a.__class__.__dict__
Out[4]:
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 [5]:
class B():
pass
In [6]:
C = type('C',(),dict())
In [7]:
D = type('C',(),dict())
In [8]:
D
Out[8]:
Takeaways:
Reminder:
object, including objects that are class definitions
In [9]:
# Start with the equivalence operator (==)
# --> remember that this will be defined by the ".__eq__()" method of the argument on the left
In [10]:
B == B
Out[10]:
In [11]:
B == C
Out[11]:
In [12]:
C == D
Out[12]:
In [15]:
B == D
Out[15]:
In [16]:
# check the directory of the object's attributes (more about this later)
vars(B)
Out[16]:
In [17]:
vars(B) == vars(B)
Out[17]:
In [18]:
vars(B) == vars(C)
Out[18]:
In [19]:
vars(C) == vars(D)
Out[19]:
In [20]:
# let's cast it to a real 'dict'
dict(vars(D))
Out[20]:
In [21]:
dict(vars(B)) == dict(vars(C))
Out[21]:
In [22]:
dict(vars(C)) == dict(vars(D))
Out[22]:
In [23]:
# check the directory of attributes (more about this later)
dir(B)
Out[23]:
In [24]:
dir(B) == dir(B)
Out[24]:
In [25]:
dir(B) == dir(C)
Out[25]:
In [26]:
dir(C) == dir(D)
Out[26]:
Takeaways:
vars, dir, etc.) are equivalent for self-comparison
In [27]:
# instance and type
isinstance(B,type)
Out[27]:
In [28]:
isinstance(B,object)
Out[28]:
In [29]:
type(B)
Out[29]:
In [30]:
B.__class__
Out[30]:
In [31]:
B.__base__
Out[31]:
In [32]:
B.__bases__
Out[32]:
In [33]:
id(B)
Out[33]:
In [34]:
# the 'is' operator compares the result of the 'id' function's application to the arguments
B is B
Out[34]:
In [35]:
id(B) == id(B)
Out[35]:
In [36]:
# now use B's callability to create an instance of it
b = B()
In [37]:
isinstance(b,B)
Out[37]:
In [38]:
type(b).__bases__
Out[38]:
In [39]:
# FWIW
type(type)
Out[39]:
In [40]:
type.__bases__
Out[40]:
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 [42]:
# 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 [43]:
vars(b)
Out[43]:
In [44]:
b.__dict__
Out[44]:
In [45]:
vars(B)
Out[45]:
Conclusion: __dict__ / vars() returns an instance's attributes.
Let iterate through b's inheritance tree, and look at the instance attributes.
In [46]:
vars(type)
Out[46]:
In [47]:
vars(object)
Out[47]:
In [48]:
# 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 [50]:
for attribute_key in attribute_keys:
print('{} : {}'.format(attribute_key,getattr(b,attribute_key)))
In [53]:
# our manual attributes collection should match that from 'dir'
attribute_keys - set(dir(b))
Out[53]:
NOTE: dir is not always reliable.
Take-aways:
__dict__ attribute lists the instance attributes of an object
In [54]:
b.an_instance_attr
Out[54]:
In [55]:
B.an_instance_attr
In [56]:
B.a_class_attr
Out[56]:
In [57]:
b.a_class_attr
Out[57]:
In [58]:
b.a_class_method
Out[58]:
In [59]:
b.a_class_method()
Out[59]:
Take-aways:
Out of scope:
In [60]:
B.mro()
Out[60]:
In [61]:
# 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()
Out[61]:
In [62]:
Z.c
Out[62]:
In [63]:
Z.b
Out[63]:
In [64]:
Z.a
Out[64]:
In [65]:
# get an attribute defined only by the base class
Z.__repr__
Out[65]:
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 [66]:
# let's start with the instance-level attribute dictionary
b.__dict__['an_attr'] = 'value'
b.__dict__
Out[66]:
In [67]:
# 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 [93]:
vars(b)[1]
Out[93]:
In [69]:
# what happens if we do the same to `b`'s class?
b.__class__.__dict__[1] = [3,4]
In [70]:
# right, we've seen this "mappingproxy" before
b.__class__.__dict__
Out[70]:
In [71]:
# also equivalent
B.__dict__
Out[71]:
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 [72]:
# turns out, it's a method of 'object'
B.__setattr__
Out[72]:
In [73]:
setattr(B,1,2)
Take-away:
object.__setattr__, thus speeding up attribute lookup.
In [74]:
%%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 [75]:
# the example
A.__dict__ is A.__dict__
Out[75]:
In [81]:
# run this a few times
id(A.__dict__)
Out[81]:
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 [86]:
# what about this?
id(A.__dict__) == id(A.__dict__)
Out[86]:
In [87]:
# or this?
x = id(A.__dict__)
y = id(A.__dict__)
x == y
Out[87]:
In [88]:
# or this?
x = A.__dict__
y = A.__dict__
id(x) == id(y)
Out[88]:
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 [ ]: