Tipi di estensione (aka. cdef classes)¶

Per supportare la programmazione orientata agli oggetti, Cython supporta la scrittura di normali classi Python esattamente come in Python:

class MathFunction(object): def __init__(self, name, operator): self.name = name self.operator = operator def __call__(self, *operands): return self.operator(*operands)

Basato su ciò che Python chiama un “tipo incorporato”, tuttavia, Cython supporta un secondo tipo di classe: i tipi di estensione, a volte chiamati “cdef classes” a causa delle parole chiave usate per la loro dichiarazione. Essi sono un po’ limitati rispetto alle classi Python, ma sono generalmente più efficienti in termini di memoria e più veloci delle classi Python generiche. La differenza principale è che usano una struct C per memorizzare i loro campi e metodi invece di un dict Python. Questo permette loro di memorizzare tipi C arbitrari nei loro campi senza richiedere un wrapper Python per essi, e di accedere a campi e metodi direttamente a livello C senza passare attraverso una ricerca nel dizionario Python.

Le classi Python normali possono ereditare dalle classi cdef, ma non il contrario. Cython richiede di conoscere la gerarchia completa dell’ereditarietà per poter disporre le proprie strutture C, e la limita aosingle inheritance. Le normali classi Python, invece, possono ereditare da qualsiasi numero di classi Python e tipi di estensione, sia nel codice Python che nel puro codice Python.

Finora il nostro esempio di integrazione non è stato molto utile, poiché integra solo una singola funzione hard-coded. Per rimediare a questo, senza sacrificare la velocità, useremo una classe cdef per rappresentare una funzione su numeri in virgola mobile:

cdef class Function: cpdef double evaluate(self, double x) except *: return 0

La direttiva cpdef rende disponibili due versioni del metodo; una veloce per l’uso da Cython e una più lenta per l’uso da Python. Quindi:

from libc.math cimport sincdef class Function: cpdef double evaluate(self, double x) except *: return 0cdef class SinOfSquareFunction(Function): cpdef double evaluate(self, double x) except *: return sin(x ** 2)

Questo fa un po’ di più che fornire un wrapper python per un metodo cdef: diversamente da un metodo cdef, un metodo cpdef è completamente sovrascrivibile da metodi e attributi di istanza nelle sottoclassi Python. Aggiunge un po’ di overhead di chiamata rispetto ad un metodo cdef.

Per rendere le definizioni delle classi visibili ad altri moduli, e quindi permettere un uso efficiente a livello C e l’ereditarietà fuori dal modulo che le implementa, le definiamo in un file sin_of_square.pxd:

cdef class Function: cpdef double evaluate(self, double x) except *cdef class SinOfSquareFunction(Function): cpdef double evaluate(self, double x) except *

Utilizzando questo, possiamo ora cambiare il nostro esempio di integrazione:

from sin_of_square cimport Function, SinOfSquareFunctiondef integrate(Function f, double a, double b, int N): cdef int i cdef double s, dx if f is None: raise ValueError("f cannot be None") s = 0 dx = (b - a) / N for i in range(N): s += f.evaluate(a + i * dx) return s * dxprint(integrate(SinOfSquareFunction(), 0, 1, 10000))

Questo è quasi veloce come il codice precedente, tuttavia è molto più flessibile poiché la funzione da integrare può essere modificata. Possiamo anche passare una nuova funzione definita nello spazio Python:

>>> import integrate>>> class MyPolynomial(integrate.Function):... def evaluate(self, x):... return 2*x*x + 3*x - 10...>>> integrate(MyPolynomial(), 0, 1, 10000)-7.8335833300000077

Questo è circa 20 volte più lento, ma ancora circa 10 volte più veloce del codice originale di integrazione solo Python. Questo mostra quanto grandi possano essere le accelerazioni quando interi cicli sono spostati dal codice Python in un modulo Cython.

Alcune note sulla nostra nuova implementazione di evaluate:

  • Il metodo dispatch veloce qui funziona solo perché evaluate è stato dichiarato in Function. Se evaluate fosse stato introdotto in SinOfSquareFunction, il codice funzionerebbe ancora, ma Cython avrebbe invece usato il più lento meccanismo di dispacciamento dei metodi Python.
  • Nello stesso modo, se l’argomento f non fosse stato digitato, ma solo passato come oggetto Python, sarebbe stato usato il più lento dispacciamento Python.
  • Siccome l’argomento è digitato, dobbiamo controllare se èNone. In Python, questo avrebbe portato ad un AttributeErrorquando il metodo evaluate veniva cercato, ma Cython avrebbe invece cercato di accedere alla struttura interna (incompatibile) di None come se fosse un Function, portando ad un crash o alla corruzione dei dati.

C’è una direttiva del compilatore nonecheck che attiva i controlli per questo, al costo di una minore velocità. Ecco come le direttive del compilatore sono usate per attivare o disattivare dinamicamente nonecheck:

# cython: nonecheck=True# ^^^ Turns on nonecheck globallyimport cythoncdef class MyClass: pass# Turn off nonecheck locally for the [email protected](False)def func(): cdef MyClass obj = None try: # Turn nonecheck on again for a block with cython.nonecheck(True): print(obj.myfunc()) # Raises exception except AttributeError: pass print(obj.myfunc()) # Hope for a crash!

Gli attributi nelle classi cdef si comportano diversamente dagli attributi nelle classi normali:

  • Tutti gli attributi devono essere predichiarati in fase di compilazione
  • Gli attributi sono per default accessibili solo da Cython (accesso tipizzato)
  • Le proprietà possono essere dichiarate per esporre gli attributi dinamici allo spazio Python
from sin_of_square cimport Functioncdef class WaveFunction(Function): # Not available in Python-space: cdef double offset # Available in Python-space: cdef public double freq # Available in Python-space, but only for reading: cdef readonly double scale # Available in Python-space: @property def period(self): return 1.0 / self.freq @period.setter def period(self, value): self.freq = 1.0 / value

Lascia un commento