Tipos de extensión (aka. clases cdef)¶

Para soportar la programación orientada a objetos, Cython soporta la escritura de clases normalesPython exactamente como en Python:

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

Basado en lo que Python llama un «tipo incorporado», sin embargo, Cython soporta una segunda clase: tipos de extensión, a veces referidos como «clases cdef» debido a las palabras clave utilizadas para su declaración. Son algo restringidas en comparación con las clases de Python, pero generalmente son más eficientes en memoria y más rápidas que las clases genéricas de Python. La principal diferencia es que utilizan un struct de C para almacenar sus campos y métodos en lugar de un dict de Python. Esto les permite almacenar tipos arbitrarios de C en sus campos sin requerir una envoltura de Python para ellos, y acceder a los campos y métodos directamente en el nivel de C sin pasar por una búsqueda en el diccionario de Python.

Las clases normales de Python pueden heredar de las clases cdef, pero no al revés. Cython requiere conocer la jerarquía de herencia completa para poder disponer de sus structs en C, y la restringe a la herencia simple. Las clases normales de Python, por otro lado, pueden heredar de cualquier número de clases y tipos de extensión de Python, tanto en código Cython como en código Python puro.

Hasta ahora nuestro ejemplo de integración no ha sido muy útil, ya que sólo integra una única función codificada. Para remediar esto, sin sacrificar apenas la velocidad, utilizaremos una clase cdef para representar una función sobre números de punto flotante:

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

La directiva cpdef pone a disposición dos versiones del método; una rápida para usar desde Cython y otra más lenta para usar desde Python. Entonces:

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)

Esto hace algo más que proporcionar una envoltura de python para un método cdef: a diferencia de un método cdef, un método cpdef es totalmente anulable por métodos y atributos de instancia en subclases de Python. Añade un poco de sobrecarga de llamadas en comparación con un método cdef.

Para que las definiciones de las clases sean visibles para otros módulos, y así permitir un uso eficiente del nivel C y la herencia fuera del módulo que las implementa, las definimos en un archivo 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 *

Usando esto, ahora podemos cambiar nuestro ejemplo de integración:

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

Esto es casi tan rápido como el código anterior, sin embargo es mucho más flexible ya que la función a integrar puede ser cambiada. Incluso podemos pasar una nueva función definida en el espacio 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

Esto es unas 20 veces más lento, pero sigue siendo unas 10 veces más rápido que el código original de integración sólo en Python. Esto demuestra lo grande que puede ser la velocidad cuando se trasladan bucles enteros desde el código de Python a un módulo de Cython.

Algunas notas sobre nuestra nueva implementación de evaluate:

  • El envío rápido de métodos aquí sólo funciona porque evaluate fue declarado en Function. Si evaluate se hubiera introducido enSinOfSquareFunction, el código seguiría funcionando, pero Cython habría utilizado el mecanismo más lento de envío de métodos de Python.
  • De la misma manera, si el argumento f no se hubiera tipado, sino que sólo se hubiera pasado como un objeto de Python, se utilizaría el envío más lento de Python.
  • Dado que el argumento está tipado, tenemos que comprobar si esNone. En Python, esto habría dado lugar a un AttributeErrorcuando el método evaluate fue buscado, pero Cython en su lugar trataría de acceder a la estructura interna (incompatible) de None como si se tratara de un Function, lo que llevaría a una caída o corrupción de datos.

Hay una directiva del compilador nonecheckque activa las comprobaciones para esto, a costa de la disminución de la velocidad. Así es como se usan las directivas del compilador para activar o desactivar dinámicamente 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!

Los atributos de las clases cdef se comportan de forma diferente a los atributos de las clases normales:

  • Todos los atributos deben ser pre-declarados en tiempo de compilación
  • Los atributos son por defecto sólo accesibles desde Cython (typed access)
  • Se pueden declarar propiedades para exponer atributos dinámicos al espacio 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

Deja un comentario