Utilizzo dei comandi magici di IPython Jupyter per migliorare l’esperienza del notebook |  di Stefan Krawczyk |  Febbraio 2024

 | Intelligenza-Artificiale

Un post sulla creazione di un comando IPython Jupyter Magic personalizzato

Impara ad applicare un po’ di magia ai tuoi quaderni. Immagine dell’autore utilizzando DALL-E-3. Originariamente è apparsa una versione di questo post Qui.

I notebook Jupyter sono comuni nella scienza dei dati. Consentono una combinazione di scrittura e documentazione del codice “ripetizione, valutazione, loop” (REPL) in un unico posto. Sono più comunemente usati per scopi di analisi e brainstorming, ma anche, cosa più controversa, alcuni preferiscono i notebook agli script per eseguire il codice di produzione (ma non ci concentreremo su questo qui).

Invariabilmente, il codice scritto nei notebook sarà ripetitivo in qualche modo, ad esempio impostando una connessione al database, visualizzando un output, salvando risultati, interagendo con uno strumento interno della piattaforma, ecc. È meglio archiviare questo codice come funzioni e/o moduli in renderli riutilizzabili e gestirli più facilmente.

Tuttavia, l’esperienza del notebook non è sempre migliorata quando si esegue questa operazione. Ad esempio, è comunque necessario importare e richiamare queste funzioni in tutto il notebook, il che non cambia affatto l’esperienza del notebook. Allora qual è la risposta all’esigenza di migliorare l’esperienza di sviluppo del notebook stessa? Comandi magici di IPython Jupyter.

Comandi IPython Jupyter Magic (ad esempio righe nelle celle del notebook che iniziano con % O %%) può decorare una cella o una linea del taccuino per modificarne il comportamento. Molti sono disponibili per impostazione predefinita, incluso %timeit per misurare il tempo di esecuzione della cella e %bash per eseguire comandi della shell e altri sono forniti dalle estensioni ad esempio %sql per scrivere query SQL direttamente in una cella del tuo notebook.

In questo post mostreremo come il tuo team può trasformare qualsiasi funzione di utilità in magie riutilizzabili di IPython Jupyter per una migliore esperienza con il notebook. Come esempio, useremo Hamiltonuna libreria open source che abbiamo creato, per motivare la creazione di una magia che faciliti una migliore ergonomia di sviluppo per il suo utilizzo. Non è necessario sapere cosa sia Hamilton per capire questo post.

Nota. Al giorno d’oggi, ci sono molti tipi di quaderni (Giove, VSCode, Databricksecc.), ma sono tutti basati su IPython. Pertanto, i Magics sviluppati dovrebbero essere riutilizzabili in tutti gli ambienti.

IPython Jupyter Magics (che abbrevieremo solo Magics) sono frammenti di codice che possono essere caricati dinamicamente nei tuoi notebook. Sono disponibili in due gusti, line e cell magics.

Magia della lineacome suggerisce, opera su un’unica linea. Cioè, accetta come input solo ciò che è specificato sulla stessa riga. Sono indicati da un singolo % davanti al comando.

# will only time the first line
%time print("hello")
print("world")

Magia cellularecome suggerisce, prende l’intero contenuto di una cella. Sono indicati con un doppio `%%«davanti al comando.

# will time the entire cell
%%timeit
print("hello")
print("world")

Jupyter viene fornito con diversi comandi magici integrati. Puoi considerarli come strumenti da “riga di comando” che hanno accesso all’intero contesto del notebook. Ciò consente loro di interagire con l’output del notebook (ad esempio, stampare risultati, visualizzare un PNG, eseguire il rendering HTML), ma anche modificare lo stato delle variabili esistenti e scrivere su altro codice e celle di markdown!

Questo è ottimo per lo sviluppo di strumenti interni perché può astrarre e nascondere all’utente complessità non necessarie, rendendo l’esperienza “magica”. Questo è un potente strumento per sviluppare i propri “sforzi sulla piattaforma”, in particolare per scopi MLOps e LLMOps, poiché è possibile nascondere ciò che viene integrato dalla necessità di essere esposto nel notebook. Significa quindi anche che i notebook non hanno bisogno di essere aggiornati se questo codice astratto cambia di nascosto, poiché può essere tutto nascosto in un aggiornamento della dipendenza Python.

I comandi magici hanno il potenziale per rendere il tuo flusso di lavoro più semplice e veloce. Ad esempio, se preferisci sviluppare in un notebook prima di spostare il codice in un modulo Python, ciò può comportare operazioni di taglia e incolla soggette a errori. A questo scopo, la magia %%writefile my_module.py creerà direttamente un file e vi copierà il contenuto della cella.

D’altra parte, potresti preferire lo sviluppo in my_module.py nel tuo IDE e poi caricalo su un notebook per testare le tue funzioni. Questo di solito comporta il riavvio del kernel del notebook per aggiornare le importazioni dei moduli, il che può essere noioso. In quel caso, %autoreload ricaricherà automaticamente ogni importazione di moduli prima dell’esecuzione di ogni cella, rimuovendo questo punto di attrito!

Nella posta Quanto dovrebbe essere ben strutturato il codice dei dati?si sostiene che gli sforzi di standardizzazione/centralizzazione/“piattaforma” dovrebbero cambiare in meglio la forma della curva di compromesso “muoversi rapidamente vs. costruito per durare”. Una tattica concreta per modificare questo compromesso è implementare strumenti migliori. Strumenti migliori dovrebbero rendere ciò che prima era complesso, più semplice e accessibile. Questo è esattamente ciò che puoi ottenere con i tuoi comandi Magic personalizzati, il che si traduce in meno compromessi da fare.

Per coloro che non hanno familiarità con Hamilton, segnaliamo ai lettori i numerosi articoli TDS relativi ad esso (ad es storia delle origini, Ingegneria tempestiva della produzione, Semplificazione della creazione e della manutenzione del DAG Airflow, Panda di produzione ordinata, ecc.) nonché https://www.tryhamilton.dev/.

Hamilton è uno strumento open source che abbiamo creato presso Stitch Fix nel 2019. Hamilton aiuta data scientist e ingegneri a definire flussi di dati testabili, modulari e autodocumentanti, che codificano lignaggio e metadati. Hamilton ottiene questi tratti in parte richiedendo che le funzioni Python siano curate in moduli.

Tuttavia, il tipico modello di utilizzo del notebook Jupyter porta al codice che risiede nel notebook e in nessun altro posto, rappresentando una sfida ergonomica per gli sviluppatori:

Come possiamo consentire a qualcuno di creare moduli Python in modo semplice e veloce da un notebook, migliorando allo stesso tempo l’esperienza di sviluppo?

Il ciclo dello sviluppatore Hamilton è simile al seguente:

Ciclo di sviluppo di Hamilton. Immagine dell’autore.

Prenditi un minuto per leggere questo loop. Il ciclo mostra che ogni volta che viene apportata una modifica al codice, l’utente dovrà non solo reimportare il modulo Python, ma anche ricreare l’oggetto Driver. Poiché i notebook consentono l’esecuzione delle celle in qualsiasi ordine, può diventare difficile per l’utente tenere traccia di quale versione è caricata per ciascun modulo e cosa è attualmente caricato in un driver. Questo onere ricade sull’utente e potrebbe richiedere il riavvio del kernel, che perderebbe altri calcoli (per fortuna, Hamilton può essere impostato per eseguire flussi di dati complessi e riprendere da dove si era interrotto…), il che non è proprio l’ideale.

Ecco come potremmo migliorare questo ciclo utilizzando Magics:

  1. Crea un modulo Python “temporaneo” dalle funzioni definite in una cella e importa questo nuovo modulo direttamente nel notebook.
  2. Visualizza automaticamente il grafico aciclico diretto (DAG) definito dalle funzioni per ridurre il codice boilerplate di visualizzazione.
  3. Ricostruisci tutti i driver Hamilton presenti nel notebook con moduli aggiornati, risparmiando all’utente il tempo di doversi ricordare di ricreare manualmente i driver per apportare la modifica.

Vorremmo un comando simile a questo:

%%cell_to_module -m my_module --display --rebuild-drivers

def my_func(some_input: str) -> str:
"""Some logic"""
return ...

E causa il seguente comportamento dopo aver eseguito la cella:

  • Crea un modulo con il nome my_module nel notebook.
  • Visualizza il DAG costruito dalle funzioni all’interno della cella.
  • Ricostruisci tutti i driver downstream che utilizzavano my_module in altre cellule, risparmiando all’utente di dover rieseguire quelle celle.

Come puoi vedere, questo è un comando di Magic non banale, poiché stiamo regolando l’output della cella e lo stato del notebook.

Qui spieghiamo passo dopo passo come creare un comando magico. Per evitare di mostrare solo un banale esempio di “ciao mondo”, spiegheremo come abbiamo costruito quello di Hamilton %%cell_to_module anche la magia.

Crea un nuovo modulo Python in cui scriveremo il codice magico e un notebook Jupyter per provarlo. Il nome di questo modulo (cioè il file `.py`) sarà il nome dell’estensione che dovrai caricare.

Se il notebook Jupyter è installato, hai già tutte le dipendenze Python richieste. Quindi, aggiungi le librerie di cui avrai bisogno per il tuo Magic, nel nostro caso Hamilton (`pip install sf-hamilton(visualization)`).

Per definire un semplice comando di Magic, puoi utilizzare funzioni o oggetti (vedere questi documenti). Per Magics più complessi in cui è necessario lo stato, avrai bisogno dell’approccio di classe. Utilizzeremo qui l’approccio basato sulla classe. Per iniziare dobbiamo importare moduli/funzioni IPython e quindi definire una classe che eredita magic.Magics. Ogni metodo decorato con @cell_magic O @line_magic definisce una nuova magia e la classe può ospitarne comunque molte.

Per iniziare, il tuo codice dovrebbe assomigliare a questo ad alto livello:

# my_magic.py

from 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):
...

Per la magia con stato, può essere utile aggiungere un file __init__() metodo (cioè costruttore). Non è necessario nel nostro caso.

Ereditando da `magic.Magics`, questa classe ha accesso a diversi campi importanti tra cui self.shell, che è il IPython InteractiveShell che sta alla base del taccuino. Il suo utilizzo consente di estrarre e analizzare le variabili caricate nel notebook Jupyter attivo.

Il nostro Hamilton Magic Command inizierà assomigliando a:

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):
...

Successivamente, specifichiamo quali argomenti verranno passati e come analizzarli. Per ogni argomento, aggiungi a @discussionee aggiungi a @magic_arguments() decoratore in cima. Seguono uno schema simile a argparse argomenti se li hai familiari, ma non sono così completi. All’interno della funzione, è necessario chiamare il file analizzare_argstring() funzione. Riceve la funzione stessa per leggere le istruzioni dei decoratori e `linea“(quello con % O %%) che contiene i valori degli argomenti.

Il nostro comando inizierebbe ad assomigliare a questo:

@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

Tieni presente che per gli argomenti richiesti non è disponibile alcuna funzionalità argomenti_magici() per questo, quindi è necessario verificare manualmente la correttezza nel corpo della funzione, ecc.

Continuando la nostra analisi dell’esempio Hamilton Magic, il metodo sulla classe ora assomiglia al seguente; usiamo molti argomenti facoltativi:

@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 ...

Nota, gli argomenti aggiuntivi a @discussione sono utili per quando qualcuno usa ? per interrogarsi su cosa fa la magia. Cioè ?%%cell_to_module mostrerà la documentazione.

Ora che abbiamo analizzato gli argomenti, possiamo implementare la logica del comando magico. Non ci sono vincoli particolari qui e puoi scrivere qualsiasi codice Python. Tralasciando un esempio generico (ne hai abbastanza per iniziare dal passaggio precedente), analizziamo il nostro esempio Hamilton Magic. Per questo, vogliamo utilizzare gli argomenti per determinare il comportamento desiderato per il comando:

  1. Crea il modulo Python con nome_modulo.
  2. Se — ricostruire il driverricostruire i driver, passando in verbosità.
  3. Se — config è presente, preparati.
  4. Se – Schermovisualizzare il DAG.

Vedi i commenti nel codice per le spiegazioni:

# 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()

Nota come usiamo self.shell. Ciò ci consente di aggiornare e inserire variabili nel notebook. I valori restituiti dalla funzione verranno utilizzati come “output della cella” (dove vengono visualizzati i valori stampati).

Infine, dobbiamo dire a IPython e al notebook del Magic Command. Il nostro modulo in cui è definita la nostra Magic deve avere la seguente funzione per registrare la nostra classe Magic e poter caricare la nostra estensione. Se fai qualcosa con stato, è qui che ne creerai un’istanza.

Si noti che l’argomento “ipython” qui è lo stesso InteractiveShell disponibile tramite self.shell nel metodo di classe che abbiamo definito.

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)

Vedi l’intero Qui l’Hamilton Magic Command.

Per caricare la tua magia nel taccuino, prova quanto segue:

%load_ext my_magic

nel caso del nostro Hamilton Magic lo caricheremo tramite:

%load_ext hamilton.plugins.jupyter_magic

Durante lo sviluppo, usalo per ricaricare la magia aggiornata senza dover riavviare il kernel del notebook.

%reload_ext my_magic

È quindi possibile richiamare i comandi magici definiti per riga o cella. Quindi per quello di Hamilton ora saremmo in grado di fare:

%%?cell_to_module

Ecco un esempio di utilizzo, con l’inserimento della visualizzazione:

Esempio che mostra la magia in azione.
GIF animata in cui si aggiungono funzioni e si preme Invio per aggiornare l’immagine.

In un caso d’uso nel mondo reale, molto probabilmente versionizzeresti e impacchetteresti la tua magia in una libreria, che potrai quindi gestire facilmente in ambienti Python come richiesto. Con l’Hamilton Magic Command è incluso nella libreria Hamilton, quindi per ottenerlo è sufficiente installare sf-hamilton e caricare il comando magico diventerà accessibile nel notebook.

In questo post ti abbiamo mostrato i passaggi necessari per creare e caricare il tuo IPython Jupyter Magic Command. Spero che ora tu stia pensando alle celle/attività/azioni comuni che esegui in un ambiente di notebook, che potrebbero essere migliorate/semplificate/o addirittura rimosse con l’aggiunta di un semplice Magic!

Per dimostrare un esempio di vita reale, abbiamo motivato e mostrato gli interni di un Hamilton Magic Command per mostrare un comando creato per migliorare l’esperienza dello sviluppatore all’interno di un notebook Jupyter, aumentando l’output e modificando lo stato interno.

Ci auguriamo che questo post ti aiuti a superare il problema e a creare qualcosa di più ergonomico e utile per te e l’esperienza Jupyter Notebook dei tuoi team.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *