#!/usr/bin/env python # coding: utf-8 # # 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](https://github.com/kikocorreoso/tutormagic/) et [nbtutor](https://github.com/lgpage/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](http://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 : TODO: # # - Et avec des tableaux numpy ? Probablement !. Réponse : TODO: # - Et avec matplotlib ? Probablement pas. Réponse : TODO: # - Et avec *truc ésotérique* ? Probablement pas.. Réponse : TODO: # ### 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](https://github.com/kikocorreoso/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](https://github.com/lgpage/nbtutor) n'a pas été mis à jour depuis 2016, probablement plus très fiable ? # ### A propos de ce document # # - C'est un [notebook Jupyter](https://jupyter.org/), écrit en [Python 3](https://www.python.org/) (mais j'expérimente avec le [Kernel C](https://github.com/brendan-rius/jupyter-c-kernel) plus bas) ; # - Auteur : [Lilian Besson](https://github.com/Naereen/notebooks) ; # - Date : 20/02/2021 ; # - License : [MIT](https://lbesson.mit-license.org/). # ## 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]: get_ipython().run_cell_magic('bash', '', 'echo "Démonstration (pas exécutée)"\nexit 0 # enlevez ça pour refaire ça chez vous\n\n# tapez ça dans votre terminal\npip install tutormagic nbtutor\n# si ça marche pas, rajoutez sudo\nsudo pip install tutormagic nbtutor\n\n# il faut manuellement installer et activer nbtutor\njupyter nbextension install --overwrite --py nbtutor\njupyter nbextension enable --py nbtutor') # Et donc ce notebook peut déclarer ses dépendances : # In[2]: get_ipython().run_line_magic('load_ext', 'watermark') # In[3]: watermark -a "Lilian Besson (Naereen)" -p tutormagic,nbtutor,numpy,matplotlib # ## 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}") # ### 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](https://perso.crans.org/besson/notebooks/F%c3%a9vrier%202021%20un%20mini%20challenge%20arithm%c3%a9tico-algorithmique.html#R%C3%A9ponse-en-Python-(par-Lilian-Besson)) (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[5]: 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) # 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[6]: get_ipython().run_cell_magic('time', '', "len(list(filter(filter_annee, ( annee\n for annee in range(1994, 2077 + 1)\n # if not calendar.isleap(annee) # en fait c'est inutile\n )\n)))") # ### Avec `tutormagic` # # Je suis le tutoriel présent [sur la documentation](https://github.com/kikocorreoso/tutormagic/). # In[7]: get_ipython().run_line_magic('load_ext', 'tutormagic') # In[8]: get_ipython().run_cell_magic('tutor', '--lang python3 --height 500', '\nURL = "http://PythonTutor.com"\ntest = "Démo de tutormagic"\nexplication = f"avec un iframe vers {URL}"\nprint(test, explication)\n\nsomme = 0\nMAX_I = 10\nfor i in range(1, MAX_I):\n somme += i**3\nprint(f"Somme des {MAX_I} premiers cubes = {somme}")') # Ca marche bien pour ce petit exemple ! # Essayons plus solide : # In[9]: get_ipython().run_cell_magic('tutor', '--lang python3 --run --height 500 --curInstr 11', "\nimport calendar\n\ndef filter_annee(annee):\n return (\n set(calendar.Calendar(annee).itermonthdays2(annee, 2))\n & {(1,0), (28, 6), (29, 0)}\n ) == {(1, 0), (28, 6)}\n\nnb_bonnes_annees = len(list(filter(filter_annee, ( annee\n for annee in range(1994, 2077 + 1)\n # if not calendar.isleap(annee) # en fait c'est inutile\n )\n)))") # 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](https://jupyter-contrib-nbextensions.readthedocs.io/en/latest/nbextensions/codefolding/readme.html)), 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](https://github.com/lgpage/nbtutor). # In[1]: get_ipython().run_line_magic('reload_ext', 'nbtutor') # Ca semble bien chargé, essayons ! # In[2]: get_ipython().run_cell_magic('nbtutor', '--reset --force --debug', '\nexplication = "Démo de nbtutor, normalement sans iframe vers PythonTutor.com"\nprint(explication)\n\nsomme = 0\nMAX_I = 10\nfor i in range(1, MAX_I):\n somme += i**3\nprint(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](https://rise.readthedocs.io/en/stable/), mais ce n'est pas grave, on peut passer de l'un à l'autre. # # Je rencontre le même problème que [ce ticket](https://github.com/lgpage/nbtutor/issues/29). # Essayons de comprendre ce problème de nbtutor !! # In[14]: get_ipython().system('pip show nbtutor') # In[15]: get_ipython().system('jupyter-nbextension list 2>&1 | grep -A 2 nbtutor') # 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]: get_ipython().run_line_magic('pinfo', '%%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]: get_ipython().run_cell_magic('nbtutor', '--reset --force --debug', '\nx = sum([i**3 for i in range(1, 10)])\nprint(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](https://github.com/lgpage/nbtutor/issues/41). # Et pour l'exemple plus gros : # In[4]: get_ipython().run_cell_magic('nbtutor', '--reset --force', "\nimport calendar\n\ndef filter_annee(annee):\n return (\n set(calendar.Calendar(annee).itermonthdays2(annee, 2))\n & {(1,0), (28, 6), (29, 0)}\n ) == {(1, 0), (28, 6)}\n\nnb_bonnes_annees = len(list(filter(filter_annee, ( annee\n for annee in range(1994, 2077 + 1)\n # if not calendar.isleap(annee) # en fait c'est inutile\n )\n)))") # Ca marche ! Wooo! # --- # ## Avec numpy et matplotlib ? # In[5]: import numpy as np import matplotlib.pyplot as plt get_ipython().run_line_magic('matplotlib', 'inline') # In[7]: 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]: get_ipython().run_cell_magic('tutor', '--lang python3 --height 100', '\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\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\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()') # Ici, le problème vient de la "cell magic" `%matplotlib` en ligne 4 ! # Et si on essaie sans cette cell magic ? # In[52]: get_ipython().run_cell_magic('tutor', '--lang python3 --height 500', '\nimport numpy as np\nimport matplotlib.pyplot as plt\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)') # 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[54]: get_ipython().run_cell_magic('tutor', '--lang py3anaconda --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)') # 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](https://github.com/kikocorreoso/tutormagic/issues/10) pour suggérer le support de ce nouveau mode ! Et [cette PR](https://github.com/kikocorreoso/tutormagic/pull/11) # In[9]: get_ipython().run_cell_magic('tutor', '--lang py3anaconda --height 500', '# https://docs.scipy.org/doc/scipy/reference/tutorial/integrate.html\n\nx = 4\nprint("Hello world!" * x)\n\nimport numpy as np\n\nI = 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))\n\nprint(I)\n\nimport scipy.integrate as integrate\nimport scipy.special as special\nresult = integrate.quad(lambda x: special.jv(2.5,x), 0, 4.5)\nprint(result)\n\nI += np.sqrt(2*np.pi) * special.fresnel(3/np.sqrt(np.pi))[0]\nprint(I)\n\nprint(abs(result[0]-I))') # POUF ça marche, merci à ma [petite modification (trois lignes changées)](https://github.com/kikocorreoso/tutormagic/pull/11). # ### Une remarque en passant : avoir PythonTutor offline? semble possible ! # # Au fait, [PythonTutor n'est plus libre](https://github.com/pgbovine/OnlinePythonTutor/), mais [un fork récent existe](https://github.com/seamile/PyTutor) : 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](https://github.com/kikocorreoso/tutormagic/issues/8) assez récent, mais pas avancé (mais très détaillé), et [celui là sur un clone de PythonTutor](https://github.com/seamile/PyTutor/issues/4). # ### Avec `nbtutor` ? # In[16]: 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()') # 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[19]: get_ipython().run_cell_magic('nbtutor', '--reset --force', '\nimport numpy as np\n\nX = np.linspace(-10, 10, 400)\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)\nY = [Y1, Y2, Y3]\n\nfor i, y in enumerate(Y):\n for f in [np.min, np.max, np.mean]:\n name_f = str(f.__name__)\n 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](https://github.com/brendan-rius/jupyter-c-kernel) ; # - essayer `tutormagic` ! Je pense que ça devrait fonctionner, car c'est une *iframe* vers [PythonTutor.com, qui supporte le C](http://pythontutor.com/c.html) (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 #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)); } } } # ### 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[1]: get_ipython().run_line_magic('reload_ext', 'tutormagic') # In[3]: get_ipython().run_cell_magic('tutor', '--lang c --height 600', '\n/* Cette cellule est écrite en langage C, il ne faut pas l\'exécuter avec le kernel Python */\n/* Installez le kernel https://github.com/brendan-rius/jupyter-c-kernel */\n\n#include \n#define MIN_I 1\n#define MAX_I 10\n#define MIN_POWER 1\n#define MAX_POWER 5\n\nunsigned long power(int n, unsigned long acc, unsigned long x) {\n if (n == 0) { return acc; }\n if (n % 2 == 0) { return power(n/2, acc, x*x); }\n else { return power((n-1)/2, acc * x, x*x); }\n}\n\nint main(void) {\n for (int i = MIN_I; i <= MAX_I; i++) {\n printf("\\n- Pour i = %i :", i);\n for (int n = MIN_POWER; n <= MAX_POWER; ++n) {\n printf("\\n\\ti^%i = %li", n, power(n, 1, i));\n }\n }\n}') # **Conclusion ?** # # tutormagic marche TROP bien pour du C ! # # > Il faudrait ajouter le OCaml... Cf [discussion initiée ici](https://github.com/seamile/PyTutor/issues/6) ! # ### Avec `nbtutor` # # Il n'y a pas pour l'instant la possibilité d'utiliser nbtutor pour d'autres langages, [mais ce ticket en parle](https://github.com/lgpage/nbtutor/issues/31). # --- # ## Conclusion # # **Bilan de ces expériences :** # # - 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](https://perso.crans.org/besson/notebooks/Comparaison_de_tutormagic_et_nbtutor_pour_visualiser_du_code_Python_et_C_dans_un_notebook_Jupyter.html), comme prévu... c'est des iframe dynamiquement généré à coup de `IPython.display(...)`, donc pas moyen de les garder... peut-être en # # # **Autres questions :** # # - Est-ce que ça marche bien depuis un [slide live RISE](https://rise.readthedocs.io/) ? Je pense que oui ! # # - 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](https://github.com/Naereen/notebooks) 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 !