[Pyodide](https://pyodide.readthedocs.io/en/latest/?badge=latest) es un proyecto de código abierto salido de las tripas de Mozilla y liberado bajo la licencia Mozilla Public License 2.0.
Permite ejecutar código Python en el navegador.
Pero, ¿cómo?
Compilando CPython a WebAssembly. Pero, además de haber hecho eso con CPython también lo han hecho con toda la biblioteca estándar y con paquetes del stack científico como numpy, pandas, matplotlib, scipy, sympy, scikit-learn, statsmodels, astropy,...
¿Qué tiene que ver eso con Pybonacci?
Pues que gracias a Pyodide ahora puedes utilizar Python desde Pybonacci sin necesidad de instalar nada. Esto puede ser interesante cuando estás en el móvil, en un PC sin lo que necesitas instalado, etc. En la barra de menús de arriba de la página ahora verás una opción que se llama 'RunPy' desde la que podrás acceder a Python en tu navegador.
¿Cómo se ha metido en Pybonacci (y cómo lo puedes meter tú en tu página?
En lo que figura a continuación voy a describir una serie de cosas que he tenido que hacerle a Pyodide para que sea más usable en el navegador.
Layout de la página
El layout dispone de dos columnas, en la parte izquierda se puede ver:
- un botón para ejecutar el código (también se puede ejecutar usando "Shift+Enter", como en una celda de Jupyter Notebook).
- Un área que es donde podemos escribir código. Para escribir código se usa
[codemirror](https://codemirror.net/)para tener un editor más interesante. - Un área donde se puede ver el resultado de los
prints, etc.
En la parte derecha se ve, inicialmente, una zona en blanco pero es la zona donde se pintarán los gráficos de matplotlib si decides hacer gráficos.


El código del layout se reduce basicamente a esto:
<style>
.CodeMirror {
font-family: Arial, monospace;
font-size: 16px;
}
* {
box-sizing: border-box;
}
.ecolumn {
float: left;
width: 50%;
padding: 10px;
height: 100%; /* Should be removed. Only for demonstration */
}
/* Clear floats after the columns */
.erow:after {
content: "";
display: table;
clear: both;
}
@media screen and (max-width: 600px) {
.ecolumn {
width: 100%;
}
}
</style>
<!--HTML LAYOUT-->
<p>You can execute any Python code. Just enter something in the box below and click the button.</p>
<div class="erow" markdown='1'>
<div id="left" class="ecolumn" markdown='1'>
<button onclick='evaluatePython()'>Run</button> (or push Shift+Enter to execute the code)
<br>
<br>
<textarea id='editorarea'></textarea>
<textarea id='output' style='width: 100%;' rows='6' disabled></textarea>
</div>
<div id="right" class="ecolumn" markdown='1'>
<div id="plotarea" markdown='1'></div>
</div>
</div>
Funcionalidad de la página, librerías de terceros
Para que la página funcione hace falta cargar una serie de bibliotecas Javascript (codemirror, pyodide).
AVISO sobre privacidad: En este momento, las librerías de terceros se cargan desde un CDN (jsdeliver) por lo que además de a Pybonacci estaréis haciendo peticiones a otra página.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/lib/codemirror.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/theme/cobalt.css">
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/lib/codemirror.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/mode/python/python.js"></script>
<script type="text/javascript">
window.languagePluginUrl = 'https://cdn.jsdelivr.net/pyodide/v0.15.0/full/';
</script>
<script src="https://cdn.jsdelivr.net/pyodide/v0.15.0/full/pyodide.js"></script>
Funcionalidad de la página, javascript local para añadir funcionalidad
El resto del código que uso es un script en Javascript que personaliza un poco como se usa Pyodide desde Pybonacci. Pongo el código completo del script a continuación y lo comento posteriormente:
<script>
const editorarea = document.getElementById("editorarea");
const output = document.getElementById("output");
// Codemirror instance
var editor = CodeMirror.fromTextArea(
editorarea,
{
value: '# Start writing python code',
lineNumbers: true,
matchBrackets: true,
theme: 'cobalt',
}
);
editor.setSize('100%', 300);
// function to add output to their html textarea
function addToOutput(s) {
output.value += s + '\n';
}
// function to add a shortcut to easily run the code in the editor
// shortcut: Shift + Enter
function run_shortcut(e) {
if (e.shiftKey && e.keyCode == 13) {
e.preventDefault();
evaluatePython();
}
};
document.addEventListener('keydown', run_shortcut, false);
// Initialization of PyOdide
output.value = 'Initializing...\n';
languagePluginLoader.then(() => {
// Injection of some names like a `print` that will print in the output
// textarea. By default, PyOdide `print` prints in the brower js console...
// The default PyOdide `print is renamed to `_print`
// Injection of a context manager to plot figures on the right of the
// page. It wraps the figure and helps to plot in the correct area using
// and img html tag.
var prev = "import sys\n";
prev += "import io\n";
prev += "import base64\n";
prev += "import matplotlib.pyplot as plt\n";
prev += "import js\n";
prev += "\n";
prev += "_print = print\n";
prev += "def print(msg):\n";
prev += " output = js.document.getElementById('output')\n";
prev += " output.value += str(msg) + '\\n'";
prev += "\n";
prev += "class Plot:\n";
prev += " def __init__(self, nrows=1, ncols=1):\n";
prev += " self.nrows = nrows\n";
prev += " self.ncols = ncols\n";
prev += " def __enter__(self):\n";
prev += " fig, axes = plt.subplots(ncols=self. ncols, nrows=self.nrows)\n";
prev += " self.fig = fig\n";
prev += " self.axes = axes\n";
prev += " return self.fig, self.axes\n";
prev += " def __exit__(self, exc_type, exc_value, exc_traceback):\n";
prev += " img = io.StringIO()\n";
prev += " plt.savefig(img, format='svg')\n";
prev += " img.seek(0)\n";
prev += " imgdata = img.read()\n";
prev += " plotarea = js.document.getElementById('plotarea')\n";
prev += " imgtag = js.document.createElement('img')\n";
prev += " imgtag.setAttribute(\n";
prev += " 'src','data:image/svg+xml;base64,' + \n";
prev += " str(base64.b64encode(imgdata.encode()),\n";
prev += " 'ascii')\n";
prev += " )\n";
prev += " plotarea.appendChild(imgtag)";
pyodide.runPythonAsync(prev)
.then(output => addToOutput(output))
.catch((err) => { addToOutput(err) });
output.value += 'Ready!\n';
});
// Function used to evaluate the code in the editor
function evaluatePython() {
output.value = '';
document.getElementById("plotarea").innerHTML = "";
pyodide.runPythonAsync(editor.getValue())
.then(output => addToOutput(output))
.catch((err) => { addToOutput(err) });
}
</script>
El código anterior lo podemos desgajar en varias partes. En la primera parte accedo a varios elementos del DOM que usaré más tarde e instancio la clase CodeMirror para poder configurar un poco el editor de código que se usa. Indico que quiero que aparezca el número de línea, que el tema del editor sea cobalt (oscuro),...:
const editorarea = document.getElementById("editorarea");
const output = document.getElementById("output");
// Codemirror instance
var editor = CodeMirror.fromTextArea(
editorarea,
{
value: '# Start writing python code',
lineNumbers: true,
matchBrackets: true,
theme: 'cobalt',
}
);
editor.setSize('100%', 300);
La función addToOutput lo único que hace es añadir información en la textarea que hay debajo del editor, que es la zona donde sale la información de un print, por ejemplo:
// function to add output to their html textarea
function addToOutput(s) {
output.value += s + '\n';
}
La función run_shortcut añade la funcionalidad necesaria para que al pulsar 'Shift+Enter' se lea el código que contiene el editor y se mande a Pyodide para que lo ejecute (con ayuda de la función evaluatePython):
// function to add a shortcut to easily run the code in the editor
// shortcut: Shift + Enter
function run_shortcut(e) {
if (e.shiftKey && e.keyCode == 13) {
e.preventDefault();
evaluatePython();
}
};
document.addEventListener('keydown', run_shortcut, false);
Lo siguiente tiene un poco más de miga. En esta parte se inicializa el editor. Cada vez que se inicializa se inyecta algo de código Python que evalúa Pyodide.
// Initialization of PyOdide
output.value = 'Initializing...\n';
languagePluginLoader.then(() => {
// Injection of some names like a `print` that will print in the output
// textarea. By default, PyOdide `print` prints in the brower js console...
// The default PyOdide `print is renamed to `_print`
// Injection of a context manager to plot figures on the right of the
// page. It wraps the figure and helps to plot in the correct area using
// and img html tag.
var prev = "import sys\n";
prev += "import io\n";
prev += "import base64\n";
prev += "import matplotlib.pyplot as plt\n";
prev += "import js\n";
prev += "\n";
prev += "_print = print\n";
prev += "def print(msg):\n";
prev += " output = js.document.getElementById('output')\n";
prev += " output.value += str(msg) + '\\n'";
prev += "\n";
prev += "class Plot:\n";
prev += " def __init__(self, nrows=1, ncols=1):\n";
prev += " self.nrows = nrows\n";
prev += " self.ncols = ncols\n";
prev += " def __enter__(self):\n";
prev += " fig, axes = plt.subplots(ncols=self. ncols, nrows=self.nrows)\n";
prev += " self.fig = fig\n";
prev += " self.axes = axes\n";
prev += " return self.fig, self.axes\n";
prev += " def __exit__(self, exc_type, exc_value, exc_traceback):\n";
prev += " img = io.StringIO()\n";
prev += " plt.savefig(img, format='svg')\n";
prev += " img.seek(0)\n";
prev += " imgdata = img.read()\n";
prev += " plotarea = js.document.getElementById('plotarea')\n";
prev += " imgtag = js.document.createElement('img')\n";
prev += " imgtag.setAttribute(\n";
prev += " 'src','data:image/svg+xml;base64,' + \n";
prev += " str(base64.b64encode(imgdata.encode()),\n";
prev += " 'ascii')\n";
prev += " )\n";
prev += " plotarea.appendChild(imgtag)";
pyodide.runPythonAsync(prev)
.then(output => addToOutput(output))
.catch((err) => { addToOutput(err) });
output.value += 'Ready!\n';
});
¿Qué se inyecta y por qué?
- Por defecto, la función
printque todos conocemos de Python enPyodidese muestra en la consola Javascript del navegador. Es decir, para ver la información delprinttienes que abrir las herramientas del desarrollador de tu navegador e ir a la consola Javascript para ver lo que está mostrando Python. Como esto es un poco engorroso lo he modificado un poco para poder usar algo mínimamente más conveniente. Cuando se iniciaPyodideen Pybonacci sobreescribo la funciónprinty elprintoriginal de Python se renombra como_print. Por tanto, elprintque te encontrarás en el editor es una versión muy básica deprintque te permite mostrar cosas sencillas en la zona detextareadebajo del editor. Si quieres usar elprintcompleto de Python lo tendrás que llamar usando_printy el resultado se mostrará en la consola Javascript de tu navegador. Todo esto lo hago inyectando este código al inicio:
import sys
import io
import base64
import js
_print = print
def print(msg):
output = js.document.getElementById('output')
output.value += str(msg) + '\n'
- Por otra parte, al usar
matplotlibdesdePyodide, cuando hacemos, por ejemplo, unplt.showno sabe dónde debe dibujar las cosas. En el escritorio disponemos de una serie de backends gráficos (Qt, Tk, etc). En el navegador no mostrará nada y no se verá la figura. Para facilitar un poco el poder usarmatplotlibenPyodidedesde Pybonacci he inyectado algo más de código. El código es un context manager que envuelve un poco el resultado de la gráfica y lo mete en una etiqueta htmlimgy se añade al DOM de la página. El código es el siguiente:
import sys
import io
import base64
import matplotlib.pyplot as plt
import js
class Plot:
def __init__(self, nrows=1, ncols=1):
self.nrows = nrows
self.ncols = ncols
def __enter__(self):
fig, axes = plt.subplots(ncols=self. ncols, nrows=self.nrows)
self.fig = fig
self.axes = axes
return self.fig, self.axes
def __exit__(self, exc_type, exc_value, exc_traceback):
img = io.StringIO()
plt.savefig(img, format='svg')
img.seek(0)
imgdata = img.read()
plotarea = js.document.getElementById('plotarea')
imgtag = js.document.createElement('img')
imgtag.setAttribute(
'src','data:image/svg+xml;base64,' +
str(base64.b64encode(imgdata.encode()),
'ascii')
)
plotarea.appendChild(imgtag)
El anterior context manager me permite crear las Figures y Axes de matplotlib y que el resultado se vea posteriormente en la página. Se puede hacer así:
with Plot() as (fig, ax):
ax.plot((1,2,3))
Si quieres meter varios Axes en la misma Figure podrías hacer:
with Plot(1, 2) as (fig, axs):
axs[0].plot((1,2,3))
axs[1].plot((3,2,1))
La última parte de la inicialización incluye el ejecutar el código que he inyectado mediante Pyodide para que print, _print y el context manager Plot que he creado estén disponibles de primeras en el editor sin necesidad de que hagas nada por tu parte.
Por último, para que se ejecute el código del editor después de que pulses el botón 'Run' o de que pulses las teclas 'Shift+Enter' se usa la función evaluatePython:
// Function used to evaluate the code in the editor
function evaluatePython() {
output.value = '';
document.getElementById("plotarea").innerHTML = "";
pyodide.runPythonAsync(editor.getValue())
.then(output => addToOutput(output))
.catch((err) => { addToOutput(err) });
}
La función anterior, primero limpia la zona del output (donde se muestra el resultado de un print). Limpia también la zona donde se muestran los gráficos para que se añadan los nuevos, si los hubiera en el nuevo código que estamos evaluando, y ejecuta el código del editor.
Mejoras y errores
Si quieres ver mejoras o algo no funciona bien puedes comentarlo en este artículo o puedes ir al repositorio que he creado para esto:
Pybonacci