See this issue: « Could we add a "super" display function that chooses the best one based on the datatype? »
%load_ext watermark
%watermark -v -m -p lolviz
CPython 3.6.9 IPython 7.16.1 lolviz 1.4.4 compiler : GCC 8.4.0 system : Linux release : 5.4.0-65-generic machine : x86_64 processor : x86_64 CPU cores : 8 interpreter: 64bit
import lolviz
modes = [ "str", "matrix", "call", "calls", "obj", "tree", "lol", "list", "tree" ]
def unified_lolviz(obj=None, mode:modes=None, **kwargs):
""" Unified function to display `obj` with lolviz, in Jupyter notebook only."""
try:
if mode == "str" or isinstance(obj, str):
return lolviz.strviz(obj, **kwargs)
elif "<class 'numpy.ndarray'>" == str(type(obj)):
# can't use isinstance(obj, np.ndarray) without import numpy!
return lolviz.matrixviz(obj, **kwargs)
elif mode == "matrix":
# Experimental transparent use of np.array()
try:
from numpy import array as _
return lolviz.matrixviz(_(obj), **kwargs)
del _
except:
return lolviz.matrixviz(obj, **kwargs)
elif mode == "call":
from sys import _getframe
return lolviz.callviz(frame=_getframe(1), **kwargs)
del _getframe
elif mode == "calls":
return lolviz.callsviz( **kwargs)
elif mode == "lol" or isinstance(obj, (tuple, list)) and obj and isinstance(obj[0], (tuple, list)):
# obj is a list, is non empty, and obj[0] is a list!
return lolviz.lolviz(obj, **kwargs)
elif mode == "list" or isinstance(obj, (tuple, list)):
return lolviz.listviz(obj, **kwargs)
elif mode == "tree" or isinstance(obj, dict):
return lolviz.treeviz(obj, **kwargs) # default
else:
return lolviz.objviz(obj, **kwargs) # default
except TypeError:
# unable to use lolviz, let's just return the object,
# it will be nicely displayed by IPython
return obj
viz = unified_lolviz
data = ['hi', 'mom', {3, 4}, {"parrt": "user"}]
viz(data)
I test here all the features of lolviz :
squares = [ i**2 for i in range(10) ]
squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
viz(squares)
Under the hood of this unified function viz
, it was calling lolvzi.listviz
:
lolviz.listviz(squares)
n, m = 3, 4
example_matrix = [[0 if i != j else 1 for i in range(n)] for j in range(m)]
example_matrix
[[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 0]]
viz(example_matrix)
If we want, we can force the lolviz.matrixviz
mode:
viz(example_matrix, mode='matrix')
And this works without having to manually import numpy or convert the list-of-list to a numpy array!
np?
Object `np` not found.
n, m, o = 2, 3, 4
example_3D_matrix = [[[
1 if i < j < k else 0
for i in range(n)]
for j in range(m)]
for k in range(o)]
example_3D_matrix
[[[0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0]], [[0, 0], [1, 0], [0, 0]], [[0, 0], [1, 0], [1, 1]]]
viz(example_3D_matrix)
It works, even if it is not as pretty.
Only for binary trees, apparently. Let's try with a dictionary that looks like a binary tree:
anakin = {
"name": "Anakin Skywalker",
"son": {
"name": "Luke Skywalker",
},
"daughter": {
"name": "Leia Skywalker",
},
}
from pprint import pprint
pprint(anakin)
{'daughter': {'name': 'Leia Skywalker'}, 'name': 'Anakin Skywalker', 'son': {'name': 'Luke Skywalker'}}
Then by remembering the correction function, we can do:
lolviz.treeviz(anakin)
But it's simpler to use the automatic function!
viz(anakin)
It works out of the box for dictionaries!
Let's check another example:
anakin_rec = {
"name": "Anakin Skywalker",
}
luke_rec = {
"name": "Luke Skywalker",
"father": anakin_rec,
}
leia_rec = {
"name": "Leia Skywalker",
"father": anakin_rec,
},
anakin_rec.update({
"son": luke_rec,
"daughter": leia_rec,
})
from pprint import pprint
pprint(anakin_rec)
{'daughter': ({'father': <Recursion on dict with id=139754864347464>, 'name': 'Leia Skywalker'},), 'name': 'Anakin Skywalker', 'son': {'father': <Recursion on dict with id=139754864347464>, 'name': 'Luke Skywalker'}}
But it's simpler to use the automatic function!
viz(anakin_rec)
It works too for recursive dictionaries!
Let's check another example:
class Tree:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
root = Tree('parrt',
Tree('mary',
Tree('jim',
Tree('srinivasan'),
Tree('april'))),
Tree('xue',None,Tree('mike')))
lolviz.treeviz(root)
viz(root, mode="tree")
viz(anakin)
viz(anakin.values())
viz(anakin.items())
For complex numbers for instance?
z = 1+4j
print(z)
(1+4j)
viz(z)
OK, this fails.
We could improve viz
for complex
, but it's beyond the scope of this small experiment.
Let's explore a recursive function:
def factorial(n):
if n < 0: return 0
elif n == 0: return 1
else: return n * factorial(n - 1)
for n in range(12):
print(f"{n:>2}! = {factorial(n):>10}")
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800 11! = 39916800
And now with some visualization:
from IPython.display import display
This is what lolviz.callviz
shows:
def factorial2(n):
display(lolviz.callviz(varnames=["n"]))
if n < 0: return 0
elif n == 0: return 1
else: return n * factorial2(n - 1)
n = 4
print(f"{n:>2}! = {factorial2(n):>10}")
4! = 24
We really see the "call stack" as the system keeps track of the nested calls. I like that! 👌
But again, I'm lazy, I want to just use the viz
function:
def factorial3(n):
display(viz(varnames=["n"], mode="call"))
if n < 0: return 0
elif n == 0: return 1
else: return n * factorial3(n - 1)
n = 4
print(f"{n}! = {factorial3(n)}")
4! = 24
Okay, by using the optional frame=
argument of lolviz.callviz
, and some sys._getframe
black magic, we can do this!
See https://code.activestate.com/recipes/579105-how-a-python-function-can-find-the-name-of-its-cal/
Can we write a @show_recursion
function decorator, to automatically do this visualization of recursive calls for us?
def show_recursion(varnames=None):
def showing_recursion_with_varnames(function):
nonlocal varnames
# https://stackoverflow.com/questions/582056/getting-list-of-parameter-names-inside-python-function#4051447
if varnames is None:
varnames = function.__code__.co_varnames
import lolviz
from IPython.display import display
from functools import wraps
@wraps(function)
def wrapped_function(*args, **kwargs):
nonlocal display, wraps, varnames, lolviz
##print(f"args = {args}, of type = {type(args)}")
##print(f"kwargs = {kwargs}, of type = {type(kwargs)}")
#print(f"varnames = {varnames}")
display(lolviz.callsviz(varnames=varnames))
return function(*args, **kwargs)
return wrapped_function
return showing_recursion_with_varnames
@show_recursion()
def factorial4(n, f0=0, f1=1):
# this is automatically done: display(viz(varnames=["n"], mode="call"))
if n < 0: return f0
elif n == 0: return f1
else: return n * factorial4(n - 1, f0=f0, f1=f1)
n = 4
print(f"{n}! = {factorial4(n)}")
4! = 24
Well it works, but it's not clean.
I would very much prefer something like rcviz, or a more powerful tool like pyan or pycallgraph.
From the standard library, I just discovered the trace module, but it seems to be oriented for the command line usage, not in a notebook.
rcviz
¶!pip install git+https://github.com/DamnedFacts/rcviz
Defaulting to user installation because normal site-packages is not writeable
Collecting git+https://github.com/DamnedFacts/rcviz
Cloning https://github.com/DamnedFacts/rcviz to /tmp/pip-req-build-5jsw_yji
Requirement already satisfied: graphviz in /usr/local/lib/python3.6/dist-packages (from rcviz==0.2) (0.16)
WARNING: You are using pip version 20.3.3; however, version 21.0.1 is available.
You should consider upgrading via the '/usr/bin/python3 -m pip install --upgrade pip' command.
import rcviz
cg = rcviz.CallGraph()
@rcviz.viz(cg)
def factorial5(n):
if n < 0: return 0
elif n == 0: return 1
else: return n * factorial5(n - 1)
n = 4
print(f"{n}! = {factorial5(n)}")
cg.render()
4! = 24 callviz: Rendering in inline in Jupyter Notebook
cg = rcviz.CallGraph()
@rcviz.viz(cg)
def fibo(n):
if n <= 0: return 0
elif n == 1: return 1
else: return fibo(n-1) + fibo(n-2)
n = 4
print(f"{n}! = {fibo(n)}")
cg.render()
4! = 3 callviz: Rendering in inline in Jupyter Notebook
from functools import lru_cache as memoize
cg = rcviz.CallGraph()
@rcviz.viz(cg)
@memoize(maxsize=None)
def fibo2(n):
if n <= 0: return 0
elif n == 1: return 1
else: return fibo2(n-1) + fibo2(n-2)
n = 4
print(f"{n}! = {fibo2(n)}")
cg.render()
4! = 3 callviz: Rendering in inline in Jupyter Notebook
Just to complicate things more, let's write a "fibonnaci" like recursive function that calls three sub-terms:
cg = rcviz.CallGraph()
@rcviz.viz(cg)
def fibo_three(n):
if n <= 0: return 0
elif n == 1: return 1
elif n == 2: return 1
else: return fibo_three(n-1) + fibo_three(n-2) + fibo_three(n-3)
n = 5
print(f"{n}! = {fibo_three(n)}")
cg.render()
5! = 7 callviz: Rendering in inline in Jupyter Notebook
Using this non-memoized function leads to already 13 nodes for just $n=5$!
cg = rcviz.CallGraph()
@rcviz.viz(cg)
@memoize(maxsize=None)
def fibo_three_memo(n):
if n <= 0: return 0
elif n == 1: return 1
elif n == 2: return 1
else: return fibo_three_memo(n-1) + fibo_three_memo(n-2) + fibo_three_memo(n-3)
n = 5
print(f"{n}! = {fibo_three_memo(n)}")
cg.render()
5! = 7 callviz: Rendering in inline in Jupyter Notebook
As before, we can see that the ternary tree is reduced: there is recursion only on the most lefty branch, thanks to memoization.
rcviz
¶It's an awesome module! Thanks to @DamnedFacts/ for having his up-to-date fork.
import string
string.hexdigits
'0123456789abcdefABCDEF'
viz(string.hexdigits)
Using IPyWidgets:
from pprint import pprint
from ipywidgets import interact
Trying on all the objects used above:
objs = [
string.ascii_lowercase,
lolviz,
data,
squares,
example_matrix,
example_3D_matrix,
anakin,
anakin_rec,
leia_rec,
luke_rec,
root,
z,
]
And the different modes, except for "call" and "calls" which only work inside functions:
partial_modes = ['str', 'matrix', 'obj', 'tree', 'lol', 'list']
@interact(
obj = objs,
mode = partial_modes,
)
def showcase_unified_viz(obj, mode):
print(f"\nObject of type = {type(obj)}, for mode = {mode}")
pprint(obj)
return viz(obj, mode=mode)
return lolviz.strviz(obj)
interactive(children=(Dropdown(description='obj', options=('abcdefghijklmnopqrstuvwxyz', <module 'lolviz' from…