Questo capitolo discute Cython, che `e un linguaggio compilato basato su Python. Il pi`u grande vantaggio che ha su Python `e che il codice pu`o essere molto pi`u veloce (a volte di ordini di grandezza) e pu`o chiamare direttamente codice C e C++. Poich`e Cython `e essenzialmente un ampliamento del linguaggio Python, spesso non si fa distinzione fra codice Cython e Python in Sage (ad esempio si parla della “libreria Python di Sage” e delle “convenzioni di codifica in Python”).
Python `e un linguaggio interpretato e non dichiara tipi di dato per le variabili. Queste caratteristiche rendono facile scrivere programmi e farne il debug, ma il codice Python a volte pu`o essere lento. Il codice Cython pu`o assomigliare molto a Python, ma viene tradotto in codice C (spesso molto efficiente) e poi compilato. Quindo offre un linguaggio familiare agli sviluppatori Python, ma con un potenziale di una molto maggiore velocit`a. Inoltre Cython permette agli sviluppatori Sage di interfacciarsi con C e C++ molto pi`u facilmente che usando la C API di Python direttamente.
Cython `e una versione compilata di Python. In origine era basata su Pyrex ma `e stato modificato in base a ci`o di cui avevano bisogno gli sviluppatori di Sage; Cython `e stato sviluppato di concerto con Sage. Comunque ora `e un progetto independente, che `e utilizzato al di l`a dell’ambito di Sage. Come tale, `e un linguaggio giovane, ma in crescita, con documentazione agli inizi, ma in crescita. Vedi la sua pagina web, http://www.cython.org/, per le informazioni pi`u aggiornate o vai a Basi del linguaggio per iniziare subito.
Ci sono molti modi di creare e compilare codice Cython in Sage.
Nel Notebook Sage, inizia ogni cella con %cython. Quando valuti tale cella,
Viene savata in un file.
Cython `e eseguito su questo, linkato a tutte le librerie standard di Sage se necessario.
Il file di libreria condivisa resultante (.so / .dll / .dylib) `e poi caricato nell’istanza in esecuzione di Sage.
La funzionalit`a definita in tale cella pu`o ora essere utilizzata nel notebook. Inoltre la cella di output ha un link al programma C che `e stato compilato per creare il file .so.
Una funzione cpdef o def, diciamo testfunction, definita in una cella %cython in un worksheet pu`o essere importata e resa disponibile in una cella %cython differente nello stesso worksheet importandola come mostrato qui sotto:
%cython
from __main__ import testfunction
Crea un file .spyx ed attaccalo o caricalo dalla riga di comando. Questo `e simile a creare una cella %cython nel notebook ma funziona in modo completo dalla riga di comando (e non dal notebook).
Crea un file .pyx e aggiungilo alla libreria Sage.
Ad esempio per compilare SAGE_ROOT/src/sage/graphs/chrompoly.pyx vediamo le righe seguenti in module_list.py:
Extension('sage.graphs.chrompoly',
sources = ['sage/graphs/chrompoly.pyx'],
libraries = ['gmp']),
Se il codice Cython `e attaccato o caricato come un file .spyx o caricato dal notebook come un blocco %cython, le seguenti pragma sono disponibili:
Ad esempio:
#clang C++
#clib givaro
#cinclude /usr/local/include/
#cargs -ggdb
#cfile foo.c
Il modo pi`u facile di provare Cython senza dover imparare nulla su distutils, ecc., `e creare un file con l’estensione spyx, che sta per “Sage Pyrex”:
Crea un file power2.spyx.
Mettici dentro quanto segue:
def is2pow(n):
while n != 0 and n%2 == 0:
n = n >> 1
return n == 1
Lancia la riga comandi di Sage e carica il file spyx (questo dar`a errore se non hai gi`a un compilatore C installato).
sage: load("power2.spyx")
Compiling power2.spyx...
sage: is2pow(12)
False
Nota che se cambi power2.spyx e poi lo ricarichi, sar`a ricompilato al volo. Puoi anche attaccare power2.spyx cos`i che venga ricaricatoogni volta che fai qualche cambiamento:
sage: attach("power2.spyx")
Cython `e usato per la sua velocit`a. Ecco un test cronometrato su un Opteron da 2.6 GHz:
sage: %time [n for n in range(10^5) if is2pow(n)]
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]
CPU times: user 0.60 s, sys: 0.00 s, total: 0.60 s
Wall time: 0.60 s
Ora, il codice nel file power2.spyx `e Python valido, e se lo copiamo in in file powerslow.py e lo carichiamo, otteniamo quanto segue:
sage: load("powerslow.py")
sage: %time [n for n in range(10^5) if is2pow(n)]
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]
CPU times: user 1.01 s, sys: 0.04 s, total: 1.05 s
Wall time: 1.05 s
Tra l’altro, possiamo guadagnare ancora un po` di velocit`a nella versione Cython una dichiarazione di tipo, cambiando def is2pow(n): in def is2pow(unsigned int n):.
Quando si scrive codice Cython per Sage, bisogna avere un’attenzione speciale ad assicurarsi che il codice possa essere interrotto con CTRL-C. Poich`e Cython `e ottimizzato per la velocit`a, Cython di solito non controlla gli interrupt. Ad esempio codice come il seguente non pu`o essere interrotto:
sage: cython('while True: pass') # DON'T DO THIS
Mentre questo `e in esecuzione, premere CTRL-C non ha effetti. Il solo modo di uscirne `e terminare il processo di Sage. Su certi sistemi puoi ancora terminare Sage con CTRL-\ (manda un segnale Quit) invece di CTRL-C.
Sage fornisce 2 meccanismi collegati per gestire gli interrupts:
Le funzioni sig_check(), sig_on() e sig_off() possono essere messe in qualunque tipo di funzione Cython: def, cdef o cpdef. Non puoi metterle in codice Python puro (i file con estensione .py). Queste funzioni sono specifiche di Sage. Per usarle, devi includere quanto segue nel tuo file .pyx (non `e sufficiente farlo in un file .pxd):
include "sage/ext/interrupt.pxi"
Note
Le funzioni Cython cdef o cpdef con un tipo di ritorno (come cdef int myfunc():) devono avere un except value per propagare le eccezioni. Ricordati di questo ogni volta che scrivi sig_check() o sig_on() dentro ad una funzione di questo tipo, altrimenti vedrai un messaggio Exception KeyboardInterrupt: KeyboardInterrupt() in <function name> ignored.
sig_check() pu`o essere usato per valutare se ci sono degli interrupt in corso. Se un interrupt accade durante l’esecuzione di codice C o Cython, verr`a catturato dalla successiva sig_check() o sig_on() o anche dalla successiva istruzione Python. Con quest’ultima intendiamo che anche certe istruzioni Python valutano gli interrupt, ad esempio l’istruzione print. Il seguente ciclo pu`o essere interrotto:
sage: cython('while True: print "Hello"')
Il tipico caso d’uso per sig_check() `e dentro piccoli cicli che fanno cose complicate (codice Python e Cython mescolati, che possono sollevare eccezioni). `E ragionevolmente sicuro da usare e da grande controllo, perch`e nel tuo codice Cython un KeyboardInterrupt pu`o solo essere sollevato durante sig_check():
def sig_check_example():
for x in foo:
# (one loop iteration which does not take a long time)
sig_check()
Questo KeyboardInterrupt `e trattato come ogni altra eccezione Python e pu`o essere gestita come al solito:
def catch_interrupts():
try:
while some_condition():
sig_check()
do_something()
except KeyboardInterrupt:
# (handle interrupt)
Naturalmente puoi anche mettere la try/except nel ciclo, nell’esempio sopra.
La funzione sig_check() `e una funzione inline molto veloce che non dovrebbe avere effetti misurabile sulle performance.
Un altro meccanismo per la gestione degli interrupt `e la coppia di funzioni sig_on() e sig_off(). `E pi`u potente di sig_check() ma anche molto pi`u pericoloso. Dovresti mettere sig_on() prima e sig_off() dopo qualunque codice Cython che pu`o impiegare molto tempo. Questi 2 devono sempre essere richiamati in coppia, cio`e ogni sig_on() deve corrispondere ad una sig_off() di chiusura.
In pratica la tua funzione probabilmente sar`a simile a:
def sig_example():
# (some harmless initialization)
sig_on()
# (a long computation here, potentially calling a C library)
sig_off()
# (some harmless post-processing)
return something
`E possibile mettere sig_on() e sig_off() in funzioni differenti, purch`e sig_off() sia chiamata prima che la funzione che chiama la sig_on() termini l’esecuzione. Il codice seguente non `e valido:
# INVALID code because we return from function foo()
# without calling sig_off() first.
cdef foo():
sig_on()
def f1():
foo()
sig_off()
Ma il seguente `e valido poich`e non si pu`o chiamare foo interattivamente:
cdef int foo():
sig_off()
return 2+2
def f1():
sig_on()
return foo()
Per chiarezza, comunque, `e meglio evitare tutto ci`o. Un buon esempio dove quanto sopra ha senso `e la funzione new_gen() in La libreria di interfaccia C di PARI.
Un errore comune `e mettere sig_off() verso la fine della funzione (prima della return) quando la funzione ha pi`u di una istruzione return. Pertanto accertati che ci sia una sig_off() davanti ad ogni return (ed anche davanti ad ogni raise).
Warning
Il codice in sig_on() dev’essere C puro o codice Cython. Se chiami del codice Python o manipoli un oggetto Python (anche qualcosa di semplice come x = []), un interrupt pu`o pasticciare lo stato interno di Python. Nel dubbio prova ad usare sig_check() invece.
Anche, quando un interrupt capita dentro sig_on(), l’esecuzione del codice viene fermata immediatamente senza fare pulizie. Ad esempio qualunque memoria allocata dentro sig_on() viene perduta. Vedi Funzioni avanzate per dei modi di gestire questo.
Quando l’utente preme CTRL-C dentro sig_on(), l’esecuzione salter`a indietro a sig_on() (la prima che c’`e nello stack) e sig_on() sollever`a KeyboardInterrupt. Come con sig_check(), questa eccezione pu`o essere gestita nel solito modo:
def catch_interrupts():
try:
sig_on() # This must be INSIDE the try
# (some long computation)
sig_off()
except KeyboardInterrupt:
# (handle interrupt)
Certe librerie C in Sage sono scritte in modo da sollevare eccezioni Python: libGAP ed NTL possono sollevare RuntimeError e PARI PariError. Queste eccezioni si comportano esattamente come KeyboardInterrupt nell’esempio sopra e possono essere raccolte mettendo sig_on() dentro un blocco try/except. Vedi Gestione degli errori nelle librerie C per come ci`o `e implementato.
`E possibile accumulare sig_on() e sig_off(). Se lo fai, l’effetto `e esattamente lo stesso che se ci fosse solo lo sig_on()/sig_off() pi`u esterno. L’interno cambier`a semplicemente un contatore di referenze e nient’altro. Assicurati che il numero di chiamate sig_on() eguagli il numero di chiamate sig_off():
def f1():
sig_on()
x = f2()
sig_off()
def f2():
sig_on()
# ...
sig_off()
return ans
Attenzione aggiuntiva va fatta con eccezioni sollevate dentro sig_on(). Il problema `e che, se non fai niente di speciale, la sig_off() non sar`a mai invocata se c’`e un’eccezione. Se devi tu stesso sollevare un’eccezione, chiama una sig_off() prima:
def raising_an_exception():
sig_on()
# (some long computation)
if (something_failed):
sig_off()
raise RuntimeError("something failed")
# (some more computation)
sig_off()
return something
In alternativa puoi usare try/finally che catturer`a ugualmente eccezioni sollevate da subroutine dentro la try:
def try_finally_example():
sig_on() # This must be OUTSIDE the try
try:
# (some long computation, potentially raising exceptions)
return something
finally:
sig_off()
Se vuoi catturare anche quest’eccezione, hai bisogno di una try annidata:
def try_finally_and_catch_example():
try:
sig_on()
try:
# (some long computation, potentially raising exceptions)
finally:
sig_off()
except Exception:
print "Trouble!Trouble!"
sig_on() `e implementata usando la chiamata di libreria C setjmp() che richiede una piccola ma non trascurabile quantit`a di tempo. In codice veramente time-critical, si possono richiamare sig_on() e sig_off() in modo condizionale:
def conditional_sig_on_example(long n):
if n > 100:
sig_on()
# (do something depending on n)
if n > 100:
sig_off()
Ci`o dovrebbe essere necessario solo se sia la verifica (n > 100 nell’esempio) che il codice dentro il blocco sig_on() richiedono molto poco tempo. Nelle versioni di Sage anteriori alla 4.7, sig_on() era molto pi`u lento, ecco perch`e ci sono pi`u verifiche come questa nel vecchio codice.
A parte la gestione degli interrupt, la sig_on() fornisce una gestione pi`u generale dei segnali. Ad esempio gestisce alarm() time-out sollevando un’eccezione AlarmInterrupt (ereditata da KeyboardInterrupt).
Se il codice dentro sig_on() genera un segmentation fault o chiama la funzione C abort() (o pi`u in generale solleva una qualunque fra SIGSEGV, SIGILL, SIGABRT, SIGFPE, SIGBUS), questa `e catturata dal framework di interrupt ed un’eccezione `e sollevata (RuntimeError per SIGABRT, FloatingPointError per SIGFPE e l’eccezione personalizzata SignalError, basata su BaseException, altrimenti):
cdef extern from 'stdlib.h':
void abort()
def abort_example():
sig_on()
abort()
sig_off()
sage: abort_example()
Traceback (most recent call last):
...
RuntimeError: Aborted
Questa eccezione pu`o essere gestita da un blocco try/except come spiegato sopra. Un segmentation fault o abort() non controllati da sig_on() possono semplicemente terminare Sage. Questo si applica solo a sig_on(), la funzione sig_check() si occupa solo di interrupt ed allarmi.
Invece di sig_on(), c’`e anche una funzione sig_str(s), che prende una stringa C s come argomento. Si comporta nello stesso mdod di sig_on(), eccetto che la stringa s sar`a utilizzata come stringa per l’eccezione. sig_str(s) deve ancora essere chiusa da sig_off(). Esempio di codice Cython:
cdef extern from 'stdlib.h':
void abort()
def abort_example_with_sig_str():
sig_str("custom error message")
abort()
sig_off()
Eseguire ci`o produce:
sage: abort_example_with_sig_str()
Traceback (most recent call last):
...
RuntimeError: custom error message
Riguardo agli interrupt ordinari (cio`e SIGINT), sig_str(s) si comporta nello stesso modo di sig_on(): `e sollevato un semplice KeyboardInterrupt.
Alcune librerie C possono produrre errori ed usare qualche sorta di meccanismo di callback per segnalare errori: una funzione esterna di gestione degli errori va messa s`u, che sar`a chiamata dalla libreria C se capita un errore.
La funzione sig_error() pu`o essere usata per gestire questi errori. Questa funzione pu`o solo esserechiamata dentro un blocco sig_on() (altrimenti Sage andr`a in crash malamente) dopo aver sollevato un’eccezione Python. Devi usare la Python/C API per questo, e chiamare sig_error() dopo aver chiamato qualche variante di PyErr_SetObject(). Anche dentro Cython non puoi usare l’istruzione raise, perch`e cos`i la sig_error() non sarebbe mai eseguita. La chiamata a sig_error() user`a i meccanismi di sig_on() cos`i che l’eccezione sar`a vista da sig_on().
Un tipico gestore di errori implementato in Cython sarebbe come segue:
include "sage/ext/interrupt.pxi"
from cpython.exc cimport PyErr_SetString
cdef void error_handler(char *msg):
PyErr_SetString(RuntimeError, msg)
sig_error()
In Sage questo meccanismo `e utilizzato per libGAP, NTL e PARI.
Ci sono molte funzioni specializzate per gestire gli interrupt. Come detto sopra, sig_on() non cerca di ripulire nulla (restore dello stato o liberare la memoria) quando capita un interrupt. Infatti sarebbe impossibile per sig_on() farlo. Se vuoi aggiungere del codice di pulizia (cleanup), usa sig_on_no_except() per questo. Questa funzione si comporta esattamente come sig_on(), eccetto che qualunque eccezione sollevata (come KeyboardInterrupt o RuntimeError) non `e ancora passata a Python. Essenzialmente l’eccezione `e l`i, ma possiamo impedire a Cython di vederla. Poi si pu`o usare cython_check_exception() per permettere a Cython di cercare l’eccezione.
Normalmente sig_on_no_except() restituisce 1. Se un segnale `e catturato ed un’eccezione sollevata, sig_on_no_except() restituisce, invece, 0. Il seguente esempio mostra come usare sig_on_no_except():
def no_except_example():
if not sig_on_no_except():
# (clean up messed up internal state)
# Make Cython realize that there is an exception.
# It will look like the exception was actually raised
# by cython_check_exception().
cython_check_exception()
# (some long computation, messing up internal state of objects)
sig_off()
C’`e anche una funzione sig_str_no_except(s) che `e analoga a sig_str(s).
Note
Vedi il file SAGE_ROOT/src/sage/tests/interrupt.pyx per maggiori esempi di come usare le varie funzioni sig_*().
Quando si scrive Stringhe di documentazione (doctring), spesso si vuole verificare che un certo codice pu`o essere interrotto in maniera pulita. Il modo migliore di farlo `e usare alarm().
Ecco un esempio du un doctest che dimostra che la funzione factor() pu`o essere interrotta:
sage: alarm(0.5); factor(10^1000 + 3)
Traceback (most recent call last):
...
AlarmInterrupt
Tutte le funzioni legate agli interrupt e la gestione dei segnali non richiedono il Python GIL (se non sai cosa significa, puoi saltare senz’altro questa sezione), essi sono dichiarati nogil. Questo significa che possono essere usati nel codice Cython dentro a blocchi with nogil. Se sig_on() deve sollevare un’eccezione, il GIL `e temporaneamente acquisito internamente.
Se usi le librerie C senza il GIL e vuoi sollevare un’eccezione prima di chiamare sig_error(), ricorda di acquisire il GIL mentre sollevi l’eccezione. Dentro Cython puoi usare un with gil context.
Warning
Il GIL non va mai rilasciato o acquisito dentro ad un blocco sig_on(). Se vuoi usare un blocco with nogil, metti entrambe le sig_on() e sig_off() dentro il blocco. Nel dubbio, usa sig_check() al posto, che `e sempre di utilizzo sicuro.
Gestire i pickle (sottaceti) per le classi Python e per le classi estensioni di Python, come Cython, `e differente. Questo `e discusso nella Python pickling documentation. Per gestire i pickle delle classi estensioni devi scrivere un metodo __reduce__() che tipicamente restituir`a una tupla (f, args, ...) tale che f(*args) restituisce (una copia del) l’oggetto originale. Ad esempio il seguente pezzetto di codice `e il metodo __reduce__() da sage.rings.integer.Integer:
def __reduce__(self):
'''
This is used when pickling integers.
EXAMPLES::
sage: n = 5
sage: t = n.__reduce__(); t
(<built-in function make_integer>, ('5',))
sage: t[0](*t[1])
5
sage: loads(dumps(n)) == n
True
'''
# This single line below took me HOURS to figure out.
# It is the *trick* needed to pickle Cython extension types.
# The trick is that you must put a pure Python function
# as the first argument, and that function must return
# the result of unpickling with the argument in the second
# tuple as input. All kinds of problems happen
# if we don't do this.
return sage.rings.integer.make_integer, (self.str(32),)