Types d’extension (alias. cdef classes)¶

Pour supporter la programmation orientée objet, Cython supporte l’écriture de classes Python normales exactement comme en Python:

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

Sur la base de ce que Python appelle un « type intégré », cependant, Cython supporte un deuxième type de classe : les types d’extension, parfois appelés « classes cdef » en raison des mots-clés utilisés pour leur déclaration. Ils sont quelque peu limités par rapport aux classes Python, mais sont généralement plus efficaces en termes de mémoire et plus rapides que les classes Python génériques. La principale différence est qu’elles utilisent un struct C pour stocker leurs champs et méthodes au lieu d’un dict Python. Cela leur permet de stocker des types C arbitraires dans leurs champs sans avoir besoin d’un wrapper Python pour eux, et d’accéder aux champs et aux méthodes directement au niveau C sans passer par une recherche dans un dictionnaire Python.

Les classes Python normales peuvent hériter des classes cdef, mais pas l’inverse. Cython a besoin de connaître la hiérarchie d’héritage complète afin d’agencer ses structures C, et le restreint à l’héritage simple. Les classes Python normales, en revanche, peuvent hériter de n’importe quel nombre de classes Python et de types d’extension, à la fois dans le code Cython et dans le code Python pur.

Jusqu’ici, notre exemple d’intégration n’a pas été très utile car il n’intègre qu’une seule fonction codée en dur. Afin de remédier à cela,en ne sacrifiant guère la vitesse, nous allons utiliser une classe cdef pour représenter une fonction sur des nombres à virgule flottante:

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

La directive cpdef rend disponible deux versions de la méthode ; une rapide pour une utilisation depuis Cython et une plus lente pour une utilisation depuis Python. Alors:

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)

Cela fait un peu plus que de fournir un wrapper python pour une méthode cdef : contrairement à une méthode cdef, une méthode cpdef est entièrement surchargeable par des méthodes et des attributs d’instance dans les sous-classes Python. Cela ajoute un peu de surcharge d’appel par rapport à une méthode cdef.

Pour rendre les définitions de classe visibles aux autres modules, et ainsi permettre une utilisation efficace au niveau C et un héritage en dehors du module qui les implémente, nous les définissons dans un fichier 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 *

En utilisant ceci, nous pouvons maintenant changer notre exemple d’intégration:

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

C’est presque aussi rapide que le code précédent, cependant c’est beaucoup plus flexible car la fonction à intégrer peut être changée. Nous pouvons même passer dans une nouvelle fonction définie dans l’espace 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

Ceci est environ 20 fois plus lent, mais toujours environ 10 fois plus rapide que le code d’intégration original uniquement en Python. Cela montre à quel point les accélérations peuvent facilement être importantes lorsque des boucles entières sont déplacées du code Python vers un module Cython.

Quelques notes sur notre nouvelle implémentation de evaluate:

  • L’envoi rapide de méthodes ici ne fonctionne que parce que evaluate a étédéclaré dans Function. Si evaluate avait été introduit enSinOfSquareFunction, le code fonctionnerait toujours, mais Cython aurait utilisé le mécanisme plus lent de répartition des méthodes Python à la place.
  • De la même manière, si l’argument f n’avait pas été typé, mais seulement passé comme un objet Python, la répartition Python plus lente aurait été utilisée.
  • Puisque l’argument est typé, nous devons vérifier s’il estNone. En Python, cela aurait entraîné un AttributeErrorlorsque la méthode evaluate a été recherchée, mais Cython essaierait plutôt d’accéder à la structure interne (incompatible) de None comme s’il s’agissait d’un Function, entraînant un plantage ou une corruption de données.

Il existe une directive de compilateur nonecheck qui active les vérifications pour cela, au prix d’une vitesse réduite. Voici comment les directives du compilateur sont utilisées pour activer ou désactiver dynamiquement 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!

Les attributs des classes cdef se comportent différemment des attributs des classes ordinaires :

  • Tous les attributs doivent être pré-déclarés à la compilation
  • Les attributs sont par défaut uniquement accessibles depuis Cython (accès typé)
  • Des propriétés peuvent être déclarées pour exposer les attributs dynamiques à l’espace 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

.

Laisser un commentaire