Typy rozszerzeń (aka. cdef classes)ś

Aby wspierać programowanie zorientowane obiektowo, Cython obsługuje pisanie normalnych klas Pythona dokładnie tak, jak w Pythonie:

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

Na podstawie tego, co Python nazywa „typem wbudowanym”, Cython obsługuje jednak drugi rodzaj klas: typy rozszerzeń, czasami określane jako „klasy cdef” ze względu na słowa kluczowe używane do ich deklaracji. Są one nieco ograniczone w porównaniu z klasami Pythona, ale generalnie są bardziej wydajne pamięciowo i szybsze niż generyczne klasy Pythona. Główna różnica polega na tym, że do przechowywania pól i metod używają one struktury C zamiast pythonowskiego dict. Pozwala to na przechowywanie dowolnych typów C w ich polach bez konieczności stosowania dla nich wrappera Pythona, a także na dostęp do pól i metod bezpośrednio na poziomie C bez konieczności przechodzenia przez wyszukiwarkę słownika Pythona.

Normalne klasy Pythona mogą dziedziczyć po klasach cdef, ale nie odwrotnie. Cython wymaga znajomości kompletnej hierarchii dziedziczenia, aby rozłożyć swoje struktury C, i ogranicza je do dziedziczenia typusingle. Normalne klasy Pythona, z drugiej strony, mogą dziedziczyć z dowolnej liczby klas Pythona i typów rozszerzeń, zarówno w kodzie Cythona, jak i w czystym kodzie Pythona.

Do tej pory nasz przykład integracji nie był zbyt użyteczny, ponieważ integruje on tylko jedną, twardo zakodowaną funkcję. Aby temu zaradzić, z trudem poświęcając szybkość, użyjemy klasy cdef do reprezentowania funkcji na liczbach zmiennoprzecinkowych:

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

Dyrektywa cpdef udostępnia dwie wersje metody; jedną szybką do użycia z Cythona i jedną wolniejszą do użycia z Pythona. Następnie:

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)

To robi nieco więcej niż zapewnienie pythonowej otoczki dla metody cdef: w przeciwieństwie do metody cdef, metoda cpdef jest w pełni nadpisywalna przez metody i atrybuty instancji w podklasach Pythona. Dodaje to niewielki narzut wywołania w porównaniu do metody cdef.

Aby uczynić definicje klas widocznymi dla innych modułów, a tym samym umożliwić efektywne wykorzystanie poziomu C i dziedziczenie poza modułem, który je implementuje, definiujemy je w pliku 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 *

Używając tego, możemy teraz zmienić nasz przykład integracji:

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

Jest to prawie tak samo szybkie jak poprzedni kod, jednak jest o wiele bardziej elastyczne, ponieważ funkcja do integracji może być zmieniana. Możemy nawet przekazać nową funkcję zdefiniowaną w przestrzeni Pythona:

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

Jest to około 20 razy wolniejsze, ale wciąż około 10 razy szybsze niż oryginalny kod integrujący tylko Pythona. To pokazuje, jak duże mogą być przyspieszenia, gdy całe pętle są przenoszone z kodu Pythona do modułu Cythona.

Kilka uwag na temat naszej nowej implementacji evaluate:

  • Szybka wysyłka metod działa tutaj tylko dlatego, że evaluate została zadeklarowana w Function. Gdyby evaluate został wprowadzony wSinOfSquareFunction, kod nadal by działał, ale Cython użyłby zamiast tego wolniejszego mechanizmu Python method dispatch.
  • W ten sam sposób, gdyby argument f nie był wpisany, lecz tylko przekazany jako obiekt Pythona, użyto by wolniejszego Python dispatch.
  • Ponieważ argument jest wpisany, musimy sprawdzić, czy jest toNone. W Pythonie spowodowałoby to pojawienie się AttributeErrorpodczas sprawdzania metody evaluate, ale Cython zamiast tego próbowałby uzyskać dostęp do (niekompatybilnej) wewnętrznej struktury None, tak jakby była Function, co doprowadziłoby do awarii lub uszkodzenia danych.

Istnieje dyrektywa kompilatora nonecheck, która włącza sprawdzanie tego, kosztem zmniejszenia szybkości. Oto jak dyrektywy kompilatora są używane do dynamicznego włączania lub wyłączania 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!

Atrybuty w klasach cdef zachowują się inaczej niż atrybuty w zwykłych klasach:

  • Wszystkie atrybuty muszą być wstępnie zadeklarowane w czasie kompilacji
  • Atrybuty są domyślnie dostępne tylko z poziomu Cythona (typed access)
  • Można zadeklarować właściwości, aby wyeksponować dynamiczne atrybuty do przestrzeni Pythona
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

.

Dodaj komentarz