Los Jupyter Notebooks son algo común en la ciencia de datos. Permiten una combinación de documentación y escritura de código de “repetición, evaluación y bucle” (REPL) en un solo lugar. Se utilizan más comúnmente con fines de análisis y lluvia de ideas, pero también, de manera más polémica, algunos prefieren cuadernos a scripts para ejecutar código de producción (pero no nos centraremos en eso aquí).
Invariablemente, el código escrito en los cuadernos será repetitivo de alguna manera, como configurar una conexión a una base de datos, mostrar una salida, guardar resultados, interactuar con una herramienta interna de la plataforma, etc. Es mejor almacenar este código como funciones y/o módulos para hacerlos reutilizables y gestionarlos más fácilmente.
Sin embargo, la experiencia con el portátil no siempre mejora cuando se hace eso. Por ejemplo, aún necesita importar y llamar estas funciones en su computadora portátil, lo que no cambia mucho la experiencia de la computadora portátil. Entonces, ¿cuál es la respuesta para mejorar la propia experiencia de desarrollo de portátiles? Comandos mágicos de IPython Jupyter.
Comandos IPython Jupyter Magic (por ejemplo, líneas en celdas del cuaderno que comienzan con % o %%) puede decorar una celda o línea del cuaderno para modificar su comportamiento. Muchos están disponibles de forma predeterminada, incluidos %timeit para medir el tiempo de ejecución de la celda y %bash para ejecutar comandos de shell, y otros son proporcionados por extensiones como %sql escribir consultas SQL directamente en una celda de su cuaderno.
En esta publicación, mostraremos cómo su equipo puede convertir cualquier función de utilidad en magia IPython Jupyter reutilizable para una mejor experiencia con la computadora portátil. Como ejemplo, usaremos hamilton, una biblioteca de código abierto que creamos, para motivar la creación de una magia que facilite un mejor desarrollo y ergonomía para su uso. No necesitas saber qué es Hamilton para entender esta publicación.
Nota. Hoy en día, existen muchos tipos de cuadernos (Jupyter, Código VS, Ladrillos de datos, etc.), pero todos están construidos sobre IPython. Por lo tanto, las Magias desarrolladas deberían poder reutilizarse en todos los entornos.
IPython Jupyter Magics (que acortaremos a simplemente Magics) son fragmentos de código que se pueden cargar dinámicamente en sus cuadernos. Vienen en dos sabores, magia de línea y de célula.
Magia de linea, como sugiere, opera en una sola línea. Es decir, solo toma como entrada lo que se especifica en la misma línea. Se denotan por un solo % frente al comando.
# will only time the first line
%time print("hello")
print("world")
magia celular, como sugiere, toma todo el contenido de una celda. Se denotan con un doble `%%`frente al comando.
# will time the entire cell
%%timeit
print("hello")
print("world")
Jupyter viene con varios comandos mágicos incorporados. Puede considerarlas como herramientas de “línea de comandos” que tienen acceso a todo el contexto del cuaderno. Esto les permite interactuar con la salida del cuaderno (por ejemplo, imprimir resultados, mostrar un PNG, representar HTML), pero también modificar el estado de las variables existentes y escribir en otro código y celdas de rebajas.
Esto es excelente para desarrollar herramientas internas porque puede abstraer y ocultar al usuario complejidades innecesarias, haciendo que la experiencia sea “mágica”. Esta es una herramienta poderosa para desarrollar sus propios “esfuerzos de plataforma”, especialmente para propósitos de MLOps y LLMOps, ya que puede ocultar lo que se está integrando para que no tenga que exponerse en el cuaderno. Por lo tanto, también significa que no es necesario actualizar los cuadernos si este código abstracto cambia bajo el capó, ya que todo puede ocultarse en una actualización de dependencia de Python.
Los comandos mágicos tienen el potencial de hacer que su flujo de trabajo sea más sencillo y rápido. Por ejemplo, si prefiere desarrollar en un cuaderno antes de mover su código a un módulo de Python, esto puede implicar cortar y pegar propenso a errores. Para ello, la magia %%writefile my_module.py creará directamente un archivo y copiará el contenido de su celda en él.
Por el contrario, es posible que prefieras desarrollar en my_module.py en su IDE y luego cárguelo en una computadora portátil para probar sus funciones. Por lo general, esto implica reiniciar el kernel del portátil para actualizar las importaciones del módulo, lo que puede resultar tedioso. En ese caso, %autoreload recargará automáticamente cada módulo importado antes de la ejecución de cada celda, ¡eliminando este punto de fricción!
En el post ¿Qué tan bien estructurado debe estar su código de datos?, se argumenta que los esfuerzos de estandarización/centralización/“plataforma” deberían cambiar para mejor la curva de equilibrio entre “moverse rápido versus construir para durar”. Una táctica concreta para cambiar esta compensación es implementar mejores herramientas. Mejores herramientas deberían hacer que lo que solía ser complejo, sea más simple y accesible. Que es exactamente lo que puedes lograr con tus propios comandos personalizados de Magic, lo que se traduce en menos concesiones.
Para aquellos que no están familiarizados con Hamilton, recomendamos a los lectores que consulten los numerosos artículos de TDS que lo contienen (p. ej. historia de origen, Ingeniería rápida de producción., Simplificando la creación y el mantenimiento de DAG de flujo de aire, Pandas de producción ordenada, etc.), así como https://www.tryhamilton.dev/.
hamilton es una herramienta de código abierto que creamos en Stitch Fix en 2019. Hamilton ayuda a los científicos e ingenieros de datos a definir flujos de datos comprobables, modulares y autodocumentados que codifican linaje y metadatos. Hamilton logra estos rasgos en parte al requerir que las funciones de Python se organicen en módulos.
No obstante, el patrón típico de uso de la computadora portátil Jupyter lleva a que el código resida en la computadora portátil y en ningún otro lugar, lo que plantea un desafío de ergonomía para el desarrollador:
¿Cómo podemos permitir que alguien cree módulos de Python fácil y rápidamente desde una computadora portátil y, al mismo tiempo, mejorar la experiencia de desarrollo?
El bucle de desarrollador de Hamilton tiene el siguiente aspecto:
Tómate un minuto para leer este bucle. El bucle muestra que cada vez que se realiza un cambio de código, el usuario no solo deberá volver a importar el módulo de Python, sino también volver a crear el objeto Driver. Dado que los cuadernos permiten la ejecución de celdas en cualquier orden, puede resultar difícil para el usuario rastrear qué versión está cargada para cada módulo y qué está cargado actualmente en un controlador. Esta carga recae sobre el usuario y podría requerir reiniciar el kernel, lo que perdería otros cálculos (afortunadamente, Hamilton se puede configurar para ejecutar flujos de datos complejos y continuar donde lo dejó…), lo cual no es ideal.
Así es como podríamos mejorar este bucle usando Magics:
- Cree un módulo Python “temporal” a partir de las funciones definidas en una celda e importe este nuevo módulo directamente en el cuaderno.
- Visualice automáticamente el gráfico acíclico dirigido (DAG) definido por las funciones para reducir el código repetitivo de visualización.
- Reconstruya todos los controladores Hamilton que se encuentran en el cuaderno con módulos actualizados, lo que le ahorrará tiempo al usuario al tener que recordar recrear manualmente los controladores para realizar el cambio.
Nos gustaría un comando que se vea así:
%%cell_to_module -m my_module --display --rebuild-driversdef my_func(some_input: str) -> str:
"""Some logic"""
return ...
Y provoca el siguiente comportamiento después de ejecutar la celda:
- Cree un módulo con el nombre my_module en el cuaderno.
- Muestra el DAG construido por las funciones dentro de la celda.
- Reconstruya los controladores posteriores que usaron my_module en otras células, evitando que el usuario tenga que volver a ejecutar esas celdas.
Como puede ver, este es un comando Magic no trivial, ya que estamos ajustando la salida de la celda y el estado del portátil.
Aquí explicamos paso a paso cómo crear un Comando Mágico. Para evitar mostrar sólo un ejemplo trivial de “hola mundo”, explicaremos cómo construimos el modelo de Hamilton. %%cell_to_module magia también.
Crea un nuevo módulo de Python donde escribiremos el código mágico y un cuaderno jupyter para probarlo. El nombre de este módulo (es decir, archivo `.py`) será el nombre de la extensión que necesitará cargar.
Si Jupyter Notebook está instalado, ya tiene todas las dependencias de Python necesarias. Luego, agregue las bibliotecas que necesitará para su Magic, en nuestro caso Hamilton (`pip install sf-hamilton[visualization]`).
Para definir un comando Magic simple, puedes usar funciones u objetos (ver estos documentos). Para Magias más complejas donde se necesita estado, necesitarás el enfoque de clase. Usaremos el enfoque basado en clases aquí. Para comenzar necesitamos importar módulos/funciones de IPython y luego definir una clase que herede magic.Magics. Cada método decorado con @cell_magic o @line_magic define una nueva magia, y la clase puede albergar muchos de ellos.
Para empezar, tu código debería verse así en un nivel alto:
# my_magic.pyfrom IPython.core import magic
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
@magic.magics_class
class MyMagic(magic.Magics):
"""Custom class you write"""
@magic_arguments() # needs to be on top to enable parsing
@argument(...)
@magic.cell_magic
def a_cell_magic_command(self, line, cell):
...
@magic_arguments() # needs to be on top to enable parsing
@argument(...)
@magic.line_magic
def a_line_magic_command(self, line):
...
Para magia con estado, puede ser útil agregar un __init__() método (es decir, constructor). En nuestro caso no es necesario.
Al heredar de `magic.Magics`, esta clase tiene acceso a varios campos importantes, incluido self.shell, que es el Shell interactivo de IPython que subyace al cuaderno. Su uso le permite extraer e introspeccionar las variables cargadas en el cuaderno activo de Jupyter.
Nuestro Hamilton Magic Command comenzará con este aspecto:
from IPython.core import magic
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring@magic.magics_class
class HamiltonMagics(magic.Magics):
"""Magics to facilitate Hamilton development in Jupyter notebooks"""
@magic_arguments() # needed on top to enable parsing
@arguments(...)
@magics.cell_magic
def cell_to_module(self, line, cell):
...
A continuación, especificamos qué argumentos se pasarán y cómo analizarlos. Para cada argumento, agregue un @argumentoy agrega un @argumentos_magicos() decorador en la parte superior. Siguen un patrón similar al analizar argumentos argumentos si está familiarizado, pero no tienen tantas funciones. Dentro de la función, necesitas llamar al parse_argstring() función. Recibe la función misma para leer las instrucciones de los decoradores y `línea` (el que tiene % o %%) que contiene los valores de los argumentos.
Nuestro comando comenzaría a verse así:
@magic_arguments() # needs to be on top to enable parsing
# flag, long form, default value, help string.
@argument("-a", "--argument", default="some_value", help="Some optional line argument")
@magic.cell_magic
def a_cell_magic_command(self, line, cell):
args = parse_argstring(self.a_cell_magic_command, line)
if args.argument:
# do stuff -- place your utility functions here
Tenga en cuenta que, para los argumentos obligatorios, no existe ninguna posibilidad en argumentos_magicos() para eso, debe verificar manualmente la corrección del cuerpo de la función, etc.
Continuando con nuestra disección del ejemplo de Hamilton Magic, el método de la clase ahora se parece al siguiente; Usamos muchos argumentos opcionales:
@magic_arguments() # needed on top to enable parsing
@argument(
"-m", "--module_name", help="Module name to provide. Default is jupyter_module."
) # keyword / optional arg
@argument(
"-c", "--config", help="JSON config, or variable name containing config to use."
) # keyword / optional arg
@argument(
"-r", "--rebuild-drivers", action="store_true", help="Flag to rebuild drivers"
) # Flag / optional arg
@argument(
"-d", "--display", action="store_true", help="Flag to visualize dataflow."
) # Flag / optional arg
@argument(
"-v", "--verbosity", type=int, default=1, help="0 to hide. 1 is normal, default"
) # keyword / optional arg
@magics.cell_magic
def cell_to_module(self, line, cell):
"""Execute the cell and dynamically create a Python module from its content.A Hamilton Driver is automatically instantiated with that module for variable `{MODULE_NAME}_dr`.
> %%cell_to_module -m MODULE_NAME --display --rebuild-drivers
Type in ?%%cell_to_module to see the arugments to this magic.
"""
# specify how to parse by passing
args = parse_argstring(self.cell_to_module, line)
# now use args for logic ...
Tenga en cuenta que los argumentos adicionales para @argumento son útiles para cuando alguien usa ? para preguntar qué hace la magia. Es decir ?%%cell_to_module Mostrará documentación.
Ahora que hemos analizado los argumentos, podemos implementar la lógica del comando mágico. Aquí no hay restricciones particulares y puedes escribir cualquier código Python. Saltándonos un ejemplo genérico (tienes suficiente para comenzar desde el paso anterior), profundicemos en nuestro ejemplo de Hamilton Magic. Para ello, queremos utilizar los argumentos para determinar el comportamiento deseado para el comando:
- Crea el módulo Python con Nombre del módulo.
- Si — controlador de reconstrucciónreconstruir los controladores, pasando en detalle.
- Si — configuración está presente, prepárelo.
- Si – mostrarmuestra el DAG.
Vea los comentarios en el código para obtener explicaciones:
# we're in the bowels of def cell_to_module(self, line, cell):
# and we remove an indentation for readability
...
# specify how to parse by passing this method to the function
args = parse_argstring(self.cell_to_module, line)
# we set a default value, else use the passed in value
# for the module name.
if args.module_name is None:
module_name = "jupyter_module"
else:
module_name = args.module_name
# we determine whether the configuration is a variable
# in the notebook environment
# or if it's a JSON string that needs to be parsed.
display_config = {}
if args.config:
if args.config in self.shell.user_ns:
display_config = self.shell.user_ns[args.config]
else:
if args.config.startswith("'") or args.config.startswith('"'):
# strip quotes if present
args.config = args.config[1:-1]
try:
display_config = json.loads(args.config)
except json.JSONDecodeError:
print("Failed to parse config as JSON. "
"Please ensure it's a valid JSON string:")
print(args.config)
# we create the python module (using a custom function)
module_object = create_module(cell, module_name)
# shell.push() assign a variable in the notebook.
# The dictionary keys are the variable name
self.shell.push({module_name: module_object})
# Note: self.shell.user_ns is a dict of all variables in the notebook
# -- we pass that down via self.shell.
if args.rebuild_drivers:
# rebuild drivers that use this module (custom function)
rebuilt_drivers = rebuild_drivers(
self.shell, module_name, module_object,
verbosity=args.verbosity
)
self.shell.user_ns.update(rebuilt_drivers)
# create a driver to display things for every cell with %%cell_to_module
dr = (
driver.Builder()
.with_modules(module_object)
.with_config(display_config)
.build()
)
self.shell.push({f"{module_name}_dr": dr})
if args.display:
# return will go to the output cell.
# To display multiple elements, use IPython.display.display(
# print("hello"), dr.display_all_functions(), ... )
return dr.display_all_functions()
Observe cómo usamos self.shell. Esto nos permite actualizar e inyectar variables en el cuaderno. Los valores devueltos por la función se utilizarán como “salida de celda” (donde verá los valores impresos).
Por último, debemos informarle a IPython y al portátil sobre el Magic Command. Nuestro módulo donde está definido nuestro Magic debe tener la siguiente función para registrar nuestra clase Magic, y poder cargar nuestra extensión. Si hace algo con estado, aquí es donde lo creará.
Observe que el argumento `ipython` aquí es el mismo InteractiveShell disponible a través de self.shell en el método de clase que definimos.
def load_ipython_extension(ipython: InteractiveShell):
"""
Any module file that define a function named `load_ipython_extension`
can be loaded via `%load_ext module.path` or be configured to be
autoloaded by IPython at startup time.
"""
ipython.register_magics(MyMagic)
ipython.register_magics(HamiltonMagics)
Ver el completo Comando mágico de Hamilton aquí.
Para cargar tu magia en el cuaderno, prueba lo siguiente:
%load_ext my_magic
en el caso de nuestro Hamilton Magic lo cargaríamos vía:
%load_ext hamilton.plugins.jupyter_magic
Mientras desarrolla, use esto para recargar su magia actualizada sin tener que reiniciar el kernel del portátil.
%reload_ext my_magic
Luego puede invocar los comandos mágicos definidos por línea o celda. Entonces, para el de Hamilton ahora podríamos hacer:
%%?cell_to_module
Aquí hay un ejemplo de uso, inyectando la visualización:
En un caso de uso del mundo real, lo más probable es que versiones y empaquetes tu magia en una biblioteca, que luego puedas administrar fácilmente en entornos Python según sea necesario. Con Hamilton Magic Command, está empaquetado en la biblioteca de Hamilton y, por lo tanto, para obtenerlo, solo es necesario instalar sf-hamilton y cargar el comando magic será accesible en el portátil.
En esta publicación, le mostramos los pasos necesarios para crear y cargar su propio IPython Jupyter Magic Command. Con suerte, ahora estás pensando en las celdas/tareas/acciones comunes que realizas en una configuración de notebook, que podrían mejorarse/simplificarse o incluso eliminarse con la adición de un simple Magic.
Para demostrar un ejemplo de la vida real, motivamos y mostramos las partes internas de un Hamilton Magic Command para mostrar un comando que fue creado para mejorar la experiencia del desarrollador dentro de una computadora portátil Jupyter, aumentando la salida y cambiando el estado interno.
Esperamos que esta publicación le ayude a superar el obstáculo y crear algo más ergonómico y útil para usted y la experiencia Jupyter Notebook de sus equipos.