memory-graph 0.3.5__tar.gz → 0.3.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memory_graph-0.3.5/memory_graph.egg-info → memory_graph-0.3.7}/PKG-INFO +161 -137
- {memory_graph-0.3.5 → memory_graph-0.3.7}/README.md +160 -136
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/__init__.py +50 -44
- {memory_graph-0.3.5 → memory_graph-0.3.7/memory_graph.egg-info}/PKG-INFO +161 -137
- {memory_graph-0.3.5 → memory_graph-0.3.7}/setup.py +1 -1
- {memory_graph-0.3.5 → memory_graph-0.3.7}/LICENSE.txt +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/MANIFEST.in +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/config.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/config_default.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/config_helpers.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/extension_numpy.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/extension_pandas.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/html_table.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/list_view.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/memory_to_nodes.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/node_base.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/node_key_value.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/node_linear.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/node_table.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/sequence.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/slicer.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/slices.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/slices_iterator.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/slices_table_iterator.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/t.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/test.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/test_memory_graph.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/test_memory_to_nodes.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/test_sequence.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/test_slicer.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/test_slices.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/test_slices_iterator.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph/utils.py +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph.egg-info/SOURCES.txt +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph.egg-info/dependency_links.txt +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph.egg-info/requires.txt +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/memory_graph.egg-info/top_level.txt +0 -0
- {memory_graph-0.3.5 → memory_graph-0.3.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: memory_graph
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Draws a graph of your data to analyze its structure.
|
|
5
5
|
Home-page: https://github.com/bterwijn/memory_graph
|
|
6
6
|
Author: Bas Terwijn
|
|
@@ -31,7 +31,7 @@ In Python, assigning the list from variable `a` to variable `b` causes both vari
|
|
|
31
31
|
<table><tr><td>
|
|
32
32
|
|
|
33
33
|
```python
|
|
34
|
-
import memory_graph
|
|
34
|
+
import memory_graph as mg
|
|
35
35
|
|
|
36
36
|
# create the lists 'a' and 'b'
|
|
37
37
|
a = [4, 3, 2]
|
|
@@ -47,7 +47,7 @@ print('ids:', id(a), id(b))
|
|
|
47
47
|
print('identical?:', a is b)
|
|
48
48
|
|
|
49
49
|
# show all local variables in a graph
|
|
50
|
-
|
|
50
|
+
mg.show( locals() )
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
</td><td>
|
|
@@ -71,7 +71,7 @@ A better way to understand what data is shared is to draw a graph of the data us
|
|
|
71
71
|
The [memory_graph](https://pypi.org/project/memory-graph/) package can graph many different data types.
|
|
72
72
|
|
|
73
73
|
```python
|
|
74
|
-
import memory_graph
|
|
74
|
+
import memory_graph as mg
|
|
75
75
|
|
|
76
76
|
class MyClass:
|
|
77
77
|
|
|
@@ -80,35 +80,35 @@ class MyClass:
|
|
|
80
80
|
self.y = y
|
|
81
81
|
|
|
82
82
|
data = [ range(1, 2), (3, 4), {5, 6}, {7:'seven', 8:'eight'}, MyClass(9, 10) ]
|
|
83
|
-
|
|
83
|
+
mg.show(data)
|
|
84
84
|
```
|
|
85
85
|

|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Instead of showing the graph you can also render it to an output file of your choosing (see [Graphviz Output Formats](https://graphviz.org/docs/outputs/)) using for example:
|
|
88
88
|
|
|
89
89
|
```python
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
mg.render(data, "my_graph.pdf")
|
|
91
|
+
mg.render(data, "my_graph.png")
|
|
92
|
+
mg.render(data, "my_graph.gv") # Graphviz DOT file
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
# Chapters #
|
|
96
96
|
|
|
97
|
-
[
|
|
97
|
+
[Python Data Model](#python-data-model)
|
|
98
98
|
|
|
99
|
-
[
|
|
99
|
+
[Call Stack](#call-stack)
|
|
100
100
|
|
|
101
|
-
[
|
|
101
|
+
[Debugging](#Debugging)
|
|
102
102
|
|
|
103
|
-
[
|
|
103
|
+
[Datastructure Examples](#datastructure-examples)
|
|
104
104
|
|
|
105
|
-
[
|
|
105
|
+
[Configuration](#configuration)
|
|
106
106
|
|
|
107
|
-
[
|
|
107
|
+
[Extensions](#extensions)
|
|
108
108
|
|
|
109
|
-
[
|
|
109
|
+
[Jupyter Notebook](#jupyter-notebook)
|
|
110
110
|
|
|
111
|
-
[
|
|
111
|
+
[Troubleshooting](#troubleshooting)
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
## Author ##
|
|
@@ -117,27 +117,30 @@ Bas Terwijn
|
|
|
117
117
|
## Inspiration ##
|
|
118
118
|
Inspired by [Python Tutor](https://pythontutor.com/).
|
|
119
119
|
|
|
120
|
+
## Supported by ##
|
|
121
|
+
<img src="https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/uva.png" alt="University of Amsterdam" width="600">
|
|
122
|
+
|
|
120
123
|
___
|
|
121
124
|
___
|
|
122
125
|
|
|
123
|
-
##
|
|
126
|
+
## Python Data Model ##
|
|
124
127
|
The [Python Data Model](https://docs.python.org/3/reference/datamodel.html) makes a distiction between immutable and mutable types:
|
|
125
128
|
|
|
126
129
|
* **immutable**: bool, int, float, complex, str, tuple, bytes, frozenset
|
|
127
|
-
* **mutable**: list,
|
|
130
|
+
* **mutable**: list, set, dict, classes, ... (most other types)
|
|
128
131
|
|
|
129
132
|
|
|
130
133
|
### Immutable Type ###
|
|
131
|
-
In the code below variable `a` and `b` both reference the same tuple value (4, 3, 2). A tuple is an immutable type and therefore when we change variable `a` its value
|
|
134
|
+
In the code below variable `a` and `b` both reference the same tuple value (4, 3, 2). A tuple is an immutable type and therefore when we change variable `a` its value **cannot** be mutated in place, and thus a copy is made and `a` and `b` reference a different value afterwards.
|
|
132
135
|
|
|
133
136
|
```python
|
|
134
|
-
import memory_graph
|
|
137
|
+
import memory_graph as mg
|
|
135
138
|
|
|
136
139
|
a = (4, 3, 2)
|
|
137
140
|
b = a
|
|
138
|
-
|
|
141
|
+
mg.render(locals(), 'immutable1.png')
|
|
139
142
|
a += (1,)
|
|
140
|
-
|
|
143
|
+
mg.render(locals(), 'immutable2.png')
|
|
141
144
|
```
|
|
142
145
|
|  |  |
|
|
143
146
|
|:-----------------------------------------------------------:|:-------------------------------------------------------------:|
|
|
@@ -148,27 +151,25 @@ memory_graph.render(locals(), 'immutable2.png')
|
|
|
148
151
|
With mutable types the result is different. In the code below variable `a` and `b` both reference the same `list` value [4, 3, 2]. A `list` is a mutable type and therefore when we change variable `a` its value **can** be mutated in place and thus `a` and `b` both reference the same new value afterwards. Thus changing `a` also changes `b` and vice versa. Sometimes we want this but other times we don't and then we will have to make a copy so that `b` is independent from `a`.
|
|
149
152
|
|
|
150
153
|
```python
|
|
151
|
-
import memory_graph
|
|
154
|
+
import memory_graph as mg
|
|
152
155
|
|
|
153
156
|
a = [4, 3, 2]
|
|
154
157
|
b = a
|
|
155
|
-
|
|
158
|
+
mg.render(locals(), 'mutable1.png')
|
|
156
159
|
a += [1] # equivalent to: a.append(1)
|
|
157
|
-
|
|
160
|
+
mg.render(locals(), 'mutable2.png')
|
|
158
161
|
```
|
|
159
162
|
|  |  |
|
|
160
163
|
|:-----------------------------------------------------------:|:-------------------------------------------------------------:|
|
|
161
164
|
| mutable1.png | mutable2.png |
|
|
162
165
|
|
|
163
|
-
|
|
164
|
-
Python makes this distiction between mutable and immutable types because a value of a mutable type could be large and therefore it would be slow to make a copy each time we change it. On the other hand, a value of a immutable type generally is small and therefore fast to copy.
|
|
165
|
-
|
|
166
|
+
One practical reason why Python makes the distinction between mutable and immutable types is that a value of a mutable type could be large, making it inefficient to copy each time we change it. Immutable values generally don't need to change as much or are smaller, which makes copying less of a concern.
|
|
166
167
|
|
|
167
168
|
### Copying ###
|
|
168
169
|
Python offers three different "copy" options that we will demonstrate using a nested list:
|
|
169
170
|
|
|
170
171
|
```python
|
|
171
|
-
import memory_graph
|
|
172
|
+
import memory_graph as mg
|
|
172
173
|
import copy
|
|
173
174
|
|
|
174
175
|
a = [ [1, 2], ['x', 'y'] ] # a nested list (a list containing lists)
|
|
@@ -178,21 +179,21 @@ c1 = a
|
|
|
178
179
|
c2 = copy.copy(a) # equivalent to: a.copy() a[:] list(a)
|
|
179
180
|
c3 = copy.deepcopy(a)
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
mg.show(locals())
|
|
182
183
|
```
|
|
183
184
|
|
|
184
|
-
* `c1` is an **assignment**, all the
|
|
185
|
-
* `c2` is a **shallow copy**, only the
|
|
186
|
-
* `c3` is a **deep copy**, all the
|
|
185
|
+
* `c1` is an **assignment**, nothing is copied, all the values are shared
|
|
186
|
+
* `c2` is a **shallow copy**, only the value referenced by the first reference is copied, all the underlying values are shared
|
|
187
|
+
* `c3` is a **deep copy**, all the values are copied, nothing is shared
|
|
187
188
|
|
|
188
189
|

|
|
189
190
|
|
|
190
191
|
|
|
191
192
|
### Custom Copy Method ###
|
|
192
|
-
We can write our own custom copy function or method in case the three "copy" options don't do what we want. For example the copy() method of My_Class
|
|
193
|
+
We can write our own custom copy function or method in case the three "copy" options don't do what we want. For example, in the code below the copy() method of My_Class copies the `digits` but shares the `letters` between two objects.
|
|
193
194
|
|
|
194
195
|
```python
|
|
195
|
-
import memory_graph
|
|
196
|
+
import memory_graph as mg
|
|
196
197
|
import copy
|
|
197
198
|
|
|
198
199
|
class My_Class:
|
|
@@ -209,70 +210,22 @@ class My_Class:
|
|
|
209
210
|
a = My_Class()
|
|
210
211
|
b = a.copy()
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
mg.show(locals())
|
|
213
214
|
```
|
|
214
215
|

|
|
215
216
|
|
|
216
217
|
|
|
217
|
-
##
|
|
218
|
-
|
|
219
|
-
```python
|
|
220
|
-
memory_graph.show(locals(), block=True)
|
|
221
|
-
```
|
|
218
|
+
## Call Stack ##
|
|
219
|
+
The `mg.get_call_stack()` function retrieves the entire call stack, including the local variables for each function on the stack. This enables us to visualize the local variables across all active functions simultaneously. Then by examining the graph, we can determine whether any local variables from different functions on the call stack share data. For instance, consider the function `add_one()` which adds the value `1` to each of its parameters, `a`, `b`, and `c`.
|
|
222
220
|
|
|
223
|
-
So much so that function `d()` is available as alias for this for easier debugging. Additionally it can optionally log the data by printing them. For example:
|
|
224
221
|
```python
|
|
225
|
-
import memory_graph
|
|
226
|
-
|
|
227
|
-
squares = []
|
|
228
|
-
squares_collector = []
|
|
229
|
-
for i in range(1,6):
|
|
230
|
-
squares.append(i**2)
|
|
231
|
-
squares_collector.append(squares.copy())
|
|
232
|
-
memory_graph.d(log=True)
|
|
233
|
-
```
|
|
234
|
-
which after pressing ENTER a number of times results in:
|
|
235
|
-
|
|
236
|
-

|
|
237
|
-
```
|
|
238
|
-
squares: [1, 4, 9, 16, 25]
|
|
239
|
-
squares_collector: [[1], [1, 4], [1, 4, 9], [1, 4, 9, 16], [1, 4, 9, 16, 25]]
|
|
240
|
-
i: 5
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
Function `d()` has these default arguments:
|
|
244
|
-
```python
|
|
245
|
-
def d(data=None, graph=True, log=False, block=True):
|
|
246
|
-
```
|
|
247
|
-
- data: the data that is handled, defaults to `locals()` when not specified
|
|
248
|
-
- graph: if True the data is visualized as a graph
|
|
249
|
-
- log: if True the data is printed
|
|
250
|
-
- block: if True the function blocks until the ENTER key is pressed
|
|
251
|
-
|
|
252
|
-
To print to a log file instead of standard output use:
|
|
253
|
-
```python
|
|
254
|
-
memory_graph.log_file = open("my_log_file.txt", "w")
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Watchpoint in Debugger ###
|
|
258
|
-
Alternatively you get an even better debugging experience when you set expression:
|
|
259
|
-
```
|
|
260
|
-
memory_graph.render(locals(), "my_debug_graph.pdf")
|
|
261
|
-
```
|
|
262
|
-
as a *watchpoint* in a debugger tool and open the "my_debug_graph.pdf" output file. This continuouly shows the graph of all the local variables while debugging and avoids having to add any memory_graph `show()`, `render()`, or `d()` calls to your code.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
## 3. Call Stack ##
|
|
266
|
-
The function `memory_graph.get_call_stack()` returns the complete call stack, including all local variables for each function in the stack. This allows us to simultaneously visualize the local variables of all the called functions. By doing so, we can identify whether any local variables from different functions in the call stack share data with one another. Here for example we call function ```add_one()``` with arguments ```a, b, c``` that adds 1 to each of its arguments.
|
|
267
|
-
|
|
268
|
-
```python
|
|
269
|
-
import memory_graph
|
|
222
|
+
import memory_graph as mg
|
|
270
223
|
|
|
271
224
|
def add_one(a, b, c):
|
|
272
225
|
a += [1]
|
|
273
226
|
b += (1,)
|
|
274
227
|
c += [1]
|
|
275
|
-
|
|
228
|
+
mg.show(mg.get_call_stack())
|
|
276
229
|
|
|
277
230
|
a = [4, 3, 2]
|
|
278
231
|
b = (4, 3, 2)
|
|
@@ -288,55 +241,127 @@ In the printed output only `a` is changed as a result of the function call:
|
|
|
288
241
|
a:[4, 3, 2, 1] b:(4, 3, 2) c:[4, 3, 2]
|
|
289
242
|
```
|
|
290
243
|
|
|
291
|
-
This is because `b` is of immutable type 'tuple' so its value gets copied automatically when it is changed. And because the function is called with a copy of `c`, its original value is not changed by the function. The value of variable `a` is the only value of mutable type that is shared between the root stack frame **0: \<module
|
|
244
|
+
This is because `b` is of immutable type 'tuple' so its value gets copied automatically when it is changed. And because the function is called with a copy of `c`, its original value is not changed by the function. The value of variable `a` is the only value of mutable type that is shared between the root stack frame **'0: \<module>'** and the **'1: add_one'** stack frame of the function so only that variable is affected as a result of the function call. The other changes remain confined to the local variables of the ```add_one()``` function.
|
|
245
|
+
|
|
246
|
+
### Block ###
|
|
247
|
+
It is often helpful to temporarily block program execution to inspect the graph. For this, you can use the `mg.block()` function:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
mg.block(fun, arg1, arg2, ..., loc=True)
|
|
251
|
+
```
|
|
292
252
|
|
|
253
|
+
This function first executes `fun(arg1, arg2, ...)`, then prints the current source location in the program, and blocks execution until the <Enter> key is pressed. To skip printing the source location, set `loc=False`.
|
|
293
254
|
|
|
294
255
|
### Recursion ###
|
|
295
|
-
The call stack
|
|
256
|
+
The call stack is also helpful to visualize how recursion works. Here we use `mg.block()` to show each step of how recursively ```factorial(3)``` is computed:
|
|
296
257
|
|
|
297
258
|
```python
|
|
298
|
-
import memory_graph
|
|
259
|
+
import memory_graph as mg
|
|
299
260
|
|
|
300
261
|
def factorial(n):
|
|
301
262
|
if n==0:
|
|
302
263
|
return 1
|
|
303
|
-
|
|
264
|
+
mg.block(mg.show, mg.get_call_stack())
|
|
304
265
|
result = n * factorial(n-1)
|
|
305
|
-
|
|
266
|
+
mg.block(mg.show, mg.get_call_stack())
|
|
306
267
|
return result
|
|
307
268
|
|
|
308
269
|
print(factorial(3))
|
|
309
270
|
```
|
|
271
|
+
|
|
310
272
|

|
|
311
273
|
|
|
312
|
-
and the
|
|
274
|
+
and the result is: 1 x 2 x 3 = 6
|
|
275
|
+
|
|
276
|
+
### Power Set ###
|
|
277
|
+
A more interesting recursive example that shows sharing of data is power_set(). A power set is the set of all subsets of a collection of values.
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
import memory_graph as mg
|
|
281
|
+
|
|
282
|
+
def get_subsets(subsets, data, i, subset):
|
|
283
|
+
mg.block(mg.show, mg.get_call_stack())
|
|
284
|
+
if i == len(data):
|
|
285
|
+
subsets.append(subset.copy())
|
|
286
|
+
return
|
|
287
|
+
subset.append(data[i])
|
|
288
|
+
get_subsets(subsets, data, i+1, subset) # do include data[i]
|
|
289
|
+
subset.pop()
|
|
290
|
+
get_subsets(subsets, data, i+1, subset) # don't include data[i]
|
|
291
|
+
mg.block(mg.show, mg.get_call_stack())
|
|
292
|
+
|
|
293
|
+
def power_set(data):
|
|
294
|
+
subsets = []
|
|
295
|
+
get_subsets(subsets, data, 0, [])
|
|
296
|
+
return subsets
|
|
297
|
+
|
|
298
|
+
print( power_set(['a', 'b', 'c']) )
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+

|
|
302
|
+
```
|
|
303
|
+
[['a', 'b', 'c'], ['a', 'b'], ['a', 'c'], ['a'], ['b', 'c'], ['b'], ['c'], []]
|
|
304
|
+
```
|
|
313
305
|
|
|
314
|
-
|
|
315
|
-
|
|
306
|
+
|
|
307
|
+
## Debugging ##
|
|
308
|
+
|
|
309
|
+
For the best debugging experience with memory_graph set for example expression:
|
|
310
|
+
```
|
|
311
|
+
mg.render(locals(), "my_graph.pdf")
|
|
312
|
+
```
|
|
313
|
+
as a *watch* in a debugger tool such as the integrated debugger in Visual Studio Code. Then open the "my_graph.pdf" output file to continuously see all the local variables while debugging. This avoids having to add any memory_graph `show()`, `render()` calls to your code.
|
|
314
|
+
|
|
315
|
+
### Call Stack in Watch Context ###
|
|
316
|
+
The ```mg.get_call_stack()``` doesn't work well in *watch* context in most debuggers because debuggers introduce additional stack frames that cause problems. Use these alternative functions for various debuggers to filter out these problematic stack frames:
|
|
316
317
|
|
|
317
318
|
| debugger | function to get the call stack |
|
|
318
319
|
|:---|:---|
|
|
319
|
-
| **pdb, pudb** | `
|
|
320
|
-
| **Visual Studio Code** | `
|
|
321
|
-
| **Pycharm** | `
|
|
320
|
+
| **pdb, pudb** | `mg.get_call_stack_pdb()` |
|
|
321
|
+
| **Visual Studio Code** | `mg.get_call_stack_vscode()` |
|
|
322
|
+
| **Pycharm** | `mg.get_call_stack_pycharm()` |
|
|
322
323
|
|
|
323
324
|
#### Other Debuggers ####
|
|
324
|
-
For other debuggers, invoke this function within the
|
|
325
|
+
For other debuggers, invoke this function within the *watch* context. Then, in the "call_stack.txt" file, identify the slice of functions you wish to include in the call stack.
|
|
325
326
|
```
|
|
326
|
-
|
|
327
|
+
mg.save_call_stack("call_stack.txt")
|
|
327
328
|
```
|
|
328
|
-
and then call this function to get the desired call stack
|
|
329
|
+
Choose 'after' and 'up_to' what function you want to slice and then call this function to get the desired call stack:
|
|
329
330
|
```
|
|
330
|
-
|
|
331
|
+
mg.get_call_stack_after_up_to(after_function, up_to_function="<module>")
|
|
331
332
|
```
|
|
332
333
|
|
|
334
|
+
### Debugging without Debugger Tool ###
|
|
335
|
+
|
|
336
|
+
To simplify debugging without a debugger tool, we offer these blocking alias functions that you can insert into your code at a specific point to visualize a graph:
|
|
337
|
+
|
|
338
|
+
| alias | purpose | function call |
|
|
339
|
+
|:---|:---|:---|
|
|
340
|
+
| `mg.l()` | graph **l**ocal variables | `mg.block(mg.show, locals())` |
|
|
341
|
+
| `mg.s()` | graph the call **s**tack | `mg.block(mg.show, mg.get_call_stack())` |
|
|
342
|
+
|
|
343
|
+
For example, executing this program:
|
|
333
344
|
|
|
334
|
-
|
|
345
|
+
```python
|
|
346
|
+
from memory_graph as mg
|
|
347
|
+
|
|
348
|
+
squares = []
|
|
349
|
+
squares_collector = []
|
|
350
|
+
for i in range(1, 6):
|
|
351
|
+
squares.append(i**2)
|
|
352
|
+
squares_collector.append(squares.copy())
|
|
353
|
+
mg.l() # graph local variables
|
|
354
|
+
```
|
|
355
|
+
and pressing <Enter> a number of times, results in:
|
|
356
|
+
|
|
357
|
+

|
|
358
|
+
|
|
359
|
+
## Datastructure Examples ##
|
|
335
360
|
Module memory_graph can be very useful in a course about datastructures, some examples:
|
|
336
361
|
|
|
337
362
|
### Doubly Linked List ###
|
|
338
363
|
```python
|
|
339
|
-
import memory_graph
|
|
364
|
+
import memory_graph as mg
|
|
340
365
|
import random
|
|
341
366
|
random.seed(0) # use same random numbers each run
|
|
342
367
|
|
|
@@ -362,7 +387,7 @@ class LinkedList:
|
|
|
362
387
|
new_node.next = self.head
|
|
363
388
|
self.head.prev = new_node
|
|
364
389
|
self.head = new_node
|
|
365
|
-
|
|
390
|
+
mg.block(mg.show, locals()) # <--- draw graph
|
|
366
391
|
|
|
367
392
|
linked_list = LinkedList()
|
|
368
393
|
n = 100
|
|
@@ -374,7 +399,7 @@ for i in range(n):
|
|
|
374
399
|
|
|
375
400
|
### Binary Tree ###
|
|
376
401
|
```python
|
|
377
|
-
import memory_graph
|
|
402
|
+
import memory_graph as mg
|
|
378
403
|
import random
|
|
379
404
|
random.seed(0) # use same random numbers each run
|
|
380
405
|
|
|
@@ -401,7 +426,7 @@ class BinTree:
|
|
|
401
426
|
node.larger = Node(new_value)
|
|
402
427
|
else:
|
|
403
428
|
self.add_recursive(new_value, node.larger)
|
|
404
|
-
|
|
429
|
+
mg.block(mg.show, locals()) # <--- draw graph
|
|
405
430
|
|
|
406
431
|
def add(self, value):
|
|
407
432
|
if self.root is None:
|
|
@@ -419,7 +444,7 @@ for i in range(n):
|
|
|
419
444
|
|
|
420
445
|
### Hash Set ###
|
|
421
446
|
```python
|
|
422
|
-
import memory_graph
|
|
447
|
+
import memory_graph as mg
|
|
423
448
|
import random
|
|
424
449
|
random.seed(0) # use same random numbers each run
|
|
425
450
|
|
|
@@ -434,7 +459,7 @@ class HashSet:
|
|
|
434
459
|
self.buckets[index] = []
|
|
435
460
|
bucket = self.buckets[index]
|
|
436
461
|
bucket.append(value)
|
|
437
|
-
|
|
462
|
+
mg.block(mg.show, locals()) # <--- draw graph
|
|
438
463
|
|
|
439
464
|
def contains(self, value):
|
|
440
465
|
index = hash(value) % len(self.buckets)
|
|
@@ -456,44 +481,44 @@ for i in range(n):
|
|
|
456
481
|

|
|
457
482
|
|
|
458
483
|
|
|
459
|
-
##
|
|
484
|
+
## Configuration ##
|
|
460
485
|
Different aspects of memory_graph can be configured. The default configuration is reset by importing 'memory_graph.config_default'.
|
|
461
486
|
|
|
462
|
-
- ***
|
|
463
|
-
- The maxium
|
|
487
|
+
- ***mg.config.max_tree_depth*** : int
|
|
488
|
+
- The maxium depth of the graph. A `★` symbol indictes where the graph is cut short.
|
|
464
489
|
|
|
465
|
-
- ***
|
|
490
|
+
- ***mg.config.max_string_length*** : int
|
|
466
491
|
- The maximum length of strings shown in the graph. Longer strings will be truncated.
|
|
467
492
|
|
|
468
|
-
- ***
|
|
493
|
+
- ***mg.config.not_node_types*** : set
|
|
469
494
|
- Holds all types for which no seperate node is drawn but that instead are shown as elements in their parent Node.
|
|
470
495
|
|
|
471
|
-
- ***
|
|
496
|
+
- ***mg.config.no_child_references_types*** : set
|
|
472
497
|
- The set of key_value types that don't draw references to their direct childeren but have their children shown as elements of their node.
|
|
473
498
|
|
|
474
|
-
- ***
|
|
499
|
+
- ***mg.config.type_to_node*** : dict
|
|
475
500
|
- Determines how a data types is converted to a Node (sub)class for visualization in the graph.
|
|
476
501
|
|
|
477
|
-
- ***
|
|
502
|
+
- ***mg.config.type_to_color*** : dict
|
|
478
503
|
- Maps each type to the [graphviz color](https://graphviz.org/doc/info/colors.html) it gets in the graph.
|
|
479
504
|
|
|
480
|
-
- ***
|
|
505
|
+
- ***mg.config.type_to_vertical_orientation*** : dict
|
|
481
506
|
- Maps each type to its orientation. Use 'True' for vertical and 'False' for horizontal. If not specified Node_Linear and Node_Key_Value are vertical unless they have references to children.
|
|
482
507
|
|
|
483
|
-
- ***
|
|
508
|
+
- ***mg.config.type_to_slicer*** : dict
|
|
484
509
|
- Maps each type to a Slicer. A slicer determines how many elements of a data type are shown in the graph to prevent the graph from getting too big. 'Slicer()' does no slicing, 'Slicer(1,2,3)' shows just 1 element at the beginning, 2 in the middle, and 3 at the end.
|
|
485
510
|
|
|
486
511
|
### Temporary Configuration ###
|
|
487
|
-
In addition to the global configuration, a temporary configuration can be set for a single `show()`, `render()`,
|
|
512
|
+
In addition to the global configuration, a temporary configuration can be set for a single `show()`, `render()`, `d()`, `ds()` call to change the colors, orientation, and slicer. This example highlights a particular list element in red, gives it a horizontal orientation, and overwrites the default slicer for lists:
|
|
488
513
|
|
|
489
514
|
```python
|
|
490
|
-
import memory_graph
|
|
491
|
-
from memory_graph.
|
|
515
|
+
import memory_graph as mg
|
|
516
|
+
from memory_graph.slicer import Slicer
|
|
492
517
|
|
|
493
518
|
data = [ list(range(20)) for i in range(1,5)]
|
|
494
519
|
highlight = data[2]
|
|
495
520
|
|
|
496
|
-
|
|
521
|
+
mg.show( locals(),
|
|
497
522
|
colors = {id(highlight): "red" }, # set color to "red"
|
|
498
523
|
vertical_orientations = {id(highlight): False }, # set horizontal orientation
|
|
499
524
|
slicers = {id(highlight): Slicer()} # set no slicing
|
|
@@ -501,14 +526,14 @@ memory_graph.show( locals(),
|
|
|
501
526
|
```
|
|
502
527
|

|
|
503
528
|
|
|
504
|
-
##
|
|
529
|
+
## Extensions ##
|
|
505
530
|
Different extensions are available for types from other Python packages.
|
|
506
531
|
|
|
507
532
|
### Numpy ###
|
|
508
|
-
Numpy types `arrray` and `matrix` and `ndarray` can be graphed with
|
|
533
|
+
Numpy types `arrray` and `matrix` and `ndarray` can be graphed with "memory_graph.extension_numpy":
|
|
509
534
|
|
|
510
535
|
```python
|
|
511
|
-
import memory_graph
|
|
536
|
+
import memory_graph as mg
|
|
512
537
|
import numpy as np
|
|
513
538
|
import memory_graph.extension_numpy
|
|
514
539
|
np.random.seed(0) # use same random numbers each run
|
|
@@ -516,15 +541,15 @@ np.random.seed(0) # use same random numbers each run
|
|
|
516
541
|
array = np.array([1.1, 2, 3, 4, 5])
|
|
517
542
|
matrix = np.matrix([[i*20+j for j in range(20)] for i in range(20)])
|
|
518
543
|
ndarray = np.random.rand(20,20)
|
|
519
|
-
|
|
544
|
+
mg.show(locals())
|
|
520
545
|
```
|
|
521
546
|

|
|
522
547
|
|
|
523
548
|
### Pandas ###
|
|
524
|
-
Pandas types `Series` and `DataFrame` can be graphed with
|
|
549
|
+
Pandas types `Series` and `DataFrame` can be graphed with "memory_graph.extension_pandas":
|
|
525
550
|
|
|
526
551
|
```python
|
|
527
|
-
import memory_graph
|
|
552
|
+
import memory_graph as mg
|
|
528
553
|
import pandas as pd
|
|
529
554
|
import memory_graph.extension_pandas
|
|
530
555
|
|
|
@@ -535,21 +560,20 @@ dataframe2 = pd.DataFrame({ 'Name' : [ 'Tom', 'Anna', 'Steve', 'Lisa'],
|
|
|
535
560
|
'Age' : [ 28, 34, 29, 42],
|
|
536
561
|
'Length' : [ 1.70, 1.66, 1.82, 1.73] },
|
|
537
562
|
index=['one', 'two', 'three', 'four']) # with row names
|
|
538
|
-
|
|
563
|
+
mg.show(locals())
|
|
539
564
|
```
|
|
540
565
|

|
|
541
566
|
|
|
542
|
-
##
|
|
567
|
+
## Jupyter Notebook ##
|
|
543
568
|
|
|
544
|
-
In Jupyter Notebook `locals()` has additional variables that cause problems in the graph, use `
|
|
569
|
+
In Jupyter Notebook `locals()` has additional variables that cause problems in the graph, use `mg.locals_jupyter()` to get the local variables with these problematic variables filtered out. Use `mg.get_call_stack_jupyter()` to get the whole call stack with these variables filtered out.
|
|
545
570
|
|
|
546
571
|
See for example [jupyter_example.ipynb](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/jupyter_example.ipynb).
|
|
547
572
|

|
|
548
573
|
|
|
549
574
|
|
|
550
|
-
##
|
|
575
|
+
## Troubleshooting ##
|
|
551
576
|
|
|
552
|
-
- Adobe Acrobat Reader [doesn't refresh a PDF file](https://superuser.com/questions/337011/windows-pdf-viewer-that-auto-refreshes-pdf-when-compiling-with-pdflatex) when it changes on disk and blocks updates which results in an `Could not open 'somefile.pdf' for writing : Permission denied` error. One solution is to install a PDF reader that does refresh ([Evince](https://www.fosshub.com/Evince.html)
|
|
577
|
+
- Adobe Acrobat Reader [doesn't refresh a PDF file](https://superuser.com/questions/337011/windows-pdf-viewer-that-auto-refreshes-pdf-when-compiling-with-pdflatex) when it changes on disk and blocks updates which results in an `Could not open 'somefile.pdf' for writing : Permission denied` error. One solution is to install a PDF reader that does refresh ([Evince](https://www.fosshub.com/Evince.html), [Okular](https://okular.kde.org/), [SumatraPDF](https://www.sumatrapdfreader.org/), ...) and set it as the default PDF reader. Another solution is to `render()` the graph to a different output format and to open it manually.
|
|
553
578
|
|
|
554
579
|
- When graph edges overlap it can be hard to distinguish them. Using an interactive graphviz viewer, such as [xdot](https://github.com/jrfonseca/xdot.py), on a '*.gv' DOT output file will help.
|
|
555
|
-
|