Extensie types (aka. cdef classes)¶

Om object-georiënteerd programmeren te ondersteunen, ondersteunt Cython het schrijven van normale Python klassen precies zoals in Python:

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

Gebaseerd op wat Python een “ingebouwd type” noemt, ondersteunt Cython echter een tweede soort klasse: extensie types, soms aangeduid als “cdef classes” vanwege de sleutelwoorden die worden gebruikt voor hun declaratie. Zij zijn enigszins beperkt in vergelijking met Python klassen, maar zijn over het algemeen geheugenefficiënter en sneller dan generieke Python klassen. Het belangrijkste verschil is dat ze een C struct gebruiken om hun velden en methodes op te slaan in plaats van een Python dict. Hierdoor kunnen ze willekeurige C types in hun velden opslaan zonder dat er een Python wrapper voor nodig is, en hebben ze direct toegang tot velden en methodes op C niveau zonder dat ze een Python dictionary hoeven op te zoeken.

Normale Python klassen kunnen erven van cdef klassen, maar niet andersom. Cython moet de volledige overervingshiërarchie kennen om hun C structs te kunnen maken, en beperkt dit tot enkelvoudige overerving. Normale Python klassen daarentegen kunnen erven van een willekeurig aantal Python klassen en extensie types, zowel in Cython code als in pure Python code.

Tot nu toe is ons integratie voorbeeld niet erg bruikbaar geweest omdat het slechts een enkele hard-coded functie integreert. Om dit te verhelpen, zonder aan snelheid in te boeten, zullen we een cdef klasse gebruiken om een functie op drijvende komma getallen weer te geven:

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

De directive cpdef maakt twee versies van de methode beschikbaar; een snelle voor gebruik vanuit Cython en een langzamere voor gebruik vanuit Python.

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)

Dit is iets meer dan een python-wrapper voor een cdef-methode: in tegenstelling tot een cdef-methode is een cpdef-methode volledig overschrijfbaar door methoden en instantie-attributen in Python-subklassen. Het voegt een beetje overhead toe vergeleken met een cdef methode.

Om de klassendefinities zichtbaar te maken voor andere modules, en dus efficiënt gebruik en overerving op C-niveau mogelijk te maken buiten de module die ze implementeert, definiëren we ze in een sin_of_square.pxd bestand:

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

Daarmee kunnen we nu ons integratievoorbeeld wijzigen:

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

Dit is bijna net zo snel als de vorige code, maar het is veel flexibeler omdat de te integreren functie kan worden gewijzigd. We kunnen zelfs een nieuwe functie, gedefinieerd in Python-ruimte, invoeren:

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

Dit is ongeveer 20 keer langzamer, maar nog steeds ongeveer 10 keer sneller dan de oorspronkelijke Python-only integratiecode. Dit laat zien hoe groot de snelheidsverhogingen kunnen zijn wanneer hele lussen worden verplaatst van Python code naar een Cython module.

Enige opmerkingen over onze nieuwe implementatie van evaluate:

  • De snelle methode dispatch hier werkt alleen omdat evaluate was gedeclared in Function. Als evaluate was ingevoerd in SinOfSquareFunction, zou de code nog steeds werken, maar Cython zou in plaats daarvan het langzamere Python method dispatch mechanisme hebben gebruikt.
  • Op dezelfde manier, als het argument f niet was getypt, maar alleen was doorgegeven als een Python object, zou het langzamere Python dispatch mechanisme zijn gebruikt.
  • Omdat het argument getypt is, moeten we controleren of hetNone is. In Python zou dit resulteren in een AttributeError wanneer de evaluate methode wordt opgezocht, maar Cython zou in plaats daarvan proberen om de (incompatibele) interne structuur van None te benaderen alsof het een Function is, wat leidt tot een crash of datacorruptie.

Er is een compiler directief nonecheck die controles hiervoor inschakelt, ten koste van verminderde snelheid. Hier volgt hoe compiler directives worden gebruikt om nonecheck dynamisch in of uit te schakelen:

# 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!

Attributen in cdef klassen gedragen zich anders dan attributen in gewone klassen:

  • Alle attributen moeten vooraf worden gedeclareerd bij het compileren
  • Attributen zijn standaard alleen toegankelijk vanuit Cython (getypte toegang)
  • Eigenschappen kunnen worden gedeclareerd om dynamische attributen bloot te stellen aan Python-ruimte
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

Plaats een reactie