Personal website https://benkurtovic.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2015-01-28-python-object-replacement.md 44 KiB

9 年之前
10 年之前
9 年之前
9 年之前
9 年之前
10 年之前
10 年之前
10 年之前
10 年之前
10 年之前
10 年之前
10 年之前
10 年之前
9 年之前
10 年之前
9 年之前
10 年之前
9 年之前
10 年之前
9 年之前
9 年之前
9 年之前
9 年之前
9 年之前
9 年之前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998
  1. ---
  2. layout: post
  3. title: Finding and Replacing Objects in Python
  4. tags: Python
  5. description: More reflection than you cared to ask for
  6. ---
  7. Today, we're going to demonstrate a fairly evil thing in Python, which I call
  8. _object replacement_.
  9. Say you have some program that's been running for a while, and a particular
  10. object has made its way throughout your code. It lives inside lists, class
  11. attributes, maybe even inside some closures. You want to completely replace
  12. this object with another one; that is to say, you want to find all references
  13. to object `A` and replace them with object `B`, enabling `A` to be garbage
  14. collected. This has some interesting implications for special object types. If
  15. you have methods that are bound to `A`, you want to rebind them to `B`. If `A`
  16. is a class, you want all instances of `A` to become instances of `B`. And so
  17. on.
  18. _But why on Earth would you want to do that?_ you ask. I'll focus on a concrete
  19. use case in a future post, but for now, I imagine this could be useful in some
  20. kind of advanted unit testing situation with mock objects. Still, it's fairly
  21. insane, so let's leave it primarily as an intellectual exercise.
  22. This article is written for [CPython](https://en.wikipedia.org/wiki/CPython)
  23. 2.7.<sup><a id="ref1" href="#fn1">[1]</a></sup>
  24. ## Review
  25. First, a recap on terminology here. You can skip this section if you know
  26. Python well.
  27. In Python, _names_ are what most languages call "variables". They reference
  28. _objects_. So when we do:
  29. {% highlight python %}
  30. a = [1, 2, 3, 4]
  31. {% endhighlight %}
  32. ...we are creating a list object with four integers, and binding it to the name
  33. `a`. In graph form:<sup><a id="ref2" href="#fn2">[2]</a></sup>
  34. <svg width="223pt" height="44pt" viewBox="0.00 0.00 223.01 44.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 40)"><polygon fill="white" stroke="none" points="-4,4 -4,-40 219.012,-40 219.012,4 -4,4"/><g id="node1" class="node"><title>L</title><polygon fill="none" stroke="black" stroke-width="0.5" points="215.018,-36 126.994,-36 126.994,-0 215.018,-0 215.018,-36"/><text text-anchor="middle" x="171.006" y="-15" font-family="Courier,monospace" font-size="10.00">[1, 2, 3, 4]</text></g><g id="node2" class="node"><title>a</title><ellipse fill="none" stroke="black" stroke-width="0.5" cx="27" cy="-18" rx="27" ry="18"/><text text-anchor="middle" x="27" y="-13.8" font-family="Courier,monospace" font-size="14.00">a</text></g><g id="edge1" class="edge"><title>a&#45;&gt;L</title><path fill="none" stroke="black" stroke-width="0.5" d="M54.0461,-18C72.2389,-18 97.1211,-18 119.173,-18"/><polygon fill="black" stroke="black" stroke-width="0.5" points="119.339,-20.6251 126.839,-18 119.339,-15.3751 119.339,-20.6251"/></g></g></svg>
  35. In each of the following examples, we are creating new _references_ to the
  36. list object, but we are never duplicating it. Each reference points to the same
  37. memory address (which you can get using `id(a)`).
  38. {% highlight python %}
  39. b = a
  40. {% endhighlight %}
  41. {% highlight python %}
  42. c = SomeContainerClass()
  43. c.data = a
  44. {% endhighlight %}
  45. {% highlight python %}
  46. def wrapper(L):
  47. def inner():
  48. return L.pop()
  49. return inner
  50. d = wrapper(a)
  51. {% endhighlight %}
  52. <svg width="254pt" height="234pt" viewBox="0.00 0.00 253.96 234.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 238)"><polygon fill="white" stroke="none" points="-4,4 -4,-238 249.96,-238 249.96,4 -4,4"/><g id="clust3" class="cluster"><title>cluster0</title><polygon fill="none" stroke="black" stroke-width="0.5" points="8,-8 8,-82 78,-82 78,-8 8,-8"/><text text-anchor="middle" x="43" y="-66.8" font-family="Courier,monospace" font-size="14.00">d</text></g><g id="node1" class="node"><title>obj</title><polygon fill="none" stroke="black" stroke-width="0.5" points="245.966,-153 157.943,-153 157.943,-117 245.966,-117 245.966,-153"/><text text-anchor="middle" x="201.954" y="-132" font-family="Courier,monospace" font-size="10.00">[1, 2, 3, 4]</text></g><g id="node2" class="node"><title>a</title><ellipse fill="none" stroke="black" stroke-width="0.5" cx="43" cy="-216" rx="27" ry="18"/><text text-anchor="middle" x="43" y="-211.8" font-family="Courier,monospace" font-size="14.00">a</text></g><g id="edge1" class="edge"><title>a&#45;&gt;obj</title><path fill="none" stroke="black" stroke-width="0.5" d="M64.8423,-205.244C88.7975,-192.881 128.721,-172.278 159.152,-156.573"/><polygon fill="black" stroke="black" stroke-width="0.5" points="160.422,-158.872 165.883,-153.1 158.014,-154.206 160.422,-158.872"/></g><g id="node3" class="node"><title>b</title><ellipse fill="none" stroke="black" stroke-width="0.5" cx="43" cy="-162" rx="27" ry="18"/><text text-anchor="middle" x="43" y="-157.8" font-family="Courier,monospace" font-size="14.00">b</text></g><g id="edge2" class="edge"><title>b&#45;&gt;obj</title><path fill="none" stroke="black" stroke-width="0.5" d="M69.2174,-157.662C90.9996,-153.915 123.147,-148.385 150.231,-143.726"/><polygon fill="black" stroke="black" stroke-width="0.5" points="150.777,-146.295 157.724,-142.437 149.887,-141.121 150.777,-146.295"/></g><g id="node4" class="node"><title>c</title><ellipse fill="none" stroke="black" stroke-width="0.5" cx="43" cy="-108" rx="41.897" ry="18"/><text text-anchor="middle" x="43" y="-103.8" font-family="Courier,monospace" font-size="14.00">c.data</text></g><g id="edge3" class="edge"><title>c&#45;&gt;obj</title><path fill="none" stroke="black" stroke-width="0.5" d="M82.3954,-114.605C102.772,-118.11 128.077,-122.463 150.069,-126.247"/><polygon fill="black" stroke="black" stroke-width="0.5" points="149.86,-128.874 157.697,-127.559 150.75,-123.7 149.86,-128.874"/></g><g id="node5" class="node"><title>L</title><ellipse fill="none" stroke="black" stroke-width="0.5" cx="43" cy="-34" rx="27" ry="18"/><text text-anchor="middle" x="43" y="-29.8" font-family="Courier,monospace" font-size="14.00">L</text></g><g id="edge4" class="edge"><title>L&#45;&gt;obj</title><path fill="none" stroke="black" stroke-width="0.5" d="M62.9324,-46.183C88.5083,-62.6411 134.554,-92.2712 166.386,-112.755"/><polygon fill="black" stroke="black" stroke-width="0.5" points="165.223,-115.128 172.951,-116.98 168.064,-110.714 165.223,-115.128"/></g></g></svg>
  53. Note that these references are all equal. `a` is no more valid a name for the
  54. list than `b`, `c.data`, or `L` (or `d.func_closure[0].cell_contents` to the
  55. outside world). As a result, if you delete one of these references—explicitly
  56. with `del a`, or implicitly if a name goes out of scope—then the other
  57. references are still around, and object continues to exist. If all of an
  58. object's references disappear, then Python's garbage collector should eliminate
  59. it.
  60. ## Dead ends
  61. My first thought when approaching this problem was to physically write over the
  62. memory where our target object is stored. This can be done using
  63. [`ctypes.memmove()`](https://docs.python.org/2/library/ctypes.html#ctypes.memmove)
  64. from the Python standard library:
  65. {% highlight pycon %}
  66. >>> class A(object): pass
  67. ...
  68. >>> class B(object): pass
  69. ...
  70. >>> obj = A()
  71. >>> print obj
  72. <__main__.A object at 0x10e3e1190>
  73. >>> import ctypes
  74. >>> ctypes.memmove(id(A), id(B), object.__sizeof__(A))
  75. 140576340136752
  76. >>> print obj
  77. <__main__.B object at 0x10e3e1190>
  78. {% endhighlight %}
  79. What we are doing here is overwriting the fields of the `A` instance of the
  80. [`PyClassObject` C struct](https://github.com/python/cpython/blob/2.7/Include/classobject.h#L12)
  81. with fields from the `B` struct instance. As a result, they now share various
  82. properties, such as their attribute dictionaries
  83. ([`__dict__`](https://docs.python.org/2/reference/datamodel.html#the-standard-type-hierarchy)).
  84. So, we can do things like this:
  85. {% highlight pycon %}
  86. >>> B.foo = 123
  87. >>> obj.foo
  88. 123
  89. {% endhighlight %}
  90. However, there are clear issues. What we've done is create a
  91. [_shallow copy_](https://en.wikipedia.org/wiki/Object_copy#Shallow_copy).
  92. Therefore, `A` and `B` are still distinct objects, so certain changes made to
  93. one will not be replicated to the other:
  94. {% highlight pycon %}
  95. >>> A is B
  96. False
  97. >>> B.__name__ = "C"
  98. >>> A.__name__
  99. 'B'
  100. {% endhighlight %}
  101. Also, this won't work if `A` and `B` are different sizes, since we will be
  102. either reading from or writing to memory that we don't necessarily own:
  103. {% highlight pycon %}
  104. >>> A = ()
  105. >>> B = []
  106. >>> print A.__sizeof__(), B.__sizeof__()
  107. 24 40
  108. >>> import ctypes
  109. >>> ctypes.memmove(id(A), id(B), A.__sizeof__())
  110. 4321271888
  111. Python(33575,0x7fff76925300) malloc: *** error for object 0x6f: pointer being freed was not allocated
  112. *** set a breakpoint in malloc_error_break to debug
  113. Abort trap: 6
  114. {% endhighlight %}
  115. Oh, and there's a bit of a problem when we deallocate these objects, too...
  116. {% highlight pycon %}
  117. >>> A = []
  118. >>> B = range(8)
  119. >>> import ctypes
  120. >>> ctypes.memmove(id(A), id(B), A.__sizeof__())
  121. 4514685728
  122. >>> print A
  123. [0, 1, 2, 3, 4, 5, 6, 7]
  124. >>> del A
  125. >>> del B
  126. Segmentation fault: 11
  127. {% endhighlight %}
  128. ## Fishing for references with Guppy
  129. A more appropriate solution is finding all of the _references_ to the old
  130. object, and then updating them to point to the new object, rather than
  131. replacing the old object directly.
  132. But how do we track references? Fortunately, there's a library called
  133. [Guppy](http://guppy-pe.sourceforge.net/) that allows us to do this. Often used
  134. for diagnosing memory leaks, we can take advantage of its robust object
  135. tracking features here. Install it with [pip](https://pypi.python.org/pypi/pip)
  136. (`pip install guppy`).
  137. I've always found Guppy hard to use (as many debuggers are, though justified by
  138. the complexity of the task involved), so we'll begin with a feature demo before
  139. delving into the actual problem.
  140. ### Feature demonstration
  141. Guppy's interface is deceptively simple. We begin by calling
  142. [`guppy.hpy()`](http://guppy-pe.sourceforge.net/guppy.html#kindnames.guppy.hpy),
  143. to expose the Heapy interface, which is the component of Guppy with the
  144. features we want:
  145. {% highlight pycon %}
  146. >>> import guppy
  147. >>> hp = guppy.hpy()
  148. >>> hp
  149. Top level interface to Heapy.
  150. Use eg: hp.doc for more info on hp.
  151. {% endhighlight %}
  152. Calling
  153. [`hp.heap()`](http://guppy-pe.sourceforge.net/heapy_Use.html#heapykinds.Use.heap)
  154. shows us a table of the objects known to Guppy, grouped together
  155. (mathematically speaking,
  156. [_partitioned_](https://en.wikipedia.org/wiki/Partition_of_a_set)) by
  157. type<sup><a id="ref3" href="#fn3">[3]</a></sup> and sorted by how much space
  158. they take up in memory:
  159. {% highlight pycon %}
  160. >>> heap = hp.heap()
  161. >>> heap
  162. Partition of a set of 45761 objects. Total size = 4699200 bytes.
  163. Index Count % Size % Cumulative % Kind (class / dict of class)
  164. 0 15547 34 1494736 32 1494736 32 str
  165. 1 8356 18 770272 16 2265008 48 tuple
  166. 2 346 1 452080 10 2717088 58 dict (no owner)
  167. 3 13685 30 328440 7 3045528 65 int
  168. 4 71 0 221096 5 3266624 70 dict of module
  169. 5 1652 4 211456 4 3478080 74 types.CodeType
  170. 6 199 0 210856 4 3688936 79 dict of type
  171. 7 1614 4 193680 4 3882616 83 function
  172. 8 199 0 177008 4 4059624 86 type
  173. 9 124 0 135328 3 4194952 89 dict of class
  174. <91 more rows. Type e.g. '_.more' to view.>
  175. {% endhighlight %}
  176. This object (called an
  177. [`IdentitySet`](http://guppy-pe.sourceforge.net/heapy_UniSet.html#heapykinds.IdentitySet))
  178. looks bizarre, but it can be treated roughly like a list. If we want to take a
  179. look at strings, we can do `heap[0]`:
  180. {% highlight pycon %}
  181. >>> heap[0]
  182. Partition of a set of 22606 objects. Total size = 2049896 bytes.
  183. Index Count % Size % Cumulative % Kind (class / dict of class)
  184. 0 22606 100 2049896 100 2049896 100 str
  185. {% endhighlight %}
  186. This isn't very useful, though. What we really want to do is re-partition this
  187. subset using another relationship. There are a number of options, such as:
  188. {% highlight pycon %}
  189. >>> heap[0].byid # Group by object ID; each subset therefore has one element
  190. Set of 22606 <str> objects. Total size = 2049896 bytes.
  191. Index Size % Cumulative % Representation (limited)
  192. 0 7480 0.4 7480 0.4 'The class Bi... copy of S.\n'
  193. 1 4872 0.2 12352 0.6 "Support for ... 'error'.\n\n"
  194. 2 4760 0.2 17112 0.8 'Heap queues\...at Art! :-)\n'
  195. 3 4760 0.2 21872 1.1 'Heap queues\...at Art! :-)\n'
  196. 4 3896 0.2 25768 1.3 'This module ...ng function\n'
  197. 5 3824 0.2 29592 1.4 'The type of ...call order.\n'
  198. 6 3088 0.2 32680 1.6 't\x00\x00|\x...x00|\x02\x00S'
  199. 7 2992 0.1 35672 1.7 'HeapView(roo... size, etc.\n'
  200. 8 2808 0.1 38480 1.9 'Directory tr...ories\n\n '
  201. 9 2640 0.1 41120 2.0 'The class No... otherwise.\n'
  202. <22596 more rows. Type e.g. '_.more' to view.>
  203. {% endhighlight %}
  204. {% highlight pycon %}
  205. >>> heap[0].byrcs # Group by what types of objects reference the strings
  206. Partition of a set of 22606 objects. Total size = 2049896 bytes.
  207. Index Count % Size % Cumulative % Referrers by Kind (class / dict of class)
  208. 0 6146 27 610752 30 610752 30 types.CodeType
  209. 1 5304 23 563984 28 1174736 57 tuple
  210. 2 4104 18 237536 12 1412272 69 dict (no owner)
  211. 3 1959 9 139880 7 1552152 76 list
  212. 4 564 2 136080 7 1688232 82 function, tuple
  213. 5 809 4 97896 5 1786128 87 dict of module
  214. 6 346 2 71760 4 1857888 91 dict of type
  215. 7 365 2 19408 1 1877296 92 dict of module, tuple
  216. 8 192 1 16176 1 1893472 92 dict (no owner), list
  217. 9 232 1 11784 1 1905256 93 dict of class, function, tuple, types.CodeType
  218. <229 more rows. Type e.g. '_.more' to view.>
  219. {% endhighlight %}
  220. {% highlight pycon %}
  221. >>> heap[0].byvia # Group by how the strings are related to their referrers
  222. Partition of a set of 22606 objects. Total size = 2049896 bytes.
  223. Index Count % Size % Cumulative % Referred Via:
  224. 0 2656 12 420456 21 420456 21 '[0]'
  225. 1 2095 9 259008 13 679464 33 '.co_code'
  226. 2 2095 9 249912 12 929376 45 '.co_filename'
  227. 3 564 2 136080 7 1065456 52 '.func_doc', '[0]'
  228. 4 243 1 103528 5 1168984 57 "['__doc__']"
  229. 5 1930 9 100584 5 1269568 62 '.co_lnotab'
  230. 6 502 2 31128 2 1300696 63 '[1]'
  231. 7 306 1 16272 1 1316968 64 '[2]'
  232. 8 242 1 12960 1 1329928 65 '[3]'
  233. 9 184 1 9872 0 1339800 65 '[4]'
  234. <7323 more rows. Type e.g. '_.more' to view.>
  235. {% endhighlight %}
  236. From this, we can see that the plurality of memory devoted to strings is taken
  237. up by those referenced by code objects (`types.CodeType` represents
  238. Python code—accessible from a non-C-defined function through
  239. `func.func_code`—and contains things like the names of its local variables and
  240. the actual sequence of opcodes that make it up).
  241. For fun, let's pick a random string.
  242. {% highlight pycon %}
  243. >>> import random
  244. >>> obj = heap[0].byid[random.randrange(0, heap[0].count)]
  245. >>> obj
  246. Set of 1 <str> object. Total size = 176 bytes.
  247. Index Size % Cumulative % Representation (limited)
  248. 0 176 100.0 176 100.0 'Define names...not listed.\n'
  249. {% endhighlight %}
  250. Interesting. Since this heap subset contains only one element, we can use
  251. [`.theone`](http://guppy-pe.sourceforge.net/heapy_UniSet.html#heapykinds.IdentitySetSingleton.theone)
  252. to get the actual object represented here:
  253. {% highlight pycon %}
  254. >>> obj.theone
  255. 'Define names for all type symbols known in the standard interpreter.\n\nTypes that are part of optional modules (e.g. array) are not listed.\n'
  256. {% endhighlight %}
  257. Looks like the docstring for the
  258. [`types`](https://docs.python.org/2/library/types.html) module. We can confirm
  259. by using
  260. [`.referrers`](http://guppy-pe.sourceforge.net/heapy_UniSet.html#heapykinds.IdentitySet.referrers)
  261. to get the set of objects that refer to objects in the given set:
  262. {% highlight pycon %}
  263. >>> obj.referrers
  264. Partition of a set of 1 object. Total size = 3352 bytes.
  265. Index Count % Size % Cumulative % Kind (class / dict of class)
  266. 0 1 100 3352 100 3352 100 dict of module
  267. {% endhighlight %}
  268. This is `types.__dict__` (since the docstring we got is actually stored as
  269. `types.__dict__["__doc__"]`), so if we use `.referrers` again:
  270. {% highlight pycon %}
  271. >>> obj.referrers.referrers
  272. Partition of a set of 1 object. Total size = 56 bytes.
  273. Index Count % Size % Cumulative % Kind (class / dict of class)
  274. 0 1 100 56 100 56 100 module
  275. >>> obj.referrers.referrers.theone
  276. <module 'types' from '/usr/local/Cellar/python/2.7.8_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/types.pyc'>
  277. >>> import types
  278. >>> types.__doc__ is obj.theone
  279. True
  280. {% endhighlight %}
  281. _But why did we find an object in the `types` module if we never imported it?_
  282. Well, let's see. We can use
  283. [`hp.iso()`](http://guppy-pe.sourceforge.net/heapy_Use.html#heapykinds.Use.iso)
  284. to get the Heapy set consisting of a single given object:
  285. {% highlight pycon %}
  286. >>> hp.iso(types)
  287. Partition of a set of 1 object. Total size = 56 bytes.
  288. Index Count % Size % Cumulative % Kind (class / dict of class)
  289. 0 1 100 56 100 56 100 module
  290. {% endhighlight %}
  291. Using a similar procedure as before, we see that `types` is imported by the
  292. [`traceback`](https://docs.python.org/2/library/traceback.html) module:
  293. {% highlight pycon %}
  294. >>> hp.iso(types).referrers
  295. Partition of a set of 10 objects. Total size = 25632 bytes.
  296. Index Count % Size % Cumulative % Kind (class / dict of class)
  297. 0 2 20 13616 53 13616 53 dict (no owner)
  298. 1 5 50 9848 38 23464 92 dict of module
  299. 2 1 10 1048 4 24512 96 dict of guppy.etc.Glue.Interface
  300. 3 1 10 1048 4 25560 100 dict of guppy.etc.Glue.Share
  301. 4 1 10 72 0 25632 100 tuple
  302. >>> hp.iso(types).referrers[1].byid
  303. Set of 5 <dict of module> objects. Total size = 9848 bytes.
  304. Index Size % Cumulative % Owner Name
  305. 0 3352 34.0 3352 34.0 traceback
  306. 1 3352 34.0 6704 68.1 warnings
  307. 2 1048 10.6 7752 78.7 __main__
  308. 3 1048 10.6 8800 89.4 abc
  309. 4 1048 10.6 9848 100.0 guppy.etc.Glue
  310. {% endhighlight %}
  311. ...and that is imported by
  312. [`site`](https://docs.python.org/2/library/site.html):
  313. {% highlight pycon %}
  314. >>> import traceback
  315. >>> hp.iso(traceback).referrers
  316. Partition of a set of 3 objects. Total size = 15992 bytes.
  317. Index Count % Size % Cumulative % Kind (class / dict of class)
  318. 0 1 33 12568 79 12568 79 dict (no owner)
  319. 1 1 33 3352 21 15920 100 dict of module
  320. 2 1 33 72 0 15992 100 tuple
  321. >>> hp.iso(traceback).referrers[1].byid
  322. Set of 1 <dict of module> object. Total size = 3352 bytes.
  323. Index Size % Cumulative % Owner Name
  324. 0 3352 100.0 3352 100.0 site
  325. {% endhighlight %}
  326. Since `site` is imported by Python on startup, we've figured out why objects
  327. from `types` exist, even though we've never used them.
  328. We've learned something important, too. When objects are stored as ordinary
  329. attributes of a parent object (like `types.__doc__`, `traceback.types`, and
  330. `site.traceback` from above), they are not referenced directly by the parent
  331. object, but by that object's `__dict__` attribute. Therefore, if we want to
  332. replace `A` with `B` and `A` is an attribute of `C`, we (probably) don't need
  333. to know anything special about `C`—just how to modify dictionaries.
  334. A good Guppy/Heapy tutorial, while a bit old and incomplete, can be found on
  335. [Andrey Smirnov's website](http://smira.ru/wp-content/uploads/2011/08/heapy.html).
  336. ## Examining paths
  337. Let's set up an example replacement using class instances:
  338. {% highlight python %}
  339. class A(object):
  340. pass
  341. class B(object):
  342. pass
  343. a = A()
  344. b = B()
  345. {% endhighlight %}
  346. Suppose we want to replace `a` with `b`. From the demo above, we know that we
  347. can get the Heapy set of a single object using `hp.iso()`. We also know we can
  348. use `.referrers` to get the set of objects that reference the given object:
  349. {% highlight pycon %}
  350. >>> import guppy
  351. >>> hp = guppy.hpy()
  352. >>> print hp.iso(a).referrers
  353. Partition of a set of 1 object. Total size = 1048 bytes.
  354. Index Count % Size % Cumulative % Kind (class / dict of class)
  355. 0 1 100 1048 100 1048 100 dict of module
  356. {% endhighlight %}
  357. `a` is only referenced by one object, which makes sense, since we've only used
  358. it in one place—as a local variable—meaning `hp.iso(a).referrers.theone` must
  359. be [`locals()`](https://docs.python.org/2/library/functions.html#locals):
  360. {% highlight pycon %}
  361. >>> hp.iso(a).referrers.theone is locals()
  362. True
  363. {% endhighlight %}
  364. However, there is a more useful feature available to us:
  365. [`.pathsin`](http://guppy-pe.sourceforge.net/heapy_UniSet.html#heapykinds.IdentitySet.pathsin).
  366. This also returns references to the given object, but instead of a Heapy set,
  367. it is a list of `Path` objects. These are more useful since they tell us not
  368. only _what_ objects are related to the given object, but _how_ they are
  369. related.
  370. {% highlight pycon %}
  371. >>> print hp.iso(a).pathsin
  372. 0: Src['a']
  373. {% endhighlight %}
  374. This looks very ambiguous. However, we find that we can extract the source of
  375. the reference using `.src`:
  376. {% highlight pycon %}
  377. >>> path = hp.iso(a).pathsin[0]
  378. >>> print path.src
  379. Partition of a set of 1 object. Total size = 1048 bytes.
  380. Index Count % Size % Cumulative % Kind (class / dict of class)
  381. 0 1 100 1048 100 1048 100 dict of module
  382. >>> path.src.theone is locals()
  383. True
  384. {% endhighlight %}
  385. ...and, we can examine the type of relation by looking at `.path[1]` (the
  386. actual reason for this isn't worth getting into, due to Guppy's lack of
  387. documentation on the subject):
  388. {% highlight pycon %}
  389. >>> relation = path.path[1]
  390. >>> relation
  391. <guppy.heapy.Path.Based_R_INDEXVAL object at 0x100f38230>
  392. {% endhighlight %}
  393. We notice that `relation` is a `Based_R_INDEXVAL` object. Sounds bizarre, but
  394. this tells us that `a` is a particular indexed value of `path.src`. What index?
  395. We can get this using `relation.r`:
  396. {% highlight pycon %}
  397. >>> rel = relation.r
  398. >>> print rel
  399. a
  400. {% endhighlight %}
  401. Ah ha! So now we know that `a` is equal to the reference source (i.e.,
  402. `path.src.theone`) indexed by `rel`:
  403. {% highlight pycon %}
  404. >>> path.src.theone[rel] is a
  405. True
  406. {% endhighlight %}
  407. But `path.src.theone` is just a dictionary, meaning we know how to modify it
  408. very easily:<sup><a id="ref4" href="#fn4">[4]</a></sup>
  409. {% highlight pycon %}
  410. >>> path.src.theone[rel] = b
  411. >>> a
  412. <__main__.B object at 0x100dae090>
  413. >>> a is b
  414. True
  415. {% endhighlight %}
  416. Bingo. We've successfully replaced `a` with `b`, using a general method that
  417. should work for any case where `a` is in a dictionary-like object.
  418. ## Handling different reference types
  419. We'll continue by wrapping this code up in a nice function, which we will
  420. expand as we go:
  421. {% highlight python %}
  422. import guppy
  423. from guppy.heapy import Path
  424. hp = guppy.hpy()
  425. def replace(old, new):
  426. for path in hp.iso(old).pathsin:
  427. relation = path.path[1]
  428. if isinstance(relation, Path.R_INDEXVAL):
  429. path.src.theone[relation.r] = new
  430. {% endhighlight %}
  431. ### Dictionaries, lists, and tuples
  432. As noted above, this is versatile to handle many dictionary-like situations,
  433. including `__dict__`, which means we already know how to replace object
  434. attributes:
  435. {% highlight pycon %}
  436. >>> a, b = A(), B()
  437. >>>
  438. >>> class X(object):
  439. ... pass
  440. ...
  441. >>> X.cattr = a
  442. >>> x = X()
  443. >>> x.iattr = a
  444. >>> d1 = {1: a}
  445. >>> d2 = [{1: {0: ("foo", "bar", {"a": a, "b": b})}}]
  446. >>>
  447. >>> replace(a, b)
  448. >>>
  449. >>> print a
  450. <__main__.B object at 0x1042b9910>
  451. >>> print X.cattr
  452. <__main__.B object at 0x1042b9910>
  453. >>> print x.iattr
  454. <__main__.B object at 0x1042b9910>
  455. >>> print d1[1]
  456. <__main__.B object at 0x1042b9910>
  457. >>> print d2[0][1][0][2]["a"]
  458. <__main__.B object at 0x1042b9910>
  459. {% endhighlight %}
  460. Lists can be handled exactly the same as dictionaries, although the keys in
  461. this case (i.e., `relation.r`) will always be integers.
  462. {% highlight pycon %}
  463. >>> a, b = A(), B()
  464. >>> L = [0, 1, 2, a, b]
  465. >>> print L
  466. [0, 1, 2, <__main__.A object at 0x104598950>, <__main__.B object at 0x104598910>]
  467. >>> replace(a, b)
  468. >>> print L
  469. [0, 1, 2, <__main__.B object at 0x104598910>, <__main__.B object at 0x104598910>]
  470. {% endhighlight %}
  471. Tuples are interesting. We can't modify them directly because they're
  472. immutable, but we _can_ create a new tuple with the new value, and then replace
  473. that tuple just like we replaced our original object:
  474. {% highlight python %}
  475. # Meanwhile, in replace()...
  476. if isinstance(relation, Path.R_INDEXVAL):
  477. source = path.src.theone
  478. if isinstance(source, tuple):
  479. temp = list(source)
  480. temp[relation.r] = new
  481. replace(source, tuple(temp))
  482. else:
  483. source[relation.r] = new
  484. {% endhighlight %}
  485. As a result:
  486. {% highlight pycon %}
  487. >>> a, b = A(), B()
  488. >>> t1 = (0, 1, 2, a)
  489. >>> t2 = (0, (1, (2, (3, (4, (5, (a,)))))))
  490. >>> replace(a, b)
  491. >>> print t1
  492. (0, 1, 2, <__main__.B object at 0x104598e50>)
  493. >>> print t2
  494. (0, (1, (2, (3, (4, (5, (<__main__.B object at 0x104598e50>,)))))))
  495. {% endhighlight %}
  496. ### Bound methods
  497. Here's a fun one. Let's upgrade our definitions of `A` and `B`:
  498. {% highlight python %}
  499. class A(object):
  500. def func(self):
  501. return self
  502. class B(object):
  503. pass
  504. {% endhighlight %}
  505. After replacing `a` with `b`, `a.func` no longer exists, as we'd expect:
  506. {% highlight pycon %}
  507. >>> a, b = A(), B()
  508. >>> a.func()
  509. <__main__.A object at 0x10c4a5b10>
  510. >>> replace(a, b)
  511. >>> a.func()
  512. Traceback (most recent call last):
  513. File "<stdin>", line 1, in <module>
  514. AttributeError: 'B' object has no attribute 'func'
  515. {% endhighlight %}
  516. But what if we save a reference to `a.func` before the replacement?
  517. {% highlight pycon %}
  518. >>> a, b = A(), B()
  519. >>> f = a.func
  520. >>> replace(a, b)
  521. >>> f()
  522. <__main__.A object at 0x10c4b6090>
  523. {% endhighlight %}
  524. Hmm. So `f` has kept a reference to `a` somehow, but not in a dictionary-like
  525. object. So where is it?
  526. Well, we can reveal it with the attribute `f.__self__`:
  527. {% highlight pycon %}
  528. >>> f.__self__
  529. <__main__.A object at 0x10c4b6090>
  530. {% endhighlight %}
  531. Unfortunately, this attribute is magical and we can't write to it directly:
  532. {% highlight pycon %}
  533. >>> f.__self__ = b
  534. Traceback (most recent call last):
  535. File "<stdin>", line 1, in <module>
  536. TypeError: readonly attribute
  537. {% endhighlight %}
  538. Python clearly doesn't want us to re-bind bound methods, and a reasonable
  539. person would give up here, but we still have a few tricks up our sleeve. Let's
  540. examine the internal C structure of bound methods,
  541. [`PyMethodObject`](https://github.com/python/cpython/blob/2.7/Include/classobject.h#L31):
  542. <svg width="559pt" height="200pt" viewBox="0.00 18.00 559.03 200.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 226)"><polygon fill="white" stroke="none" points="-4,4 -4,-226 555.032,-226 555.032,4 -4,4"/><g id="clust2" class="cluster"><title>cluster</title><polygon fill="none" stroke="black" stroke-width="0" points="8,-8 8,-214 272,-214 272,-8 8,-8"/><text text-anchor="middle" x="140" y="-14.8" font-family="Courier,monospace" font-size="14.00">PyMethodObject</text></g><g id="node1" class="node"><title>obj</title><polygon fill="none" stroke="black" stroke-width="0.5" points="551.048,-110 336.984,-110 336.984,-74 551.048,-74 551.048,-110"/><text text-anchor="middle" x="444.016" y="-89" font-family="Courier,monospace" font-size="10.00">&lt;__main__.A object at 0xdeadbeef&gt;</text></g><g id="node2" class="node"><title>struct</title><polygon fill="#eeeeee" stroke="none" points="24,-182 24,-202 256,-202 256,-182 24,-182"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-182 24,-202 256,-202 256,-182 24,-182"/><text text-anchor="start" x="27" y="-188.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">struct _object* </text><text text-anchor="start" x="161.422" y="-188.8" font-family="Courier,monospace" font-style="oblique" font-size="14.00" fill="#666666">_ob_next</text><polygon fill="#eeeeee" stroke="none" points="24,-162 24,-182 256,-182 256,-162 24,-162"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-162 24,-182 256,-182 256,-162 24,-162"/><text text-anchor="start" x="27" y="-168.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">struct _object* </text><text text-anchor="start" x="161.422" y="-168.8" font-family="Courier,monospace" font-style="oblique" font-size="14.00" fill="#666666">_ob_prev</text><polygon fill="#eeeeee" stroke="none" points="24,-142 24,-162 256,-162 256,-142 24,-142"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-142 24,-162 256,-162 256,-142 24,-142"/><text text-anchor="start" x="27" y="-148.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">Py_ssize_t </text><text text-anchor="start" x="119.415" y="-148.8" font-family="Courier,monospace" font-size="14.00">ob_refcnt</text><polygon fill="#eeeeee" stroke="none" points="24,-122 24,-142 256,-142 256,-122 24,-122"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-122 24,-142 256,-142 256,-122 24,-122"/><text text-anchor="start" x="26.5815" y="-128.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">struct _typeobject* </text><text text-anchor="start" x="194.609" y="-128.8" font-family="Courier,monospace" font-size="14.00">ob_type</text><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-102 24,-122 256,-122 256,-102 24,-102"/><text text-anchor="start" x="27" y="-108.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">PyObject* </text><text text-anchor="start" x="111.014" y="-108.8" font-family="Courier,monospace" font-size="14.00">im_func</text><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-82 24,-102 256,-102 256,-82 24,-82"/><text text-anchor="start" x="27" y="-88.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">PyObject* </text><text text-anchor="start" x="111.014" y="-88.8" font-family="Courier,monospace" font-size="14.00">im_self</text><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-62 24,-82 256,-82 256,-62 24,-62"/><text text-anchor="start" x="27" y="-68.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">PyObject* </text><text text-anchor="start" x="111.014" y="-68.8" font-family="Courier,monospace" font-size="14.00">im_class</text><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-42 24,-62 256,-62 256,-42 24,-42"/><text text-anchor="start" x="27" y="-48.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">PyObject* </text><text text-anchor="start" x="111.014" y="-48.8" font-family="Courier,monospace" font-size="14.00">im_weakreflist</text></g><g id="edge1" class="edge"><title>struct:f&#45;&gt;obj</title><path fill="none" stroke="black" stroke-width="0.5" d="M257,-92C280.313,-92 305.269,-92 329.087,-92"/><polygon fill="black" stroke="black" stroke-width="0.5" points="329.277,-94.6251 336.777,-92 329.277,-89.3751 329.277,-94.6251"/></g></g></svg>
  543. The four gray fields of the struct come from
  544. [`PyObject_HEAD`](https://github.com/python/cpython/blob/2.7/Include/object.h#L78),
  545. which exist in all Python objects. The first two fields are from
  546. [`_PyObject_HEAD_EXTRA`](https://github.com/python/cpython/blob/2.7/Include/object.h#L66),
  547. and only exist when the debugging macro `Py_TRACE_REFS` is defined, in order to
  548. support more advanced reference counting. We can see that the `im_self` field,
  549. which mantains the reference to our target object, is either forth or sixth in
  550. the struct depending on `Py_TRACE_REFS`. If we can figure out the size of the
  551. field and its offset from the start of the struct, then we can set its value
  552. directly using `ctypes.memmove()`:
  553. {% highlight python %}
  554. ctypes.memmove(id(f) + offset, ctypes.byref(ctypes.py_object(b)), field_size)
  555. {% endhighlight %}
  556. Here, `id(f)` is the memory location of our method, which refers to the start
  557. of the C struct from above. `offset` is the number of bytes between this memory
  558. location and the start of the `im_self` field. We use
  559. [`ctypes.byref()`](https://docs.python.org/2/library/ctypes.html#ctypes.byref)
  560. to create a reference to the replacement object, `b`, which will be copied over
  561. the existing reference to `a`. Finally, `field_size` is the number of bytes
  562. we're copying, equal to the size of the `im_self` field.
  563. Well, all but one of these fields are pointers to structure types, meaning they
  564. have the same size,<sup><a id="ref5" href="#fn5">[5]</a></sup> equal to
  565. [`ctypes.sizeof(ctypes.py_object)`](https://docs.python.org/2/library/ctypes.html#ctypes.sizeof).
  566. This is (probably) 4 or 8 bytes, depending on whether you're on a 32-bit or a
  567. 64-bit system. The other field is a `Py_ssize_t` object—possibly the same size
  568. as the pointers, but we can't be sure—which is equal to
  569. `ctypes.sizeof(ctypes.c_ssize_t)`.
  570. We know that `field_size` must be `ctypes.sizeof(ctypes.py_object)`, since we
  571. are copying a structure pointer. `offset` is this value multiplied by the
  572. number of structure pointers before `im_self` (4 if `Py_TRACE_REFS` is defined
  573. and 2 otherwise), plus `ctypes.sizeof(ctypes.c_ssize_t)` for `ob_type`. But how
  574. do we determine if `Py_TRACE_REFS` is defined? We can't check the value of a
  575. macro at runtime, but we can check for the existence of
  576. [`sys.getobjects()`](https://github.com/python/cpython/blob/2.7/Misc/SpecialBuilds.txt#L54),
  577. which is
  578. [only defined when that macro is](https://github.com/python/cpython/blob/2.7/Python/sysmodule.c#L951).
  579. Therefore, we can make our replacement like so:
  580. {% highlight pycon %}
  581. >>> import ctypes
  582. >>> import sys
  583. >>> field_size = ctypes.sizeof(ctypes.py_object)
  584. >>> ptrs_in_struct = 4 if hasattr(sys, "getobjects") else 2
  585. >>> offset = ptrs_in_struct * field_size + ctypes.sizeof(ctypes.c_ssize_t)
  586. >>> ctypes.memmove(id(f) + offset, ctypes.byref(ctypes.py_object(b)), field_size)
  587. 4470258440
  588. >>> f.__self__ is b
  589. True
  590. >>> f()
  591. <__main__.B object at 0x10a8af290>
  592. {% endhighlight %}
  593. Excellent—it worked!
  594. There's another kind of bound method, which is the built-in variety as opposed
  595. to the user-defined variety we saw above. An example is `a.__sizeof__()`:
  596. {% highlight pycon %}
  597. >>> a, b = A(), B()
  598. >>> f = a.__sizeof__
  599. >>> f
  600. <built-in method __sizeof__ of A object at 0x10ab44b50>
  601. >>> replace(a, b)
  602. >>> f.__self__
  603. <__main__.A object at 0x10ab44b50>
  604. {% endhighlight %}
  605. This is stored internally as a
  606. [`PyCFunctionObject`](https://github.com/python/cpython/blob/2.7/Include/methodobject.h#L81).
  607. Let's take a look at its layout:
  608. <svg width="559pt" height="180pt" viewBox="0.00 18.00 559.03 180.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 206)"><polygon fill="white" stroke="none" points="-4,4 -4,-206 555.032,-206 555.032,4 -4,4"/><g id="clust2" class="cluster"><title>cluster</title><polygon fill="none" stroke="black" stroke-width="0" points="8,-8 8,-194 272,-194 272,-8 8,-8"/><text text-anchor="middle" x="140" y="-14.8" font-family="Courier,monospace" font-size="14.00">PyCFunctionObject</text></g><g id="node1" class="node"><title>obj</title><polygon fill="none" stroke="black" stroke-width="0.5" points="551.048,-90 336.984,-90 336.984,-54 551.048,-54 551.048,-90"/><text text-anchor="middle" x="444.016" y="-69" font-family="Courier,monospace" font-size="10.00">&lt;__main__.A object at 0xdeadbeef&gt;</text></g><g id="node2" class="node"><title>struct</title><polygon fill="#eeeeee" stroke="none" points="24,-162 24,-182 256,-182 256,-162 24,-162"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-162 24,-182 256,-182 256,-162 24,-162"/><text text-anchor="start" x="27" y="-168.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">struct _object* </text><text text-anchor="start" x="161.422" y="-168.8" font-family="Courier,monospace" font-style="oblique" font-size="14.00" fill="#666666">_ob_next</text><polygon fill="#eeeeee" stroke="none" points="24,-142 24,-162 256,-162 256,-142 24,-142"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-142 24,-162 256,-162 256,-142 24,-142"/><text text-anchor="start" x="27" y="-148.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">struct _object* </text><text text-anchor="start" x="161.422" y="-148.8" font-family="Courier,monospace" font-style="oblique" font-size="14.00" fill="#666666">_ob_prev</text><polygon fill="#eeeeee" stroke="none" points="24,-122 24,-142 256,-142 256,-122 24,-122"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-122 24,-142 256,-142 256,-122 24,-122"/><text text-anchor="start" x="27" y="-128.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">Py_ssize_t </text><text text-anchor="start" x="119.415" y="-128.8" font-family="Courier,monospace" font-size="14.00">ob_refcnt</text><polygon fill="#eeeeee" stroke="none" points="24,-102 24,-122 256,-122 256,-102 24,-102"/><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-102 24,-122 256,-122 256,-102 24,-102"/><text text-anchor="start" x="26.5815" y="-108.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">struct _typeobject* </text><text text-anchor="start" x="194.609" y="-108.8" font-family="Courier,monospace" font-size="14.00">ob_type</text><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-82 24,-102 256,-102 256,-82 24,-82"/><text text-anchor="start" x="27" y="-88.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">PyMethodDef* </text><text text-anchor="start" x="136.218" y="-88.8" font-family="Courier,monospace" font-size="14.00">m_ml</text><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-62 24,-82 256,-82 256,-62 24,-62"/><text text-anchor="start" x="27" y="-68.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">PyObject* </text><text text-anchor="start" x="111.014" y="-68.8" font-family="Courier,monospace" font-size="14.00">m_self</text><polygon fill="none" stroke="black" stroke-width="0.5" points="24,-42 24,-62 256,-62 256,-42 24,-42"/><text text-anchor="start" x="27" y="-48.8" font-family="Courier,monospace" font-size="14.00" fill="#888888">PyObject* </text><text text-anchor="start" x="111.014" y="-48.8" font-family="Courier,monospace" font-size="14.00">m_module</text></g><g id="edge1" class="edge"><title>struct:f&#45;&gt;obj</title><path fill="none" stroke="black" stroke-width="0.5" d="M257,-72C280.313,-72 305.269,-72 329.087,-72"/><polygon fill="black" stroke="black" stroke-width="0.5" points="329.277,-74.6251 336.777,-72 329.277,-69.3751 329.277,-74.6251"/></g></g></svg>
  609. Fortunately, `m_self` here has the same offset as `im_self` from before, so we
  610. can just use the same code:
  611. {% highlight pycon %}
  612. >>> ctypes.memmove(id(f) + offset, ctypes.byref(ctypes.py_object(b)), field_size)
  613. 4474703768
  614. >>> f.__self__ is b
  615. True
  616. >>> f
  617. <built-in method __sizeof__ of B object at 0x10ab4f150>
  618. {% endhighlight %}
  619. ### Dictionary keys
  620. Dictionary keys have a different reference relation type than values, but the
  621. replacement works mostly the same way. We pop the value of the old key from the
  622. dictionary, and then insert it in again under the new key. Here's the code,
  623. which we'll stick into the main block in `replace()`:
  624. {% highlight python %}
  625. elif isinstance(relation, Path.R_INDEXKEY):
  626. source = path.src.theone
  627. source[new] = source.pop(source.keys()[relation.r])
  628. {% endhighlight %}
  629. And, a demonstration:
  630. {% highlight pycon %}
  631. >>> a, b = A(), B()
  632. >>> d = {a: 1}
  633. >>> replace(a, b)
  634. >>> d
  635. {<__main__.B object at 0x10fb47950>: 1}
  636. {% endhighlight %}
  637. ### Closure cells
  638. We'll cover just one more case, this time involving a
  639. [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)). Here's
  640. our test function:
  641. {% highlight python %}
  642. def wrapper(obj):
  643. def inner():
  644. return obj
  645. return inner
  646. {% endhighlight %}
  647. As we can see, an instance of the inner function keeps references to the locals
  648. of the wrapper function, even after using our current
  649. version of `replace()`:
  650. {% highlight pycon %}
  651. >>> a, b = A(), B()
  652. >>> f = wrapper(a)
  653. >>> f()
  654. <__main__.A object at 0x109446090>
  655. >>> replace(a, b)
  656. >>> f()
  657. <__main__.A object at 0x109446090>
  658. {% endhighlight %}
  659. Internally, CPython implements this using things called
  660. [_cells_](https://docs.python.org/2/c-api/cell.html). We notice that
  661. `f.func_closure` gives us a tuple of `cell` objects, and we can examine an
  662. individual cell's contents with `.cell_contents`:
  663. {% highlight pycon %}
  664. >>> f.func_closure
  665. (<cell at 0x10ad9f478: instance object at 0x109446090>,)
  666. >>> f.func_closure[0].cell_contents
  667. <__main__.A object at 0x109446090>
  668. {% endhighlight %}
  669. As expected, we can't just modify it...
  670. {% highlight pycon %}
  671. >>> f.func_closure[0].cell_contents = b
  672. Traceback (most recent call last):
  673. File "<stdin>", line 1, in <module>
  674. AttributeError: attribute 'cell_contents' of 'cell' objects is not writable
  675. {% endhighlight %}
  676. ...because that would be too easy. So, how can we replace it? Well, we could
  677. go back to `memmove`, but there's an easier way thanks to the `ctypes` module
  678. also exposing Python's C API. Specifically, the
  679. [`PyCell_Set`](https://docs.python.org/2/c-api/cell.html#c.PyCell_Set) function
  680. (which seems to lack a pure Python equivalent) does exactly what we want. Since
  681. the function expects `PyObject*`s as arguments, we'll need to use
  682. `ctypes.py_object` as a wrapper. Here it is:
  683. {% highlight pycon %}
  684. >>> from ctypes import py_object, pythonapi
  685. >>> pythonapi.PyCell_Set(py_object(f.func_closure[0]), py_object(b))
  686. 0
  687. >>> f()
  688. <__main__.B object at 0x10ad94dd0>
  689. {% endhighlight %}
  690. Perfect – the replacement worked. To tie it together with `replace()`, we'll
  691. note that Guppy represents the cell contents relationship with
  692. `Based_R_INTERATTR`, for what I assume to be "internal attribute". We can use
  693. this to find the cell object within the inner function that references our
  694. target object, and then use the method above to make the change:
  695. {% highlight python %}
  696. elif isinstance(relation, Path.R_INTERATTR):
  697. if isinstance(source, CellType):
  698. pythonapi.PyCell_Set(py_object(source), py_object(new))
  699. return
  700. {% endhighlight %}
  701. ### Other cases
  702. There are many, many more types of possible replacements. I've written a more
  703. extensible version of `replace()` with some test cases, which can be viewed
  704. on Gist [here](https://gist.github.com/earwig/28a64ffb94d51a608e3d).
  705. Certainly, not every case is handled by it, but it seems to cover the majority
  706. that I've found through testing. There are a number of reference relations in
  707. Guppy that I couldn't figure out how to replicate without doing something
  708. insane (`R_HASATTR`, `R_CELL`, and `R_STACK`), so some obscure replacements are
  709. likely unimplemented.
  710. Some other kinds of replacements are known, but impossible. For example,
  711. replacing a class object that uses `__slots__` with another class will not work
  712. if the replacement class has a different slot layout and instances of the old
  713. class exist. More generally, replacing a class with a non-class object won't
  714. work if instances of the class exist. Furthermore, references stored in data
  715. structures managed by C extensions cannot be changed, since there's no good way
  716. for us to track these.
  717. ## Footnotes
  718. 1. <a id="fn1" href="#ref1">^</a> This post relies _heavily_ on implementation
  719. details of CPython 2.7. While it could be adapted for Python 3 by examining
  720. changes to the internal structures of objects that we used above, that would
  721. be a lost cause if you wanted to replicate this on
  722. [Jython](http://www.jython.org/) or some other implementation. We are so
  723. dependent on concepts specific to CPython that you would need to start from
  724. scratch, beginning with a language-specific replacement for Guppy.
  725. 2. <a id="fn2" href="#ref2">^</a> The
  726. [DOT files](https://en.wikipedia.org/wiki/DOT_(graph_description_language))
  727. used to generate graphs in this post are
  728. [available on Gist](https://gist.github.com/earwig/edc13f04f871c110eea6).
  729. 3. <a id="fn3" href="#ref3">^</a> They're actually grouped together by _clodo_
  730. ("class or dict object"), which is similar to type, but groups `__dict__`s
  731. separately by their owner's type.
  732. 4. <a id="fn4" href="#ref4">^</a> Python's documentation tells us not to modify
  733. the locals dictionary, but screw that; we're gonna do it anyway.
  734. 5. <a id="fn5" href="#ref5">^</a> According to the
  735. [C99](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) and
  736. [C11 standards](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf);
  737. section 6.2.5.27 in the former and 6.2.5.28 in the latter: "All pointers to
  738. structure types shall have the same representation and alignment
  739. requirements as each other."