Comparaison de tutormagic et nbtutor pour visualiser du code Python et C dans un notebook Jupyter

Je viens de découvrir ces deux projets très chousettes : tutormagic et nbtutor, tous les deux lias bres, gratuits, en Python, et proposant d'intégrer des visualisations intéractives, venant ou inspirées du merveilleux PythonTutor.com.

But : je souhaite savoir laquelle des deux extensions est la meilleure, et pour quels usages

  • Peut-on utiliser l'un des deux en mode offline (sans une iframe vers PythonTutor) ? Je pense que non pour tutormagic, je pense que oui pour nbtutor.

  • Est-ce que ça marche bien pour tous les types de Python de base : booléens/entiers/flottants, chaînes, objets, nombres complexes, tableau/list, dictionnaire/ensemble ? Probablement ! Réponse : oui !

  • Et avec des tableaux numpy ? Probablement !. Réponse : oui pour tutormagic avec ma nouvelle version qui ajoute le mode "py3anaconda" (à voir si cette PR est acceptée !)

  • Et avec matplotlib ? Probablement pas. Réponse : avec tutormagic peut-être, tant qu'on ne génère pas de figure... avec nbtutor apparemment oui.
  • Et avec truc ésotérique ? Probablement pas.. Réponse : probablement non ! Mais tutormagic en mode "py3anaconda" supporte plein de modules scientifiques puisque Anaconda 5.2 supporte plein de trucs : scipy, sympa, scikit-learn, et j'en passe !

Attention ? Projets pas récents !

Jupyter a beaucoup changé ces dernières anneés, entre 2018 et 2021..

Mais ave jupyter classique, normalement ça va les "vieilles" extensions fonctionnent encore. Jupyter Lab, c'est une autre histoire !

  • tutormagic n'a pas été mis à jour depuis 2017, probablement plus très fiable ! Et pas de documentation Sphinx sur ReadTheDocs, ça sent le projet assez expérimental. Mais c'est distribué proprement sur pip, et s'installe en une commande !
  • nbtutor n'a pas été mis à jour depuis 2016, probablement plus très fiable ?

A propos de ce document

Dépendances

Dans mon installation de Python 3, j'ai installé les deux paquets tutormagic et nbtutor

Où installer ?

  • dans l'installation globale avec sudo pip install ;
  • ou dans une installation locale dans un virtualenv ou pyenv ou autre, avec pip install (ou avec conda install mais pas testé).
In [1]:
%%bash
echo "Démonstration (pas exécutée)"
exit 0  # enlevez ça pour refaire ça chez vous

# tapez ça dans votre terminal
pip install tutormagic nbtutor
# si ça marche pas, rajoutez sudo
sudo pip install tutormagic nbtutor

# il faut manuellement installer et activer nbtutor
jupyter nbextension install --overwrite --py nbtutor
jupyter nbextension enable --py nbtutor
Démonstration (pas exécutée)

Et donc ce notebook peut déclarer ses dépendances :

In [5]:
%load_ext watermark
In [6]:
watermark -a "Lilian Besson (Naereen)" -p tutormagic,nbtutor,numpy,matplotlib
Lilian Besson (Naereen) 

tutormagic 0.3.0
nbtutor 1.0.4
numpy 1.19.2
matplotlib 3.3.2

Première comparaison

In [4]:
test = "Démo de jupyter notebook"
print(test)

somme = 0
MAX_I = 10
for i in range(1, MAX_I):
    somme += i**3
print(f"Somme des {MAX_I} premiers cubes = {somme}")
Démo de jupyter notebook
Somme des 10 premiers cubes = 2025

Un petit programme à tester avec les deux interfaces

Je vais emprunter le petit problème arithmético-algorithmique suivant, posé à mes élèves au début de février 2021 :

Mini challenge algorithmique pour les passionnés en manque de petits exercices de code : (optionnel) Vous avez dû observer que ce mois de février est spécial parce que le 1er février est un lundi, et qu'il a exactement 4 lundis, 4 mardis, 4 mercredis, 4 jeudis, 4 vendredis, 4 samedis et 4 dimanches.

Question : Comptez le nombre de mois de février répondant à ce critère (je n'ai pas trouvé de nom précis), depuis l'année de création de l'ENS Rennes (1994, enfin pour Cachan antenne Bretagne) jusqu'à 2077 (1994 et 2077 inclus).

Pour plus d'informations, cf ce notebook) (aussi sur GitHub et nbviewer).

Avec le module calendar on pourrait faire comme en Bash : imprimer les calendriers, et rechercher des chaînes particulières... mais ce n'est pas très propre. Essayons avec ce même module mais en écrivant une solution fonctionnelle !

In [7]:
import calendar

def filter_annee(annee):
    return (
        set(calendar.Calendar(annee).itermonthdays2(annee, 2))
        & {(1,0), (28, 6), (29, 0)}
    ) == {(1, 0), (28, 6)}

filter_annee(2020), filter_annee(2021), filter_annee(2022)
Out[7]:
(False, True, False)

C'est un bon exemple car il utilise des listes, des ensembles etc. Peut-être restreindre le nombre d'année considérées, pour simplifier ?

Et donc on a juste à compter les années, de 1994 à 2077 inclus, qui ne sont pas des années bissextiles et qui satisfont le filtre :

In [8]:
%%time
len(list(filter(filter_annee, ( annee
        for annee in range(1994, 2077 + 1)
        # if not calendar.isleap(annee) # en fait c'est inutile
    )
)))
CPU times: user 1.16 ms, sys: 696 µs, total: 1.85 ms
Wall time: 1.87 ms
Out[8]:
9

Avec tutormagic

Je suis le tutoriel présent sur la documentation.

In [9]:
%load_ext tutormagic
The tutormagic extension is already loaded. To reload it, use:
  %reload_ext tutormagic
In [10]:
%%tutor --lang python3 --height 500

URL = "http://PythonTutor.com"
test = "Démo de tutormagic"
explication = f"avec un iframe vers {URL}"
print(test, explication)

somme = 0
MAX_I = 10
for i in range(1, MAX_I):
    somme += i**3
print(f"Somme des {MAX_I} premiers cubes = {somme}")

Ca marche bien pour ce petit exemple !

Essayons plus solide :

In [11]:
%%tutor --lang python3 --run --height 500 --curInstr 11

import calendar

def filter_annee(annee):
    return (
        set(calendar.Calendar(annee).itermonthdays2(annee, 2))
        & {(1,0), (28, 6), (29, 0)}
    ) == {(1, 0), (28, 6)}

nb_bonnes_annees = len(list(filter(filter_annee, ( annee
        for annee in range(1994, 2077 + 1)
        # if not calendar.isleap(annee) # en fait c'est inutile
    )
)))

Whooo! Ca a pris 763 étapes, on était pas loin de la limite des 1000 étapes sur PythonTutor.com, mais ça marche !

Ca marche vraiment bien !

En repliant le code (avec une extension, Codefolding), on peut montrer juste le début (la "magic" %%tutor) et l'iframe !

Bonus : ça fonctionne si on ferme le notebook et qu'on le rouvre, sans même avoir à réexécuter la cellule ! Très pratique pour préparer son cours !

Avec nbtutor

Je suis aussi la documentation sur la page GitHub de nbtutor.

In [1]:
%reload_ext nbtutor

Ca semble bien chargé, essayons !

In [12]:
%%nbtutor --reset --force --debug

explication = "Démo de nbtutor, normalement sans iframe vers PythonTutor.com"
print(explication)

somme = 0
MAX_I = 10
for i in range(1, MAX_I):
    somme += i**3
print(f"Somme des {MAX_I} premiers cubes = {somme}")

Ca marche pas :( Apparemment ça marche pas bien si tutormagic a aussi été exécuté...

Je vois sur la documentation du projet que l'installation de l'extension ajoute une "barre d'outil de cellule", mais je ne l'ai pas... je l'ai désormais, en rechargeant le notebook !

Elle prend la place de la barre d'outil pour les slides RISE, mais ce n'est pas grave, on peut passer de l'un à l'autre.

Je rencontre le même problème que ce ticket.

Essayons de comprendre ce problème de nbtutor !!

In [14]:
!pip show nbtutor
Name: nbtutor
Version: 1.0.4
Summary: Visualize Python code execution in Jupyter Notebook cells
Home-page: https://github.com/lgpage/nbtutor
Author: Logan Page
Author-email: page.lg@gmail.com
License: BSD 3-Clause
Location: /usr/local/lib/python3.6/dist-packages
Requires: notebook
Required-by: 
In [15]:
!jupyter-nbextension list 2>&1 | grep -A 2 nbtutor
      nbtutor/js/nbtutor.min  enabled 
      - Validating: OK
    tree section

Bon, le package semble installé, et ajouté comme une extension Jupyter, qui le reconnaît et la valide...

Chargez l'extension avec %load_ext nbtutor a fonctionné... juste la magic %%nbtutor ne marche pas.

In [16]:
%%nbtutor?

Sa documentation se charge bien !

Docstring:
::

  %nbtutor [-r] [-f] [-i] [-d N] [--digits D] [--max_size S] [--step_all]
               [--expand_arrays] [--nolies] [--debug]

Mais rien ne se passe ! Même en mode --debug...

In [3]:
%%nbtutor --reset --force --debug

x = sum([i**3 for i in range(1, 10)])
print(x)

Aucun message d'erreur, rien dans la console qui a lancé Jupyter...

Je pense que l'extension est cassée avec les nouvelles variantes de Python ou de Jupyter, qui comme je le suspectais a beaucoup changé depuis 2016 (dernier commit de ce projet)...

J'ai demandé au développeur s'il maintenait encore son projet.

Et pour l'exemple plus gros :

In [13]:
%%nbtutor --reset --force

import calendar

def filter_annee(annee):
    return (
        set(calendar.Calendar(annee).itermonthdays2(annee, 2))
        & {(1,0), (28, 6), (29, 0)}
    ) == {(1, 0), (28, 6)}

nb_bonnes_annees = len(list(filter(filter_annee, ( annee
        for annee in range(1994, 2077 + 1)
        # if not calendar.isleap(annee) # en fait c'est inutile
    )
)))

Ca marche ! Wooo!


Avec numpy et matplotlib ?

In [14]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
In [16]:
X = np.linspace(-10, 10)

def f1(x): return np.cos(x**2)
def f2(x): return np.sin(x**2)
def f3(x): return f1(x)+f2(x) 
Y1 = f1(X)
Y2 = f2(X)
Y3 = f3(X)

# avec la police "Humour Sans", téléchargée depuis https://aur.archlinux.org/packages/ttf-humor-sans/
with plt.xkcd():
    plt.figure(figsize=(14,8))
    plt.plot(X, Y1, "+-r", label="$f_1(x)$", ms=10, lw=3)
    plt.plot(X, Y2, "o-b", label="$f_2(x)$", ms=10, lw=3)
    plt.plot(X, Y3, "d-y", label="$f_3(x)$", ms=10, lw=3)
    plt.legend()
    plt.xlabel("Axe $x$")
    plt.ylabel("Axe $y$")
    plt.title("Trois fonctions de la variable réelle")
    plt.show()

Avec tutormagic ?

In [51]:
%%tutor --lang python3 --height 100

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

X = np.linspace(-10, 10)

def f1(x): return np.cos(x**2)
def f2(x): return np.sin(x**2)
def f3(x): return f1(x)+f2(x) 
Y1 = f1(X)
Y2 = f2(X)
Y3 = f3(X)

with plt.xkcd():
    plt.figure(figsize=(14,8))
    plt.plot(X, Y1, "+-r", label="$f_1(x)$", ms=10, lw=3)
    plt.plot(X, Y2, "o-b", label="$f_2(x)$", ms=10, lw=3)
    plt.plot(X, Y3, "d-y", label="$f_3(x)$", ms=10, lw=3)
    plt.legend()
    plt.xlabel("Axe $x$")
    plt.ylabel("Axe $y$")
    plt.title("Trois fonctions de la variable réelle")
    plt.show()

Ici, le problème vient de la "cell magic" %matplotlib en ligne 4 !

Et si on essaie sans cette cell magic ?

In [52]:
%%tutor --lang python3 --height 500

import numpy as np
import matplotlib.pyplot as plt

X = np.linspace(-10, 10)

def f1(x): return np.cos(x**2)
def f2(x): return np.sin(x**2)
def f3(x): return f1(x)+f2(x) 
Y1 = f1(X)
Y2 = f2(X)
Y3 = f3(X)

print(Y1)
print(Y2)
print(Y3)

Evidemment, ça ne marche pas ! Numpy et matplotlib ne sont PAS supportés dans PythonTutor.com...

Seul les modules suivants sont disponibles avec le mode "Python3" basique :

Only these modules can be imported:
__future__, abc, array, bisect, calendar, cmath,
collections, copy, datetime, decimal, doctest, fractions,
functools, hashlib, heapq, io, itertools, json,
locale, math, operator, pickle, pprint, random,
re, string, time, types, typing, unittest

Ca allait être un avantage de nbtutor... au moins la partie numpy.

Bon, et bien on peut essayer "Python 3.6 with Anaconda (experimental)".

Depuis le site web, j'arrive à utiliser Python 3.6 + Anaconda 5.2 EXPERIMENTAL! pour le code suivant.

In [18]:
%%tutor --lang py3anaconda --height 500

import numpy as np

X = np.linspace(-10, 10)

def f1(x): return np.cos(x**2)
def f2(x): return np.sin(x**2)
def f3(x): return f1(x)+f2(x) 
Y1 = f1(X)
Y2 = f2(X)
Y3 = f3(X)

print(Y1)
print(Y2)
print(Y3)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-18-f2046576b7cf> in <module>
----> 1 get_ipython().run_cell_magic('tutor', '--lang py33anaconda --height 500', '\nimport numpy as np\n\nX = np.linspace(-10, 10)\n\ndef f1(x): return np.cos(x**2)\ndef f2(x): return np.sin(x**2)\ndef f3(x): return f1(x)+f2(x) \nY1 = f1(X)\nY2 = f2(X)\nY3 = f3(X)\n\nprint(Y1)\nprint(Y2)\nprint(Y3)\n')

/usr/local/lib/python3.6/dist-packages/IPython/core/interactiveshell.py in run_cell_magic(self, magic_name, line, cell)
   2369             with self.builtin_trap:
   2370                 args = (magic_arg_s, cell)
-> 2371                 result = fn(*args, **kwargs)
   2372             return result
   2373 

<decorator-gen-126> in tutor(self, line, cell, local_ns)

/usr/local/lib/python3.6/dist-packages/IPython/core/magic.py in <lambda>(f, *a, **k)
    185     # but it's overkill for just that one bit of state.
    186     def magic_deco(arg):
--> 187         call = lambda f, *a, **k: f(*a, **k)
    188 
    189         if callable(arg):

/usr/local/lib/python3.6/dist-packages/tutormagic.py in tutor(self, line, cell, local_ns)
    150                     "{} not supported. Only the following options are allowed: "
    151                     "'python2', 'python3', 'java', 'javascript', "
--> 152                     "'typescript', 'ruby', 'c', 'c++', 'py3anaconda'".format(args.lang[0]))
    153         else:
    154             lang = "python3"

ValueError: py33anaconda not supported. Only the following options are allowed: 'python2', 'python3', 'java', 'javascript', 'typescript', 'ruby', 'c', 'c++', 'py3anaconda'

Ah zut, pour l'instant la magic cell %%tutor ne connaît pas d'autres modes pour Python 3. En effet elle date de 2017, mais le nouveau mode de PythonTutor est plus récent !

J'ai ouvert ce ticket pour suggérer le support de ce nouveau mode ! Et cette PR

In [19]:
%%tutor --lang py3anaconda --height 500
# https://docs.scipy.org/doc/scipy/reference/tutorial/integrate.html

x = 4
print("Hello world!" * x)

import numpy as np

I = np.sqrt(2/np.pi)*(18.0/27*np.sqrt(2)*np.cos(4.5) - 4.0/27*np.sqrt(2)*np.sin(4.5))

print(I)

import scipy.integrate as integrate
import scipy.special as special
result = integrate.quad(lambda x: special.jv(2.5,x), 0, 4.5)
print(result)

I += np.sqrt(2*np.pi) * special.fresnel(3/np.sqrt(np.pi))[0]
print(I)

print(abs(result[0]-I))

POUF ça marche, merci à ma petite modification (trois lignes changées).

C'est quand même beau les logiciels open-source...

Une remarque en passant : avoir PythonTutor offline? semble possible !

Au fait, PythonTutor n'est plus libre, mais un fork récent existe : c'est peut-être un vol de propriété intellectuelle, mais bon, ça peut dépanner !

TODO: l'installer chez moi, et faire en sorte que tutormagic fonctionne en local aussi ! Regardez ce ticket assez récent, mais pas avancé (mais très détaillé), et celui là sur un clone de PythonTutor.

=> J'ai réussi à avoir PyTutor (v5-unity) mais juste pour Python3... pas C ou Java... et je sais pas comment faire !

Avec nbtutor ?

In [16]:
%%nbtutor --reset --force --debug

import numpy as np
import matplotlib.pyplot as plt

X = np.linspace(-10, 10, 40)

def f1(x): return np.cos(x**2)
def f2(x): return np.sin(x**2)
def f3(x): return f1(x)+f2(x) 
Y1 = f1(X)
Y2 = f2(X)
Y3 = f3(X)

with plt.xkcd():
    plt.figure(figsize=(14,8))
    plt.plot(X, Y1, "+-r", label="$f_1(x)$", ms=10, lw=3)
    #plt.plot(X, Y2, "o-b", label="$f_2(x)$", ms=10, lw=3)
    #plt.plot(X, Y3, "d-y", label="$f_3(x)$", ms=10, lw=3)
    plt.legend()
    #plt.xlabel("Axe $x$")
    #plt.ylabel("Axe $y$")
    plt.title("Trois fonctions de la variable réelle")
    plt.show()
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/debugger.py in run_cell(self, cell)
     47             with redirect_stdout(self.stdout):
---> 48                 self.run(cell, globals, locals)
     49         except:

/usr/lib/python3.6/bdb.py in run(self, cmd, globals, locals)
    433         try:
--> 434             exec(cmd, globals, locals)
    435         except BdbQuit:

<string> in <module>

/usr/local/lib/python3.6/dist-packages/matplotlib/pyplot.py in show(*args, **kwargs)
    352     _warn_if_gui_out_of_main_thread()
--> 353     return _backend_mod.show(*args, **kwargs)
    354 

/usr/local/lib/python3.6/dist-packages/ipykernel/pylab/backend_inline.py in show(close, block)
     42                 figure_manager.canvas.figure,
---> 43                 metadata=_fetch_figure_metadata(figure_manager.canvas.figure)
     44             )

/usr/local/lib/python3.6/dist-packages/IPython/core/display.py in display(include, exclude, metadata, transient, display_id, *objs, **kwargs)
    312         else:
--> 313             format_dict, md_dict = format(obj, include=include, exclude=exclude)
    314             if not format_dict:

/usr/local/lib/python3.6/dist-packages/IPython/core/formatters.py in format(self, obj, include, exclude)
    179             try:
--> 180                 data = formatter(obj)
    181             except:

<decorator-gen-9> in __call__(self, obj)

/usr/local/lib/python3.6/dist-packages/IPython/core/formatters.py in catch_format_error(method, self, *args, **kwargs)
    223     try:
--> 224         r = method(self, *args, **kwargs)
    225     except NotImplementedError:

/usr/local/lib/python3.6/dist-packages/IPython/core/formatters.py in __call__(self, obj)
    340             else:
--> 341                 return printer(obj)
    342             # Finally look for special method names

/usr/local/lib/python3.6/dist-packages/IPython/core/pylabtools.py in <lambda>(fig)
    247     if 'png' in formats:
--> 248         png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs))
    249     if 'retina' in formats or 'png2x' in formats:

/usr/local/lib/python3.6/dist-packages/IPython/core/pylabtools.py in print_figure(fig, fmt, bbox_inches, **kwargs)
    131 
--> 132     fig.canvas.print_figure(bytes_io, **kw)
    133     data = bytes_io.getvalue()

/usr/local/lib/python3.6/dist-packages/matplotlib/backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2192                     with ctx:
-> 2193                         self.figure.draw(renderer)
   2194 

/usr/local/lib/python3.6/dist-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     40 
---> 41             return draw(artist, renderer, *args, **kwargs)
     42         finally:

/usr/local/lib/python3.6/dist-packages/matplotlib/figure.py in draw(self, renderer)
   1863             mimage._draw_list_compositing_images(
-> 1864                 renderer, self, artists, self.suppressComposite)
   1865 

/usr/local/lib/python3.6/dist-packages/matplotlib/image.py in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130         for a in artists:
--> 131             a.draw(renderer)
    132     else:

/usr/local/lib/python3.6/dist-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     40 
---> 41             return draw(artist, renderer, *args, **kwargs)
     42         finally:

/usr/local/lib/python3.6/dist-packages/matplotlib/cbook/deprecation.py in wrapper(*inner_args, **inner_kwargs)
    410                 **kwargs)
--> 411         return func(*inner_args, **inner_kwargs)
    412 

/usr/local/lib/python3.6/dist-packages/matplotlib/axes/_base.py in draw(self, renderer, inframe)
   2746 
-> 2747         mimage._draw_list_compositing_images(renderer, self, artists)
   2748 

/usr/local/lib/python3.6/dist-packages/matplotlib/image.py in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130         for a in artists:
--> 131             a.draw(renderer)
    132     else:

/usr/local/lib/python3.6/dist-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     40 
---> 41             return draw(artist, renderer, *args, **kwargs)
     42         finally:

/usr/local/lib/python3.6/dist-packages/matplotlib/axis.py in draw(self, renderer, *args, **kwargs)
   1163 
-> 1164         ticks_to_draw = self._update_ticks()
   1165         ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(ticks_to_draw,

/usr/local/lib/python3.6/dist-packages/matplotlib/axis.py in _update_ticks(self)
   1022         major_labels = self.major.formatter.format_ticks(major_locs)
-> 1023         major_ticks = self.get_major_ticks(len(major_locs))
   1024         self.major.formatter.set_locs(major_locs)

/usr/local/lib/python3.6/dist-packages/matplotlib/axis.py in get_major_ticks(self, numticks)
   1381             # Update the new tick label properties from the old.
-> 1382             tick = self._get_tick(major=True)
   1383             self.majorTicks.append(tick)

/usr/local/lib/python3.6/dist-packages/matplotlib/axis.py in _get_tick(self, major)
   2012             tick_kw = self._minor_tick_kw
-> 2013         return XTick(self.axes, 0, major=major, **tick_kw)
   2014 

/usr/local/lib/python3.6/dist-packages/matplotlib/axis.py in __init__(self, *args, **kwargs)
    430             xdata=[0, 0], ydata=[0, 1],
--> 431             transform=self.axes.get_xaxis_transform(which="grid"),
    432         )

/usr/local/lib/python3.6/dist-packages/matplotlib/artist.py in set(self, **kwargs)
   1087         """A property batch setter.  Pass *kwargs* to set properties."""
-> 1088         kwargs = cbook.normalize_kwargs(kwargs, self)
   1089         move_color_to_start = False

/usr/local/lib/python3.6/dist-packages/matplotlib/cbook/deprecation.py in wrapper(*inner_args, **inner_kwargs)
    410                 **kwargs)
--> 411         return func(*inner_args, **inner_kwargs)
    412 

/usr/local/lib/python3.6/dist-packages/matplotlib/cbook/deprecation.py in wrapper(*inner_args, **inner_kwargs)
    385     def wrapper(*inner_args, **inner_kwargs):
--> 386         arguments = signature.bind(*inner_args, **inner_kwargs).arguments
    387         if is_varargs and arguments.get(name):

/usr/lib/python3.6/inspect.py in bind(*args, **kwargs)
   2996         """
-> 2997         return args[0]._bind(args[1:], kwargs)
   2998 

/usr/lib/python3.6/inspect.py in _bind(self, args, kwargs, partial)
   2934 
-> 2935                     if param.name in kwargs:
   2936                         raise TypeError(

/usr/lib/python3.6/inspect.py in _bind(self, args, kwargs, partial)
   2934 
-> 2935                     if param.name in kwargs:
   2936                         raise TypeError(

/usr/lib/python3.6/bdb.py in trace_dispatch(self, frame, event, arg)
     50         if event == 'line':
---> 51             return self.dispatch_line(frame)
     52         if event == 'call':

/usr/lib/python3.6/bdb.py in dispatch_line(self, frame)
     68         if self.stop_here(frame) or self.break_here(frame):
---> 69             self.user_line(frame)
     70             if self.quitting: raise BdbQuit

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/debugger.py in user_line(self, frame)
     62         """This function is called when we stop or break at this line."""
---> 63         self.get_stack_data(frame, None, 'step_line')
     64 

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/debugger.py in get_stack_data(self, frame, traceback, event_type)
    122             stack_data.add(frame, lineno, event_type, user_locals)
--> 123             heap_data.add(user_locals)
    124 

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/history.py in add(self, filtered_locals)
    298         for obj in filtered_locals.values():
--> 299             self._add(obj)
    300 

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/history.py in _add(self, obj, **kwargs)
    289         elif type_info[0] == 'array':
--> 290             self._add_array_object(obj, type_info, **kwargs)
    291         elif type_info[0] == 'class' or type_info[0].endswith('instance'):

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/history.py in _add_array_object(self, obj, type_info, **kwargs)
    149         if not self.options.expand_arrays and obj.ndim == 1:
--> 150             self._add_array_data_object(obj, type_info, **kwargs)
    151             self.data[-1]['type'] = '{} ({})'.format(type_name, obj.dtype)

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/history.py in _add_array_data_object(self, obj, type_info, **kwargs)
    120                 break
--> 121             data_values.append(format(val, self.options))
    122 

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/utils.py in format(obj, options)
    118         if isinstance(obj, _types):
--> 119             return fmtr(obj)
    120     try:

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/utils.py in <lambda>(x)
    114     formatters = {
--> 115         float_types: lambda x: '{:.{}g}'.format(x, options.digits),
    116     }

KeyboardInterrupt: 

During handling of the above exception, another exception occurred:

BdbQuit                                   Traceback (most recent call last)
<ipython-input-16-72b13d736f51> in <module>
----> 1 get_ipython().run_cell_magic('nbtutor', '--reset --force --debug', '\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nX = np.linspace(-10, 10, 40)\n\ndef f1(x): return np.cos(x**2)\ndef f2(x): return np.sin(x**2)\ndef f3(x): return f1(x)+f2(x) \nY1 = f1(X)\nY2 = f2(X)\nY3 = f3(X)\n\nwith plt.xkcd():\n    plt.figure(figsize=(14,8))\n    plt.plot(X, Y1, "+-r", label="$f_1(x)$", ms=10, lw=3)\n    #plt.plot(X, Y2, "o-b", label="$f_2(x)$", ms=10, lw=3)\n    #plt.plot(X, Y3, "d-y", label="$f_3(x)$", ms=10, lw=3)\n    plt.legend()\n    #plt.xlabel("Axe $x$")\n    #plt.ylabel("Axe $y$")\n    plt.title("Trois fonctions de la variable réelle")\n    plt.show()\n')

/usr/local/lib/python3.6/dist-packages/IPython/core/interactiveshell.py in run_cell_magic(self, magic_name, line, cell)
   2369             with self.builtin_trap:
   2370                 args = (magic_arg_s, cell)
-> 2371                 result = fn(*args, **kwargs)
   2372             return result
   2373 

<decorator-gen-126> in nbtutor(self, line, cell)

/usr/local/lib/python3.6/dist-packages/IPython/core/magic.py in <lambda>(f, *a, **k)
    185     # but it's overkill for just that one bit of state.
    186     def magic_deco(arg):
--> 187         call = lambda f, *a, **k: f(*a, **k)
    188 
    189         if callable(arg):

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/magic.py in nbtutor(self, line, cell)
     68 
     69         bdb = Bdb(self.shell, opts)
---> 70         bdb.run_cell(cell)
     71 
     72         self.shell.run_cell(cell)

/usr/local/lib/python3.6/dist-packages/nbtutor/ipython/debugger.py in run_cell(self, cell)
     50             self.code_error = True
     51             if self.options.debug:
---> 52                 raise BdbQuit
     53         finally:
     54             self.finalize()

BdbQuit: 

Je suspectais que ça n'allait pas marcher avec nbtutor puisque ça ne marchait pas avant !! puisque nbtutor a les mêmes limitations que tutormagic !

Mais ça marche pour du numpy pur !

Et ça marche aussi pour des petits codes utilisant Matplotlib, mais très vite mon CPU prend 100%, ça rame, je ne vais pas chercher davantage.

In [25]:
%%nbtutor --reset --force

import numpy as np

X = np.linspace(-10, 10, 400)

def f1(x): return np.cos(x**2)
def f2(x): return np.sin(x**2)
def f3(x): return f1(x)+f2(x) 
Y1 = f1(X)
Y2 = f2(X)
Y3 = f3(X)
Y = [Y1, Y2, Y3]

for i, y in enumerate(Y):
    for f in [np.min, np.max, np.mean]:
        name_f = str(f.__name__)
        print(f"{name_f} of Y{i+1}, result = {f(y)}")

Cet exemple fonctionne !


Et pour exécuter du code C ??

Je vais écrire un programme C qui fait un petit truc intéressant, et essayer les choses suivantes :

  • l'exécuter depuis ce notebook, avec le kernel jupyter-c-kernel ;
  • essayer tutormagic ! Je pense que ça devrait fonctionner, car c'est une iframe vers PythonTutor.com, qui supporte le C (version C11 ou C17) avec gcc ;
  • essayer nbtutor ! Si ça marche, c'est que ça utilise le serveur PythonTutor sans le montrer, sinon ça ne devrait pas marcher.

Exemple de programme C

In [4]:
/* Cette cellule est écrite en langage C, il ne faut pas l'exécuter avec le kernel Python */
/* Installez le kernel https://github.com/brendan-rius/jupyter-c-kernel */

#include <stdio.h>
#define MIN_I 1
#define MAX_I 10
#define MIN_POWER 1
#define MAX_POWER 5

unsigned long power(int n, unsigned long acc, unsigned long x) {
    if (n == 0) { return acc; }
    if (n % 2 == 0) { return power(n/2, acc, x*x); }
    else { return power((n-1)/2, acc * x, x*x); }
}

int main(void) {
    for (int i = MIN_I; i <= MAX_I; i++) {
        printf("\n- Pour i = %i :", i);
        for (int n = MIN_POWER; n <= MAX_POWER; ++n) {
            printf("\n\ti^%i = %li", n, power(n, 1, i));
        }
    }
}
- Pour i = 1 :
	i^1 = 1
	i^2 = 1
	i^3 = 1
	i^4 = 1
	i^5 = 1
- Pour i = 2 :
	i^1 = 2
	i^2 = 4
	i^3 = 8
	i^4 = 16
	i^5 = 32
- Pour i = 3 :
	i^1 = 3
	i^2 = 9
	i^3 = 27
	i^4 = 81
	i^5 = 243
- Pour i = 4 :
	i^1 = 4
	i^2 = 16
	i^3 = 64
	i^4 = 256
	i^5 = 1024
- Pour i = 5 :
	i^1 = 5
	i^2 = 25
	i^3 = 125
	i^4 = 625
	i^5 = 3125
- Pour i = 6 :
	i^1 = 6
	i^2 = 36
	i^3 = 216
	i^4 = 1296
	i^5 = 7776
- Pour i = 7 :
	i^1 = 7
	i^2 = 49
	i^3 = 343
	i^4 = 2401
	i^5 = 16807
- Pour i = 8 :
	i^1 = 8
	i^2 = 64
	i^3 = 512
	i^4 = 4096
	i^5 = 32768
- Pour i = 9 :
	i^1 = 9
	i^2 = 81
	i^3 = 729
	i^4 = 6561
	i^5 = 59049
- Pour i = 10 :
	i^1 = 10
	i^2 = 100
	i^3 = 1000
	i^4 = 10000
	i^5 = 100000

Avec tutormagic

Alors évidemment, la "cell magic" %%tutor ne marche pas depuis le kernel C, mais en repassant au kernel Python 3, on peut faire :

In [21]:
%reload_ext tutormagic
In [22]:
%%tutor --lang c --height 600

/* Cette cellule est écrite en langage C, il ne faut pas l'exécuter avec le kernel Python */
/* Installez le kernel https://github.com/brendan-rius/jupyter-c-kernel */

#include <stdio.h>
#define MIN_I 1
#define MAX_I 10
#define MIN_POWER 1
#define MAX_POWER 5

unsigned long power(int n, unsigned long acc, unsigned long x) {
    if (n == 0) { return acc; }
    if (n % 2 == 0) { return power(n/2, acc, x*x); }
    else { return power((n-1)/2, acc * x, x*x); }
}

int main(void) {
    for (int i = MIN_I; i <= MAX_I; i++) {
        printf("\n- Pour i = %i :", i);
        for (int n = MIN_POWER; n <= MAX_POWER; ++n) {
            printf("\n\ti^%i = %li", n, power(n, 1, i));
        }
    }
}

Conclusion ?

tutormagic marche TROP bien pour du C !

Il faudrait ajouter le OCaml... Cf discussion initiée ici !

Avec nbtutor

Il n'y a pas pour l'instant la possibilité d'utiliser nbtutor pour d'autres langages, mais ce ticket en parle.


Conclusion

Bilan de ces expériences...

Bilan de tutormagic

  • c'est trop cool ;
  • avec tutormagic, tout marche sans soucis,
  • ? et c'est des iframe donc l'export en HTML se passe bien non ça ne marche pas, comme prévu... c'est des iframe dynamiquement généré à coup de IPython.display(...), donc pas moyen de les garder... et même en "Save notebook widget state" ça ne fonctionne pas, je m'en doutais ;
  • ça supporte tous les langages supportés par http://PythonTutor.com, donc Python2, Python3, Java, JavaScript, TypeScript, Ruby, C, C++, et Python3 avec Anaconda pour des modules scientifiques (mais pas de figures Matplotlib, évidement) !
  • trivial d'ajouter un autre langage, s'il est ajouté dans PythonTutor,
  • TODO: ajouter OCaml ?
  • devrait pouvoir utiliser une version locale de PythonTutor, cf ma discussion lancée ici

Bilan de nbtutor

  • c'est aussi très cool !
  • ça supporte que Python, mais discussion lancée pour ajouter les autres langages... j'ai des doutes que ce soit aussi facile qu'avec tutormagic !
  • avantage de pouvoir voir la mémoire et le temps sépare, mais est-ce un avantage ?
  • pas de iframe donc pas possible d'utiliser les "points de débug" (breakpoint) de PythonTutor qui sont très pratiques !
  • pas facile d'utiliser une version locale de PythonTutor...
  • et l'interface demande d'utiliser une "barre d'outil de cellule" spécifique, ça me saoule, j'utilise celle de Jupyter live slides RISE par défaut, parce que présenter des notebooks dans des slides, c'est TROP COOL !

Autres questions

  • Est-ce que ça marche bien depuis un slide live RISE ? Je pense que oui ! Oui !

    • avec tutormagic, ça marche sans aucun problème ! Pour Python et C !
    • avec nbtutor, ça marche mais les contrôles ne s'affichent pas... j'ai ouvert ce ticket, à voir si le développeur du projet répondra à mes questions !
    • maaaaais j'ai trouvé une solution cet userstyle.
  • Qu'est-ce que ça donnerait une fois le notebook transformé en page web HTML statique ?

    • tutormagic avec son iframe devrait marcher, mais pas si la page est vue hors-ligne ;
    • nbtutor ne devrait pas marcher !
  • Et si je transforme en PDF, ça donne quoi ?

    • évidemment l'interactivité sera perdue ;
    • mais ce serait 😍 d'avoir une image PNG représentant l'interface... probablement pas le cas, mais on peut bidouiller si besoin (avec une capture d'écran faite à la main ?)

Plus de notebooks ? Merci d'avoir lu ! Allez voir d'autres notebooks intéressants dans ma collection qui grandit depuis 2016. Plein de notebooks en Python niveau L2/L3 mais aussi en OCaml et Python niveau agrégation, et bien plus ! En Java, Rust, Julia, Bash, GNUPlot et GNU Octave et des démonstrations d'extensions peu connues mais super intéressantes !