Erweiterungstypen (aka. cdef classes)¶

Um objektorientiertes Programmieren zu unterstützen, unterstützt Cython das Schreiben normalerPython-Klassen genau wie in Python:

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

Basierend auf dem, was Python einen „eingebauten Typ“ nennt, unterstützt Cython jedoch eine zweite Art von Klasse: Erweiterungstypen, die aufgrund der Schlüsselwörter, die für ihre Deklaration verwendet werden, manchmal als „cdef-Klassen“ bezeichnet werden. Sie sind im Vergleich zu Python-Klassen etwas eingeschränkt, sind aber im Allgemeinen speichereffizienter und schneller als generische Python-Klassen. Der Hauptunterschied besteht darin, dass sie ein C struct zum Speichern ihrer Felder und Methoden verwenden, anstatt eines Python dict. Dies erlaubt es ihnen, beliebige C-Typen in ihren Feldern zu speichern, ohne dass ein Python-Wrapper für sie erforderlich ist, und auf Felder und Methoden direkt auf C-Ebene zuzugreifen, ohne eine Python-Dictionary-Lookup zu durchlaufen.

Normale Python-Klassen können von cdef-Klassen erben, aber nicht andersherum. Cython benötigt die Kenntnis der kompletten Vererbungshierarchie, um ihre C-Strukturen anzulegen, und beschränkt sich auf Einzelvererbung. Normale Python-Klassen hingegen können von einer beliebigen Anzahl von Python-Klassen und Erweiterungstypen erben, sowohl in Cython-Code als auch in reinem Python-Code.

Bislang war unser Integrationsbeispiel nicht sehr nützlich, da es nur eine einzige fest kodierte Funktion integriert. Um hier Abhilfe zu schaffen, ohne die Geschwindigkeit zu beeinträchtigen, werden wir eine cdef-Klasse verwenden, um eine Funktion für Fließkommazahlen zu repräsentieren:

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

Die Direktive cpdef stellt zwei Versionen der Methode zur Verfügung; eine schnelle für die Verwendung in Cython und eine langsamere für die Verwendung in Python. Dann:

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)

Dies ist etwas mehr als nur ein Python-Wrapper für eine cdef-Methode: Im Gegensatz zu einer cdef-Methode ist eine cpdef-Methode vollständig überschreibbar durch Methoden und Instanzattribute in Python-Unterklassen. Sie fügt im Vergleich zu einer cdef-Methode ein wenig Aufruf-Overhead hinzu.

Um die Klassendefinitionen für andere Module sichtbar zu machen und damit eine effiziente Verwendung auf C-Ebene und Vererbung außerhalb des Moduls, das sie implementiert, zu ermöglichen, definieren wir sie in einer sin_of_square.pxdDatei:

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

Damit können wir nun unser Integrationsbeispiel ändern:

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

Dies ist fast genauso schnell wie der vorherige Code, aber viel flexibler, da die zu integrierende Funktion geändert werden kann. Wir können sogar eine neue Funktion übergeben, die im Python-Raum definiert ist:

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

Dies ist etwa 20 Mal langsamer, aber immer noch etwa 10 Mal schneller als der ursprüngliche, nur in Python geschriebene Integrationscode. Das zeigt, wie groß der Geschwindigkeitszuwachs sein kann, wenn ganze Schleifen aus dem Python-Code in ein Cython-Modul verlagert werden.

Ein paar Anmerkungen zu unserer neuen Implementierung von evaluate:

  • Die schnelle Methodenauslieferung funktioniert hier nur, weil evaluate in Function deklariert wurde. Wäre evaluate inSinOfSquareFunction eingeführt worden, würde der Code immer noch funktionieren, aber Cython hätte stattdessen den langsameren Python-Methoden-Dispatch-Mechanismus verwendet.
  • Wäre das Argument f nicht typisiert, sondern nur als Python-Objekt übergeben worden, würde der langsamere Python-Dispatch verwendet werden.
  • Da das Argument typisiert ist, müssen wir prüfen, ob esNone ist. In Python würde dies zu einem AttributeError führen, wenn die evaluate-Methode nachgeschlagen wird, aber Cython würde stattdessen versuchen, auf die (inkompatible) interne Struktur von None zuzugreifen, als ob es ein Function wäre, was zu einem Absturz oder einer Datenbeschädigung führen würde.

Es gibt eine Compilerdirektive nonecheck, die diese Prüfung einschaltet, allerdings auf Kosten der Geschwindigkeit. So werden Compilerdirektiven verwendet, um nonecheck dynamisch ein- oder auszuschalten:

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

Attribute in cdef-Klassen verhalten sich anders als Attribute in normalen Klassen:

  • Alle Attribute müssen zur Kompilierzeit vordeklariert werden
  • Attribute sind standardmäßig nur von Cython aus zugänglich (typisierter Zugriff)
  • Eigenschaften können deklariert werden, um dynamische Attribute dem Python-Raum zugänglich zu machen
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

Schreibe einen Kommentar