Implementierung einer Python-Schnittstelle für das Softwarepaket H2Lib - Uni Kiel
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Christian-Albrechts-Universität zu Kiel Technische Fakultät Institut für Informatik Implementierung einer Python-Schnittstelle für das Softwarepaket H2Lib Bachelorarbeit Betreuer: Prof. Dr. Steffen Börm Arbeitsgruppe Scientific Computing vorgelegt von Marek Hummel stu210310@mail.uni-kiel.de — Matrikel-Nr. 1126000 Kiel, 23. September 2020
Eidesstattliche Erklärung zur Bachelorarbeit Ich versichere, dass ich die vorliegende Arbeit selbstständig verfasst und keine anderen als die angegebenen Hilfsmittel benutzt habe. Alle Stellen, die dem Wortlaut oder dem Sinne nach anderen Texten entnommen sind, wurden unter Angabe der Quellen (einschließlich des World Wide Web und anderer elektronischer Text- und Datensammlungen) und nach den üblichen Regeln des wissenschaftlichen Zitierens nachgewiesen. Dies gilt auch für Zeichnungen, bildliche Darstellungen, Skizzen, Tabellen und dergleichen. Mir ist bewusst, dass wahrheitswidrige Angaben als Täuschungsversuch behandelt werden und dass bei einem Täuschungsverdacht sämtliche Verfahren der Plagiatserkennung angewandt werden können. Kiel, 23. September 2020 Marek Hummel
Inhaltsverzeichnis Listingverzeichnis 4 Abkürzungsverzeichnis 5 1. Einleitung 7 2. Installation 8 2.1. Als Nutzer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2. Als Entwickler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3. Die Schnittstelle 10 3.1. Umfang . . . . . . . . . . . . . . . . .. . . .. . . . . . . . . . . . . . . . . 10 3.2. Aufbau . . . . . . . . . . . . . . . . . .. . . .. . . . . . . . . . . . . . . . . 11 3.3. Code Style . . . . . . . . . . . . . . .. . . .. . . . . . . . . . . . . . . . . 13 3.4. Paket h2libpy . . . . . . . . . . . . .. . . .. . . . . . . . . . . . . . . . . 13 3.4.1. Submodul h2libpy/lib/ .. . . .. . . . . . . . . . . . . . . . . 14 3.4.1.1. Datei: h2libpy/lib/util/structs.py . . . . . . . . . . 15 3.4.1.2. Datei: h2libpy/lib/util/helper.py . . . . . . . . . . . 16 3.4.1.3. Dateien: h2libpy/lib/*.py . . . . . . . . . . . . . . . . . 17 3.4.2. Submodul h2libpy/base/ . . . . . . . . . . . . . . . . . . . . . 18 3.4.2.1. Datei: h2libpy/base/util.py . . . . . . . . . . . . . . . . 18 3.4.2.2. Datei: h2libpy/base/structwrapper.py . . . . . . . . . 19 3.4.3. Submodul h2libpy/data/ . . . . . . . . . . . . . . . . . . . . . 23 3.4.3.1. Beispiel: h2libpy/data/matrix/amatrix.py . . . . . . . 26 3.4.3.2. Submodule von h2libpy/data/ . . . . . . . . . . . . . . . 31 3.4.4. Submodul h2libpy/solver/ . . . . . . . . . . . . . . . . . . . . 37 3.5. Verschiedenes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 4. Nutzung 39 4.1. Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 4.2. Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 4.3. Callbacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 4.4. Referencing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 4.5. Grenzen der Abstraktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 5. Beispielprogramme 44 5.1. Beschränkung auf Lib-Dateien . . . . . . . . . . . . . . . . . . . . . . . . 44 5.2. Wechsel zu Wrapper-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . 45 6. Laufzeit 47 7. Ausblick 49
A. Lib-Dateien i A.1. Lib-Datei bem3d.py (Ausschnitt) . . . . . . . . . . . . . . . . . . . . . . i B. StructWrapper-Klasse v C. Beispielprogramme vii C.1. example_h2matrix_bem3d_libonly.py . . . . . . . . . . . . . . . . vii C.2. example_h2matrix_bem3d.py . . . . . . . . . . . . . . . . . . . . . . . xii Literaturverzeichnis xvii
4 Listingverzeichnis Listingverzeichnis 3.1. __init__.py-Datei des Wurzelverzeichnisses . . . . . . . . . . . . . . . . 14 3.2. Definition CEnumBasisFunctionBem3d . . . . . . . . . . . . . . . . . . . 15 3.3. get_func-Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.4. Aufbau Lib-Datei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.5. __init_subclass__-Methode in StructWrapper . . . . . . . . . . . . 20 3.6. __getattr_-Methode in StructWrapper . . . . . . . . . . . . . . . . . . 22 3.7. __setattr_-Methode in StructWrapper . . . . . . . . . . . . . . . . . . 22 3.8. delete-Methode in StructWrapper . . . . . . . . . . . . . . . . . . . . . 22 3.9. Aufbau einer Subklasse von StructWrapper . . . . . . . . . . . . . . . . 24 3.10. Imports Wrapper-Klasse AMatrix . . . . . . . . . . . . . . . . . . . . . . . 26 3.11. Definition Wrapper-Klasse AMatrix . . . . . . . . . . . . . . . . . . . . . . 26 3.12. Klassenmethoden Wrapper-Klasse AMatrix . . . . . . . . . . . . . . . . . 27 3.13. Beispielmethoden Wrapper-Klasse AMatrix . . . . . . . . . . . . . . . . . 29 3.14. Operatoren Wrapper-Klasse AMatrix . . . . . . . . . . . . . . . . . . . . . 30 3.15. get_properties-Methode in Surface3d . . . . . . . . . . . . . . . . . . 32 3.16. iterate-Methode in H2Matrix . . . . . . . . . . . . . . . . . . . . . . . . 33 3.17. Gebrauch des Bibliotheks-internen Reference Counting . . . . . . . . . . 34 3.18. Unterschiede von Enumerationen . . . . . . . . . . . . . . . . . . . . . . . . 34 3.19. Dynamische Typisierung bei der mvm-Methode . . . . . . . . . . . . . . . . 36 3.20. Dynamische Typisierung bei der solve_cg-Methode . . . . . . . . . . . 37
Abkürzungsverzeichnis 5 Abkürzungsverzeichnis BEM . . . . . . . . . . . . . . Boundary Element Method (Randelementmethode) BLAS . . . . . . . . . . . . . . Basic Linear Algebra Subprograms GC . . . . . . . . . . . . . . . Garbage Collection/Collector LAPACK . . . . . . . . . . . . Linear Algebra PACKage LGPLv3 . . . . . . . . . . . . GNU Lesser General Public License v3 OOP . . . . . . . . . . . . . . Object Oriented Programming PEP8 . . . . . . . . . . . . . . Python Enhancement Proposal Nr. 8 PyPI . . . . . . . . . . . . . . Python Package Index REPL . . . . . . . . . . . . . . Read-eval-print loop TOML . . . . . . . . . . . . . Tom’s Obvious, Minimal Language
7 1. Einleitung In dieser Bachelorarbeit wird eine in Python geschriebene Schnittstelle vorgestellt, wel- che Teile der existierenden H2Lib1 bereitstellen soll. Die H2Lib ist eine Open Source C- Bibliothek, gedacht für das Arbeiten mit hierarchischen und H2 -Matrizen, welche wie diese Arbeit aus in der Arbeitsgruppe Scientific Computing2 der Christian-Albrechts- Universität zu Kiel stammt. Grundlegend umfasst diese Bibliothek die benötigten Da- tenstrukturen für besagte Matrizen, Clusters, Basen und Blocks, sowie Algorithmen für deren arithmetischen Operationen. Darüber hinaus sind auch verschiedene Kompressi- onsalgorithmen für Integraloperatoren, Konvertierungsmethoden zwischen einzelner Matrixdarstellungen sowie Funktionen für Modellprobleme wie die Randelementmethode gegeben. Die Schnittstelle dieser Arbeit, welche den Namen h2libpy trägt, stellt nun eben diese Funktionalitäten in Python bereit. Da Python als Interpretersprache bekannterweise deutlich langsamer als C ist, war das Vorhaben nicht, den Code zu übersetzen, sondern die Bibliothek in Python einzubinden, sodass letztendlich die tatsächlichen Methoden innerhalb der H2Lib genutzt werden. Somit bleibt die Effizienz größtenteils erhalten, aber gleichzeitig können höhere Programmierkonzepte wie objektorientierte Programmie- rung mit Vererbung und Überladung genutzt werden. Des Weiteren bietet Python durch eine einfachere Syntax und Handhabung ein hoffentlich leichteren Einstieg zur H2Lib, besonders wenn weniger Programmierkenntnisse vorhanden sind. Die Zielsetzung war und ist es also, ohne die Performanz der internen Algorithmen zu komprimieren, einen Zugang in Python bereitzustellen, wobei das Interfacing maximal abstrahiert wird, sodass der Nutzer dieser Schnittstelle möglichst »pythonischen« Quellcode schreiben kann. Da diese Arbeit im Rahmen des Bachelorabschlusses verfasst worden ist, ist nur ein Teil der H2Lib in Python zur Verfügung gestellt. Allerdings sollte die bisherige Art der Entwicklung zusammen mit dieser Arbeit ein gutes Fundament darstellen, um die Schnitt- stelle beliebig zu erweitern, da für alle bekannten Hürden ein Proof of Concept vorliegt. Im Folgenden soll erklärt werden, wie die Schnittstelle installiert werden kann, was dafür benötigt wird, wie sie aufgebaut ist und letztendlich wie sie genutzt werden kann. 1 H2Lib: http://www.h2lib.org/ 2 Arbeitsgruppe SC: https://www.math.uni-kiel.de/scicom/de
8 2. Installation 2. Installation Zunächst soll geklärt werden, wie die Schnittstelle genutzt werden kann. Da es sich um eine Python-Implementierung handelt, sollte auf jeden Fall eine Python-Installation auf dem Zielsystem vorhanden sein. Als minimale Version wird 3.5 vorausgesetzt, da intern Type Annotations / Type Hints genutzt werden, entwickelt wurde allerdings mit der Version 3.8, welche daher auch empfohlen wird. Die Schnittstelle ist als Repository auf GitHub veröffentlicht und wird dort verwaltet. Der Quellcode kann also über dieses Repository bezogen werden, oder aber kann ein Nutzer eine fertige Release-Datei downloaden und diese lokal installieren. Zu finden ist das Repository unter https://github.com/marekhummel/h2libpy. 2.1. Als Nutzer Möchte ein Anwender die Schnittstelle nur benutzen, so muss die Schnittstelle als Paket lokal installiert werden. Im genannten GitHub-Repository findet sich unter den Releases eine .whl-Datei, welche heruntergeladen werden kann. Es ist auch ein Tarball in diesem Release vorhanden, allerdings ist dieser nur der Vollständigkeit wegen dabei, da beide Dateien bei der Generierung des Pakets erstellt werden. Im Normalfall wird die .whl- Datei bevorzugt und sollte deswegen auch verwendet werden. Mit folgendem Befehl oder Varianten davon kann dieses Paket dann in die System-, User- oder virtuelle Umgebung geladen werden, je nachdem was gewünscht ist. 1 $ pip install path/to/wheel/.whl In der Zukunft mag es vorteilhaft sein, dieses Paket über den offiziellen Index PyPI1 zu veröffentlichen. Ein vollständiger Support für die Distribution der Schnittstelle ist nicht Teil dieser Arbeit, allerdings wurde das Paket als Grundlage auf dem Test-Server des Indizes hochgeladen. Mit folgendem Shell-Befehl lässt sich die Schnittstelle also auch herunterladen und installieren. 1 $ pip install --index-url https://test.pypi.org/simple/ h2libpy Es sei nochmals betont, dass dies wirklich nur zu experimentellen Zwecken gedacht ist und nur die Möglichkeiten offen legen soll. Der bevorzugte Weg ist folglich die Installation über die .whl-Datei, solange das Paket nicht auf dem offiziellen PyPI-Server zu finden ist. Natürlich kann der Quellcode auch direkt heruntergeladen und im eigenen 1 PyPI: https://pypi.org/
2.2. Als Entwickler 9 Projektverzeichnis hinterlegt werden. Hier ist aber anzumerken, dass dann ggf. Probleme mit den Importen entstehen können, weswegen dies demnach auch nicht empfohlen ist. In allen Fällen kann im Anschluss der Befehl > import h2libpy zum Beispiel in Pythons REPL getestet werden und sollte keine Fehler werfen. 2.2. Als Entwickler Soll die Schnittstelle nicht nur genutzt, sondern weiterentwickelt werden, so werden zunächst zwei Dinge benötigt: • Der Quellcode ist wie erwähnt in einem GitHub-Repository veröffentlicht und kann demnach mittels 1 $ git clone https://github.com/marekhummel/h2libpy.git geclont werden. Es sei an dieser Stelle anzumerken, dass diese Arbeit, wie auch die H2Lib unter der LGPLv3-Lizenz2 steht. • Für die Paketverwaltung wird in dieser Software das Python-Paket pipenv genutzt und muss daher ebenfalls installiert sein, mindestens auf Nutzerebene. Das Paket pipenv stellt dabei eine einfache Verbindung aus pip und venv dar, was bedeutet, dass alle genutzten Packages über pipenv installiert werden sollten. Mit dem nun lokal verfügbaren Quellcode kann mit $ pipenv install dieses Verzeich- nis initialisiert werden, sprich es wird eine virtuelle Umgebung angelegt und es werden die benötigten Packages installiert. Da im Moment h2libpy keine Abhängigkeiten hat, außer welche, die für Entwickler relevant sind, wie Linter, Type Checker etc., sollte direkt $ pipenv install --dev genutzt werden. Jetzt steht ein lauffähiges Projekt zur Verfügung, es kann also mit $ pipenv run python eine REPL innerhalb der zuvor erstellten virtuellen Umgebung gestartet werden und der Befehl > import h2libpy dürfte wieder keinen Fehler werfen. In dem misc-Ordner des Repositorys finden sich verschiedene Shell-Dateien, darunter auch packaging.sh, welche genutzt werden kann, um lokal das Packaging vorzuneh- men. Besonders mit dem Befehl 1 $ pipenv run python setup.py sdist bdist_wheel werden die beiden Dateien im dist-Verzeichnis erstellt, die auch im Release hochgeladen wurden. 2 LGPL v3: https://www.gnu.org/licenses/lgpl-3.0.en.html
10 3. Die Schnittstelle 3. Die Schnittstelle Es soll sich nun in diesem Abschnitt genauer angeschaut werden, wie das Paket h2libpy aufgebaut ist und warum es diese Struktur hat. Genauer werden hier jedes Unterverzeich- nis bzw. dessen Dateien beschrieben, sodass die Rolle für die Schnittstelle klar werden soll. Zunächst aber der generelle Umfang des Pakets und der grobe Aufbau, bevor es zu den einzelnen Modulen geht. 3.1. Umfang Diese Bachelorarbeit hat als Ziel zu zeigen, wie eine Schnittstelle in Python für die H2Lib aussehen kann. Dazu wurde sich inhaltlich auf die drei-dimensionale Randele- mentmethode fokussiert, welche in der Bibliothek in der bem3d.h-Datei definiert ist. Um dessen Funktionalitäten lückenlos in Python zu integrieren, werden sämtliche fun- damentalen Datenstrukturen und Algorithmen benötigt. Darunter sind verschiedene Matrix-Repräsentationen und dessen Komponenten, Vektoren, die Datenstrukturen für BEM an sich und einige Algorithmen der iterativen Krylow-Löser. In der H2Lib existieren drei Beispielprogramme für BEM, jeweils für normal besetzte Matrizen, für H-Matrizen und für H2 -Matrizen, welche in Python dann letztendlich vollständig umgesetzt werden können. Diese Zielsetzung hat als Folge, dass zum Zeitpunkt dieser Arbeit 29 Dateien der H2Lib in der Schnittstelle fast vollständig integriert sind. Bei diesen Dateien handelt es sich um folgende: amatrix avector bem3d block cluster clusterbasis clustergeometry clusteroperator dblock dcluster dclusterbasis dclusteroperator dh2matrix duniform h2matrix hmatrix krylov krylovsolvers laplacebem3d macrosurface3d realavector rkmatrix settings singquad2d sparsematrix sparsepattern surface3d truncation uniform Das heißt, dass beinahe jede Methode dieser C-Dateien über ein Pendant in Python auf- gerufen werden kann, jede C-struct eine Python-Klasse besitzt und auch allgemeinere Typdefinitionen in einer Form zur Verfügung stehen. Es sind einige Ausnahmen gegeben,
3.2. Aufbau 11 wie zum Beispiel CDF1 -Methoden zum Import und Export einiger Datenstrukturen im CDF-Format, allerdings sind diese Ausnahmen weniger relevante Methoden, da sie selbst in der H2Lib explizit über Optionen aktiviert werden müssen. Darüber hinaus können alle statischen bzw. Inline-Methoden auch nicht bereitgestellt werden, da beide nach der Kompilierung maximal innerhalb der Bibliothek noch existieren, nicht aber der Zugriff von Außen erlauben. Die H2Lib bietet bei der Kompilierung verschiedene Optionen, die die Funktionsweise der Bibliothek beeinflussen. Es kann unter anderem: • USE_BLAS aktiviert werden, sodass die LAPACK-, BLAS- und gfortran- Bibliothe- ken mit eingebunden werden. • USE_FLOAT aktiviert werden, sodass nur Fließkommazahlen einfacher Genauigkeit verwendet werden. • USE_CAIRO aktiviert werden, sodass das cairo-Paket zur Visualisierung bereit- steht. Die kompilierte H2Lib-Version, welche in der Schnittstelle verwendet wird, hat all diese Optionen deaktiviert und darüber hinaus hat die Schnittstelle keinen absichtlichen Sup- port für jegliche Optionen. Dies ist auch der Grund, weswegen besagte CDF-Methoden (USE_NETCDF-Option) und weitere nicht zur Verfügung stehen. Außerdem ist die Bi- bliothek für die Schnittstelle als .so-Datei, also als dynamische Bibliothek, kompiliert worden. Dies hat den Hauptgrund, dass das verwendete ctypes-Paket keine statischen Bibliotheken unterstützt. Wie diese Dateien nun in der Schnittstelle eingebunden sind und verfügbar gemacht werden, soll im folgenden Abschnitt erläutert werden. 3.2. Aufbau Das Repository der h2libpy ist aufgebaut wie eine klassisches Python-Package bzw. all- gemeines Software-Repository. Es liegen zunächst ein paar Standarddateien im Wurzelver- zeichnis und es gibt einen misc-Ordner, welcher verschiedene Skripte für die Entwicklung beinhaltet, auf die in Abschnitt 3.5 eingegangen wird. Es bleiben ein examples-Ordner, in dem die besagten Python-Übersetzungen der BEM-Beispielprogramme liegen und der h2libpy-Ordner, wo nun der Code der Schnittstelle zu finden ist. Das Programm benötigt für die Ausführung an sich keine weiteren Pakete. Zentral für die Entwicklung ist das bereits erwähnte Paket ctypes [1, 2], welches allerdings schon in der Standardinstallation von Python vorhanden ist. Neben ctypes gibt es noch andere Möglichkeiten, in Python mit C-Code umzugehen. Die meisten sind aber darauf spezia- lisiert, Python-Code in C zu übersetzen, nicht C in Python einzubinden. Nach einigen Tests mit anderen Paketen wie cython resultierte ctypes als die mit Abstand leichteste und eleganteste Lösung. Für die Entwicklung sind einige weitere Pakete eingebunden, 1 https://www.unidata.ucar.edu/software/netcdf/docs/faq.html
12 3. Die Schnittstelle wie der Linter flake8, der Type Checker mypy, Packaging-Pakete setuptools, wheel und twine, sowie das Refactoring Paket rope. Die Schnittstelle hat gewissermaßen zwei Abstraktionsebenen. In der ersten Ebene werden alle Methoden, Strukturen, Enumerationen und Funktionszeigersignaturen zu- nächst eingebunden und zur Verfügung gestellt. In der zweiten Ebene werden diese dann in sinnvolle OOP-Klassen eingebunden und das nötige Handling mit C-Aspekten wei- testgehend versteckt. Es scheint zunächst abwegig, alles praktisch doppelt zu definieren, allerdings bietet dies einige Vorteile und ergab letztendlich die schönere Lösung, da eine gewisse Zweiteilung ohnehin nicht umgehbar ist. Im Folgenden sollen daher mit den Begriffen Lib-Datei bzw. Lib-Methode / Lib-Klasse jene Inhalte der ersten Ebene gemeint werden, und die zweite Ebene analog die Begriffe Wrapper-Datei bzw. Wrapper-Methode / Wrapper-Klasse erhalten. Um diese Entitäten auch im Code auseinander zu halten, folgen die Lib-Dateien einigen Namenskonventionen wie Präfixe im Typnamen, weiteres dazu in Abschnitt 3.4.1.3. Durch die erste Ebene ist ein inhaltliches Spiegelbild der C-Bibliothek geschaffen. Ist einem also der Umgang mit der H2Lib bekannt, so ist die Verwendung der Lib-Dateien intuitiv. Für Methoden ergibt sich folgendes Szenario: Zum einen wird die Delegation an die Bibliothek benötigt, also ein Objekt, welches die C-Methode verkörpert, und darüber die Konvertierung der Argumente und Rückgabewerte, sodass ein Nutzer Python-Datentypen verwenden kann. Nun hätten diese Lib-Methoden natürlich auch immer erst in den Wrap- per-Methoden definiert können, allerdings stellte es sich als deutlich schöner heraus, alle C-Methoden zentral in einer Lib-Datei zu halten und dann diese gesamte Datei zu importieren. Wie dies genau funktioniert, kann in den einzelnen Modulen später gesehen werden, unter anderem am Beispiel der amatrix.py in Abschnitt 3.4.3.1. Bei den Strukturen bzw. den dazugehörigen Python-Klassen ist es nicht nur schöner, son- dern sogar stückweise notwendig. Eines der größten Probleme wären Importe geworden, da die Datentypen durchaus gegenseitig verwendet werden. Würden also zwei Klassen jeweils die andere nutzen wollen und damit importieren, so würde das Programm wegen zirkulärer Importe nicht ausführbar sein. Daher ist eine Auslagerung der Klassendefini- tion ohnehin notwendig. Wird die Definition aber getrennt vorgenommen, so müssten alle Elemente der Klasse, wie die Methoden, nachträglich hinzugefügt werden. Dies hätte wiederum zur Folge, dass keine Auto Completion genutzt werden könnte und explizit darauf geachtet werden muss, dass die Klasse vollständig gefüllt ist, bevor sie das erste Mal verwendet wird. Nun hätte sich überlegt werden können, ob die jeweilige Lib-Klasse eine Basisklasse der Wrapper-Klasse ist, anstatt dass die Wrapper-Klasse die Aufrufe delegiert, allerdings war letzteres die erste Idee und konnte problemlos umgesetzt werden. Bei dem Ansatz über die Basisklasse kommen ggf. Probleme auf wie das Diamant-Problem, da es also nicht nötig war, wurde in diese Richtung nicht weiter gedacht. Für Enumerationen und Funktionssignaturen ist es dann nur eine Frage der Konsistenz, diese ebenfalls in den Lib-Klassen unterzubringen. Später kann gesehen werden, dass dies im Falle der Enumerationen sogar einen Vorteil bietet, und da die Funktionssignaturen
3.3. Code Style 13 mehrfach verwendet werden, wäre eine lokale Definition dieser nur unnötig redundanter Code. 3.3. Code Style Bevor sich nun die Schnittstelle detailliert angeschaut wird, sollten folgende Informatio- nen zum Code Style gegeben sein. • Da als Linter flake8 verwendet wurde, ist der Code größtenteils PEP8 konform [3]. • Soweit es sinnvoll ist, wurden die Methoden und Parameter mit Typ-Annotationen versehen. Diese sind keineswegs verbindlich für Python, heißen folglich nicht, dass Python durch statisch typisiert ist. Allerdings kann mit entsprechenden Pro- grammen (hier mypy) geprüft werden, ob die Annotationen mit den tatsächlichen Typen übereinstimmen werden, sofern das prüfbar ist. Dies ist eher zum Zweck der Übersicht und des Verständnisses gedacht. • Einige Felder und Methoden sind mit einfachen oder doppelten Unterstrichen eingeleitet. Dies ist ein Indiz für deren Zugänglichkeit, analog zu Schlüsselwörtern wie protected und private in anderen Sprachen. Es wird also angedeutet, dass diese für den internen Gebrauch gedacht sind, was aber wieder nicht heißt, dass es unmöglich ist, sie von außen zu erreichen [3, Naming Conventions]. • Wrapper-Methoden, die mehrere Lib-Methoden gruppieren, benötigen zusätzliche Parameter. Diese sind durch ein Stern-Argument * in der Methodensignatur abge- trennt, sodass diese Parameter immer mit Schlüsselwort gesetzt werden müssen [4]. • Die meisten Wrapper-Methoden sind bisher nicht mit DocString-Kommentaren versehen. Dies kommt daher, dass deren Erklärung bereits vollständig in der C- Bibliothek zu finden ist. 3.4. Paket h2libpy Wie im vorherigen Abschnitt erläutert wurde, hat die h2libpy zwei Abstraktionsschich- ten. Die erste, welche nur die C-Methoden und Definitionen bereitstellt, ist vollständig in h2libpy/lib/ zu finden. Alle anderen Dateien und Ordner im h2libpy/-Verzeichnis tragen zur zweiten Schicht bei und beinhalten die Module, die ein Nutzer hauptsächlich verwenden sollte. Allerdings gibt es Ausnahmen, welche die Nutzung des h2libpy/lib /-Ordners rechtfertigen, wie in Abschnitt 4.3 zu sehen ist. Es ist an dieser Stelle anzumerken, dass jedes Modul, sprich jeder Ordner, eine __init __.py-Datei beinhaltet. Diese trägt generell nur dazu bei, dass Python den jeweiligen Ordner als Modul anerkennt und beim Importieren findet, weswegen diese Datei leer sein kann. Da diese Datei aber bei dem ersten Import des Moduls auch ausgeführt wird, wird in einigen Fällen dieses Verhalten dazu genutzt, um das Importieren für den Nutzer
14 3. Die Schnittstelle einfacher zu gestalten. Konkrete Beispiele dafür können in den nächsten Abschnitten gefunden werden. Sollte eine __init__.py-Datei nicht weiter erwähnt werden, dann ist diese zwar vorhanden, aber leer. Der Ordner h2libpy/ stellt das Wurzelverzeichnis des Codes dar, analog zu einem src-Ordner, und beinhaltet daher nur die vier Submodule lib, base, data und solver. Zusätzlich liegt hier auch die kompilierte Bibliothek als libh2.so. Generell könnte diese Datei in einen separaten Ordner gepackt werden, allerdings muss sie beim Packaging mit inkludiert werden, was nicht ohne Weiteres möglich wäre, wenn sie woanders ab- gelegt wäre. Die besagte __init__.py-Datei ist hier schon damit gefüllt, dass global die C-Bibliothek in einer Instanz gehalten wird und die Initialisierung der Bibliothek vorgenommen wird. 1 # Root folder of this module 2 root = os.path.join(os.path.dirname(__file__), ’..’) 3 4 # Init h2lib and load library instance 5 lib = ctypes.CDLL(os.path.join(root, ’h2libpy/libh2.so’)) 6 init_h2lib = lib.init_h2lib 7 init_h2lib.restype = None 8 init_h2lib.argtypes = [PTR(c_int), PTR(PTR(PTR(c_char)))] 9 init_h2lib(c_int(0), None) Listing 3.1: __init__.py-Datei des Wurzelverzeichnisses Beim ersten Import von h2libpy (oder einer dessen Submodule), wird diese Datei ausge- führt, es wird also eine globale Variable lib existieren, die den Zugriff auf die Bibliothek ermöglicht. Darüber hinaus wird die init_h2lib-Methode definitiert und aufgerufen. Was genau hier passiert, wird in den nächsten Abschnitten verdeutlicht. 3.4.1. Submodul h2libpy/lib/ Das h2libpy/lib/-Modul umfasst Dateien für die erste Abstraktionsebene. Der Inhalt des Ordners ist denkbar einfach strukturiert: • Für jede Datei .h bzw. .c aus der H2Lib existiert in diesem Ver- zeichnis eine Datei namens .py, welche wie erwähnt als Lib-Dateien be- zeichnet werden. Innerhalb dieser Datei finden sich dann die Definitionen der C- Strukturen und der Funktionen, sowie ggf. Signaturen von verwendeten Funkti- onszeigern und C-Enumerationen. • Ein util-Ordner, mit den Dateien helper.py und structs.py, dessen Bedeu- tung gleich genauer erklärt wird. • Die übliche __init__.py-Datei, wobei diese ein import-Statement für jede Datei h2libpy/lib/*.py enthält. Dies ist notwendig, damit die Klassen für die C-
3.4.1. Submodul h2libpy/lib/ 15 struct vollständig definiert werden, da dessen Definition teils in die structs.py verlegt ist, wie gleich gesehen werden kann. Grundsätzlich existiert für jede struct und jedes enum eine Python-Klasse, sowie für jede Methode und jede Signatur von Funktionszeigern eine globale (aufrufbare) Variable. Damit diese Entitäten gekennzeichnet sind, werden in den Lib-Dateien einige Namens- konventionen genutzt. Generell beginnen alle Lib-Klassen für Strukturen mit CStruct und alle Lib-Klassen für Enumerationen mit CEnum. Diese Präfixe helfen auch innerhalb der Schnittstelle dabei, zwischen Lib-Klassen und Wrapper-Klassen zu unterscheiden, da diese sonst den gleichen Namen hätten. Die Variablen für Lib-Methoden haben den gleichen Bezeichner, wie dessen Methode in C. Zu guter Letzt werden Signaturen für Funktionszeiger mit dem Präfix CFunc eingeleitet. 3.4.1.1. Datei: h2libpy/lib/util/structs.py Wie erwähnt, existiert eine Datei h2libpy/lib/util/structs.py, welche vergleich- bar mit einer Header-Datei in C für die Lib-Dateien fungiert. In dieser Datei werden alle später gefüllten Klassen für eine C-struct bzw. ein C-enum leer vordefiniert. Soll also beispielsweise die struct mit dem Namen hmatrix in Python umgesetzt werden, so sind zunächst in dieser Datei folgende Zeilen zu finden: 1 from ctypes import Structure as Struct 2 class CStructHMatrix(Struct): pass Für Enums geht das ganze einen kleinen Schritt weiter, dort werden zusätzlich die Felder des Enums mit Typ-Annotationen versehen, damit der Nutzer der h2libpy Gebrauch von Auto Completion in seinem Editor machen kann. Aus funktionaler Sicht hat dies sonst keinen Mehrwert, egal ob nun die Felder hier schon angedeutet werden oder nicht, da diese Annotationen keiner Deklarationen entsprechen. So sieht nun zum Beispiel die C-Enumeration basisfunction3d mit den Einträgen BASIS_NONE_BEM3D, BASIS _CONSTANT_BEM3D und BASIS_LINEAR_BEM3D in der Datei wie folgt aus: 1 class CEnumBasisFunctionBem3d(c_uint): 2 BASIS_NONE_BEM3D: ’CEnumBasisFunctionBem3d’ 3 BASIS_CONSTANT_BEM3D: ’CEnumBasisFunctionBem3d’ 4 BASIS_LINEAR_BEM3D: ’CEnumBasisFunctionBem3d’ Listing 3.2: Definition CEnumBasisFunctionBem3d Nun könnten die Klassendefinitionen direkt in den einzelnen Dateien stattfinden, aller- dings muss dann gesondert auf zirkuläre Importe geachtet werden. So braucht die Datei uniform.py Zugriff auf die Klasse CStructClusterBasis aus clusterbasis.py, umgekehrt braucht letztere aber auch Zugriff auf CStructUniform aus der ersten Da- tei. Selbst wenn dies noch möglich ist, so müsste genau darauf geachtet werden, wo in der jeweiligen Datei der Import stattfindet, sodass das Programm ausgeführt werden kann. Zusätzlich wird nach den Style-Guidelines von Python nicht empfohlen, Imports
16 3. Die Schnittstelle anderswo als im Kopf einer Datei zu haben [3, Imports]. Hinzu kommt, dass die Klassen ohnehin in Definition und Zuweisung der Felder aufgeteilt werden müssen, da einige Strukturen Felder des eigenen Typs besitzen, was ohne diese Trennung in Python nicht funktionieren würde. Daher ergibt sich diese gesonderte Datei als elegante Lösung für dieses Problem. 3.4.1.2. Datei: h2libpy/lib/util/helper.py Die helper.py ist eine vergleichsweise einfache Datei. In dieser werden nur zwei Funk- tionen definiert, die intern den Zugriff auf die Bibliotheks-Instanz vereinfachen sollen. Zunächst wird die uninit_h2lib-Methode hier bereitgestellt, damit die H2Lib ord- nungsgemäß aufräumen kann. Theoretisch würde diese in einer Datei h2libpy/lib/ basic.py unterkommen, da diese aber ansonsten uninteressant für die Schnittstelle ist, ist diese Funktion praktisch die einzige Ausnahme zum Aufbau der Lib-Dateien. Dazu kommt die get_func-Methode, welche in jeder der Lib-Dateien genutzt wird. Diese vereinfacht die Definition der C-Funktionen in Python dahingehend, dass Funktionsname, Rückgabetyp und Argumenttypen in einem Methodenaufruf gekapselt werden. Sämtliche Datentypen, die dann beim Aufruf dieser Methode genutzt werden, stammen dabei aus dem ctypes-Modul und sind selbsterklärend, ergänzend dazu ein Datentyp POINTER, um Zeiger darzustellen. 1 from h2libpy import lib 2 from typing import Callable, Any, List 3 def get_func(name: str, returntype: Any, argtypes: List[Any]) -> Callable: 4 f = lib.__getattr__(name) 5 f.restype = returntype 6 f.argtypes = argtypes 7 return f Listing 3.3: get_func-Methode Nun existiert zum Beispiel die Methode new_amatrix, welche nach folgender C-Signatur zwei Parameter vom Typ unsigned int als Argumente erwartet und einen Zeiger auf die amatrix-struct zurückgibt. 1 HEADER_PREFIX pamatrix new_amatrix(uint rows, uint cols); In Python kann besagte Methode nun mit benötigten Imports deklariert und dann wie eine normale Methode aufgerufen werden. Diese Definition der Methode lässt sich genauso in der Lib-Datei amatrix.py wiederfinden. 1 from ctypes import POINTER as PTR, c_uint 2 from h2libpy.lib.util.helper import get_func 3 from h2libpy.lib.util.structs import CStructAMatrix 4 new_amatrix = get_func(’new_amatrix’, PTR(CStructAMatrix), [c_uint, c_uint Ç ])
3.4.1. Submodul h2libpy/lib/ 17 5 # ... 6 a = new_amatrix(c_uint(4), c_uint(3)) 3.4.1.3. Dateien: h2libpy/lib/*.py Die Lib-Dateien sind prinzipiell das Grundgerüst der Schnittstelle. Innerhalb einer Datei .py befinden sich alle Funktionsdefinitionen aus der analogen .c der Bibliothek, sowie den konkreten Aufbau der struct, Funktionszeigersignaturen und enum-Werte. Eine Ausnahme bildet die Datei settings.py, welche nur drei Datentypen real, field und longindex definiert, so wie es auch in der korrespondierenden C-Datei passiert. Ansonsten hat eine Lib-Datei folgenden Aufbau: 1 ... Imports ... 2 # ------------------------------------ 3 ... Signaturen von Funktionszeigern ... 4 ... Enum-Werte ... 5 # ------------------------------------ 6 ... Strukturen ... 7 # ------------------------------------ 8 ... Funktionsdeklarationen ... Listing 3.4: Aufbau Lib-Datei In den Imports werden nun zunächst die benötigten Datentypen aus ctypes geladen, sowie die get_func-Methode aus helper.py und die ganzen verwendeten Struktur-, und Enum-Klassen aus der structs.py-Datei. Signaturen der Funktionszeiger werden analog zu den Zuweisungen der Lib-Methoden definiert, mit dem ctypes-Datentyp CFUNCTYPE. Gebraucht werden sie dann bei den Feldern der Struktur-Klassen, als Ar- gumenttypen bei den Funktionen und bei der Umwandlung von Callbacks. Die Enum- Werte werden als einzelne Attribute der jeweiligen Enum-Klasse hinzugefügt. Bei den Struktur-Klassen wird nun jeweils das von ctypes erwartete _fields_-Array gefüllt, und zwar in der gleichen Reihenfolge wie in C, damit eine C-struct im Speicher richtig gelesen werden kann. Abschließend kommen dann alle Funktionsdeklarationen in dem Format, wie es schon bei der helper.py-Datei gezeigt wurde. Ein paar Beispiele zu den jeweiligen Abschnitten einer Lib-Datei folgen hier, ein großer Ausschnitt der Lib-Datei bem3d.py kann im Anhang A.1 gefunden werden. 1 # Funktionszeiger in block.py 2 CFuncAdmissible = CFUNCTYPE(c_bool, *(PTR(CStructCluster), PTR( Ç CStructCluster), c_void_p)) 3 4 # Enum-Felder in bem3d.py 5 CEnumBasisFunctionBem3d.BASIS_NONE_BEM3D = CEnumBasisFunctionBem3d(0) 6 CEnumBasisFunctionBem3d.BASIS_CONSTANT_BEM3D = CEnumBasisFunctionBem3d(ord Ç (’c’))
18 3. Die Schnittstelle 7 CEnumBasisFunctionBem3d.BASIS_LINEAR_BEM3D = CEnumBasisFunctionBem3d(ord(’ Ç l’)) 8 9 # Strukturdefinition in realavector.py 10 CStructRealAVector._fields_ = [ 11 (’v’, PTR(real)), 12 (’dim’, c_uint), 13 (’owner’, c_void_p) 14 ] 15 16 # Funktionsdeklaration in krylovsolvers.py 17 solve_pcg_avector = get_func(’solve_pcg_avector’, c_uint, [c_void_p, Ç CFuncAddevalT, CFuncPrcdT, c_void_p, PTR(CStructAVector), PTR( Ç CStructAVector), real, c_uint]) 3.4.2. Submodul h2libpy/base/ Das h2libpy/base/-Modul ist für Funktionalitäten der Schnittstelle gedacht, welche von den Wrapper-Klassen und ggf. auch vom Nutzer verwendet werden. 3.4.2.1. Datei: h2libpy/base/util.py Die util.py-Datei stellt zunächst einige Methoden bereit, um C-Datentypen zu hand- haben. • deref(ptr) löst eine Referenz auf und gibt das Objekt dahinter zurück, analog zum *-Operator auf Zeigern in C. • cptr_to_list(ptr, length: int) konvertiert einen Zeiger zu einer Liste in Python. Dazu muss offensichtlich die Länge der Liste bekannt sein, da diese nicht im Speicher hinterlegt ist. • carray_to_tuple(carray) wandelt einen Array-Zeiger um, diesmal allerdings zu einem Tupel. Hier wird keine Liste genommen, da Arrays eher für Auflistun- gen fester Länge stehen, die Konvertierung ist im Nachhinein natürlich immer noch möglich. Zudem ist bei Arrays die Länge bekannt, da diese schon in einer der Signaturen der Lib-Methoden hinterlegt ist, also muss diese nicht angegeben werden. • pylist_to_ptr(lst, ctype) erstellt einen Zeiger eines gegebenen Datentyps zu einer Liste, sodass dieser an die C-Methoden weitergegeben werden kann. • get_address(obj, ctype=None) erstellt ein C-Objekt des gegebenen Python- Objekts und gibt einen Pointer dessen zurück. Falls das Python-Objekt nicht einem einfachen Datentyp entspricht, muss der gewünschte C-Datentyp mit übergeben werden.
3.4.2. Submodul h2libpy/base/ 19 • try_wrap(cobj, wrapperclass) gibt eine Instanz der Wrapper-Klasse mit dem gegebenen C-Objekt zurück. Mit dieser Methode wird sichergestellt, dass keine Wrapper-Instanz mit einem nicht-existierenden C-Objekt erstellt wird, sondern dann None genutzt wird. • to_enum(obj, enum) konvertiert einen Lib-Enum-Wert in den passenden Wert der entsprechenden Python-Enum-Klasse. 3.4.2.2. Datei: h2libpy/base/structwrapper.py In der structwrapper.py-Datei wird nun eine Klasse StructWrapper definiert, wel- che die Basisklasse aller Wrapper-Klassen ist. Diese Basisklasse stellt zunächst einige Methoden bereit, die jede Wrapper-Klasse haben sollte. Die grundlegende Idee einer Wrapper-Klasse ist, das jeweilige C-Objekt als Instanz in einer Klassenvariable zu halten und jegliche Anfragen mit diesem Objekt umzusetzen. • cobj(self) gibt das tatsächliche C-Objekt hinter dem gewrappten C-Zeiger zu- rück. • as_voidp(self) konvertiert die Wrapper-Klasse in ein void*, wie es von einigen Methoden der Bibliothek gebraucht wird. • avail_fields(self) liefert eine Liste der Namen aller Felder der Struktur. Diese Methoden sind, obwohl dies der eigentliche Zweck einer Basisklasse ist, eher zweit- rangig. Wichtiger sind die »Magic Methods« __init_subclass__ sowie __getattr__ und __setattr__ und die Methode delete. Erstere wird genutzt, um einen Konstruktor und Destruktor für alle Wrapper-Klassen zu setzen. Auch wenn die meisten C-Strukturen in der Bibliothek eine passende Methode new_() haben, so wird diese hier nicht auf den Konstruktor in der Wrapper- Klasse übersetzt, sondern auf entsprechende Klassenmethoden, wie in den einzelnen Wrapper-Klassen dann gesehen werden kann. Dies hat zwei Gründe: Die Idee einer Wrapper-Klasse ist, wie der Name schon sagt, einen Wrapper für ein C-Objekt darzustel- len. Da dies somit auch Voraussetzung für jegliche Nutzung der Klasse ist, bietet sich das Wrappen als Konstruktor gut an. Zum anderen haben einige Strukturen mehrere Methoden zur Erzeugung und Python erlaubt keine Überladung des Konstruktors. Es wird also ein Konstruktor in den Subklassen gebraucht, der eine Instanz des C-Objekts annehmen kann. Leider kann hier nicht einfach ein Konstruktor in der Basisklasse defi- niert werden, weil in Python beim Initialisieren einer Subklasse nicht der Konstruktor der Oberklasse automatisch aufgerufen wird. Somit müsste jede Wrapper-Klasse einen Kon- struktor definieren, der wiederum den Konstruktor der StructWrapper-Klasse aufruft. Die __init_subclass__-Methode bietet nun die Möglichkeit, den Zeitpunkt abzufan- gen, wo eine Subklasse definiert wird. Sprich diese Methode wird einmalig aufgerufen, wenn der Python-Interpreter eine Klassendefinition findet, die von StructWrapper erbt, und nicht jedes Mal, wenn eine Instanz dieser Klasse erstellt wird. Dies erlaubt, der neuen
20 3. Die Schnittstelle Subklasse (im Parameter cls) bspw. Methoden hinzuzufügen, wie in diesem Fall einen Konstruktor und einen Destruktor. Ist einem Python unbekannt, so scheint dies mögli- cherweise etwas abstrus zu sein, allerdings gilt in Python die Devise »Everything Is An Object«. Das bedeutet, dass wirklich alles, eingeschlossen bspw. auch Nummernliterale und Klassendefinitionen wie oben Attribute haben, welche gesetzt, gelesen und verändert werden können. Methoden sind damit letztendlich nur Attribute einer Klassendefinition. Der Konstruktor der Subklasse hat verschiedene Aufgaben. Zunächst soll geprüft werden, ob die übergebene Lib-Klassen-Instanz dem Typ entspricht, den die Wrapper-Klasse abstrahieren soll. Dieser Typ muss dann auch bei der Definition der Subklasse bei der Vererbung angegeben werden. Darüber hinaus muss die Instanz offensichtlich in einer Klassenvariable festgehalten werden, damit diese immer verfügbar ist. Nun gibt es noch das Szenario, dass zwei oder mehrere Wrapper-Instanzen die gleiche C-Instanz wrappen, das heißt, die C-Instanz darf erst gelöscht werden, wenn alle Wrapper keine Referenz mehr darauf halten, nicht direkt bei dem ersten. Um dies zu realisieren, gibt es eine Art Reference Counting, was bedeutet, dass nicht nur ein Konstruktor hinzugefügt werden muss, sondern auch besagter Destruktor, der den Counter wieder senkt. Es ergibt sich folgender Code für die __init_subclass__-Methode. 1 _objs: Dict[Any, int] = defaultdict(lambda: 0) 2 3 def __init_subclass__(cls, *, cstruct): 4 def _new_init(self, cobj, refs=[]): 5 assert isinstance(cobj, POINTER(cstruct)) 6 self._as_parameter_ = cobj 7 self._refs = refs 8 StructWrapper._objs[ptr_address(cobj)] += 1 9 old_init(self) 10 11 def _new_del(self): 12 StructWrapper._objs[ptr_address(self._as_parameter_)] -= 1 13 14 old_init = cls.__init__ 15 cls.__init__ = _new_init 16 cls.__del__ = _new_del 17 return super().__init_subclass__() Listing 3.5: __init_subclass__-Methode in StructWrapper Innerhalb der __init_subclass__-Methode wird also eine Methode _new_init de- finiert, welche später als Konstruktor __init__ der Subklasse gesetzt wird. Analog eine Methode _new_del, welche als Destruktor __del__ der Subklasse gesetzt wird. Der alte Konstruktor wird durch old_init mit eingebunden und am Ende des neuen Konstruktors ausgeführt, sodass dieser nicht überschrieben wird. Im Konstruktor wird zunächst mittels einer Assertion sichergestellt, dass das C-Objekt vom richtigen Typ ist, und im _as_parameter_-Attribut der Klasse gespeichert. Dieses
3.4.2. Submodul h2libpy/base/ 21 Attribut ist besonders für ctypes, da dann eine Instanz der Wrapper-Klasse direkt als Parameter für eine C-Methode genutzt werden kann, ohne explizit das C-Objekt anzu- geben. Zum Beispiel existiert die Lib-Methode getsize_amatrix, die als Parameter einen Zeiger auf ein Objekt vom Typ CStructAMatrix erwartet. Ist nun x eine Instanz der Wrapper-Klasse von CStructAMatrix, so kann die Methode wie folgt angerufen werden. 1 # getsize_amatrix = get_func(’getsize_amatrix’, c_size_t, [PTR( Ç CStructAMatrix)]) 2 getsize_amatrix(x) # funktioniert bereits 3 getsize_amatrix(x._as_parameter_) # nicht notwendig Zusätzlich hat der neue Konstruktor der Subklasse einen optionalen Parameter refs. Dieser wird nirgends genutzt, sondern soll nur sicherstellen, dass Speicher, der vom C- Objekt benötigt wird, nicht vom GC freigegeben wird, solange der Wrapper noch existiert. So können bspw. Vektoren anhand einer Liste von Koeffizienten erstellt werden, Python würde aber den Speicher der Liste freigeben, wenn diese nicht explizit im Wrapper hin- terlegt würde. Die Verwendung von _refs kann im Beispiel 3.4.3.1 gesehen werden. Darüber hinaus ist sowohl im Konstruktor als auch im Destruktor die Implementation des Reference Counting zu sehen. Zunächst hält die StructWrapper-Klasse eine Klas- senvariable _objs, welche eine HashMap ist, also praktisch ein Wörterbuch. Da leider die C-Objekte nicht direkt »hashbar« sind, wird als Schlüssel die Adresse des Zeigers verwendet. In Zukunft kann sich überlegt werden, ob ein besserer Schlüssel gefunden werden kann, bspw. ein Hash-Wert über alle Felder, falls es vonnöten sein sollte. Nun wird im Konstruktor im Wörterbuch der Eintrag für das aktuelle C-Objekt inkrementiert und im Destruktor wieder dekrementiert. Der Wert hinter einem Schlüssel bezeichnet also die Anzahl an Referenzen auf den jeweiligen Zeiger. Dieser Wert wird dann in der delete-Methode genutzt, welche gleich folgt. Zunächst aber die anderen beiden Magic Methods __getattr__ und __setattr_ _. Fast alle Lib-Klassen haben Felder, dessen Werte von Nutzen sein können. Ist nun wieder x eine Instanz einer Wrapper-Klasse und ein Feld y der dazugehörigen Lib-Klasse, so kann bereits mittels x.cobj().y auf das Feld zugegriffen werden. Dies sieht aber etwas komplizierter aus, als es sein sollte, und außerdem wird dieser Aufruf einen C- Datentyp zurückgeben, welcher damit noch interpretiert werden muss. Deshalb sind in den einzelnen Wrapper-Klassen Getter-Methoden zu finden, die diesen Datentyp sinnvoll auflösen. Damit aber etwas wie x.get_y() nicht geschrieben werden muss, sondern direkt x.y genutzt werden kann, wird die __getattr__-Methode gebraucht. Wann diese Methode intern genau aufgerufen wird, ist etwas komplexer, aber generell gilt, ein Aufruf erfolgt, wenn ein Attribut der Klasse nicht gefunden werden kann. So hat jede Wrapper-Klasse das erwähnte Attribut _refs, hier wird diese also nicht aufgerufen. Da aber alle Felder der Lib-Klasse keine Attribute der Wrapper-Klasse sind, findet ein Aufruf statt, welcher dann wiederum an die spezielle Getter-Methode weitergeleitet wird. Die __getattr__-Methode sieht nun wie folgt aus.
22 3. Die Schnittstelle 1 def __getattr__(self, name: str): 2 # C struct doesnt have this field 3 if name not in self.avail_fields(): 4 error = f"’{self.__class__.__name__}’ has no attribute ’{name}’" 5 raise AttributeError(error) 6 7 # Corresponding method for field is not implemented in this class 8 getter = f’_{self.__class__.__name__}__getter_{name}’ 9 if getter not in dir(self.__class__): 10 raise AttributeError(f"’{self.__class__.__name__}’ has no getter Ç for attribute ’{name}’") 11 12 # Return value 13 return getattr(self, getter)() Listing 3.6: __getattr_-Methode in StructWrapper Die Getter-Methode für das Feld y sollte also die Signatur __getter_y(self) tragen. Die doppelten Unterstriche am Anfang dienen als Indiz für eine private Methode, sodass diese nicht unnötig von Außen aufgerufen wird, wie schon im Abschnitt 3.3 erklärt wurde. Damit nicht während der Laufzeit ein Attribut mit dem Namen eines Feldes der Klasse hinzugefügt wird, was in Python tatsächlich möglich ist, wird die __setattr__- Methode verwendet, um solche Zuweisungen abzufangen. Wird nun x.y = 42 geschrie- ben, wird ein Fehler geworfen, da ansonsten beim weiteren Nutzen von x.y immer 42 zurückgegeben würde, anstatt die Getter-Methode aufzurufen. 1 def __setattr__(self, name: str, value: Any): 2 if name not in [’_as_parameter_’, ’_refs’]: 3 if name in self.avail_fields(): 4 raise AttributeError("Can’t set fields of C struct.") 5 super().__setattr__(name, value) Listing 3.7: __setattr_-Methode in StructWrapper Abschließend noch eine kurze Erläuterung der Methode delete. Jede Lib-Klasse hat eine passende Methode del_..., welche als Parameter das Objekt erwartet und dessen Speicher freigibt. Da dies allgemein gültig ist, kann dies in die Basisklasse verlagert werden, die Wrapper-Klassen müssen nur die richtige Lib-Methode übergeben. Diese ist dann wie folgt umgesetzt: 1 # StructWrapper 2 def delete(self, del_func): 3 if del_func: 4 if StructWrapper._objs[ptr_address(self._as_parameter_)] == 1: 5 del_func(self) 6 self.__del__() 7
3.4.3. Submodul h2libpy/data/ 23 8 # AMatrix Subklasse 9 def delete(self) -> None: 10 super().delete(libamatrix.del_amatrix) Listing 3.8: delete-Methode in StructWrapper Die Subklasse delegiert folglich nur an die Basisklasse. Diese wiederum prüft, ob eine Methode gegeben ist, und ruft diese nur auf, wenn die Anzahl der übrig gebliebenen Referenzen gleich Eins ist, also die eigene Instanz die letzte ist. Abschließend wird der Destruktor noch mit aufgerufen, um den Zähler auf diese Referenz zu dekrementieren, da diese Wrapper-Instanz nun nicht weiter verwendet werden sollte. Leider kann diese Methode nicht direkt als Destruktor verwendet werden, da dieser auch automatisch von Pythons GC aufgerufen wird. Dann werden die C-Objekte ggf. in einer unvorteilhaften Reihenfolge freigegeben, sodass es zu fehlschlagenden assert-Anweisungen in der H2Lib kommen kann. Die vollständige Datei kann im Anhang B gefunden werden, und um bei dem Beispiel der amatrix-struct zu bleiben, folgt nun die Klassendefinition des dazugehörigen Wrappers, wo nochmal die Vererbung mit dem Parameter cstruct für die Assertion gesehen werden kann. 1 import h2libpy.lib.amatrix as libamatrix 2 from h2libpy.base.structwrapper import StructWrapper 3 4 class AMatrix(StructWrapper, cstruct=libamatrix.CStructAMatrix): 5 # ... 3.4.3. Submodul h2libpy/data/ Das data-Modul umfasst alle Datentypen, ergo Subklassen von StructWrapper, die in Python zur Verfügung stehen sollen. Dabei sind diese grob kategorisiert bzw. gruppiert, und zwar in die Submodule geometry, matrix, misc, problem und vector. Es sei an dieser Stelle angemerkt, dass die Namen der einzelnen Dateien zwar noch mit den Lib- Dateien bzw. C-Dateien gleich seien mögen, aber inhaltlich nun etwas weiter abweichen können bzw. die Methoden dann in anderen Klassen wiederzufinden sind. Das ist meistens dadurch begründet, dass die Methoden in Python praktisch zu einem Objekt gehören, und dies nicht immer mit der Ursprungsdatei der Methode übereinstimmt. So ist bspw. die Lib-Datei bem3d.py aufgeteilt auf mehrere Wrapper-Dateien, da sie mehrere Lib- Klassen beinhaltet und sämtliche addeval-Methoden befinden sich in der AVector- Wrapper-Klasse, da dieser immer als Ziel überschrieben wird. Alle Dateien haben ziemlich denselben Aufbau, daher soll zunächst eine generische Datei betrachtet werden.
24 3. Die Schnittstelle 1 ... Allgemeine Imports ... 2 ... Wrapper-Klassen Imports ... 3 ... Lib-Klassen Imports ... 4 ... Andere Imports ... 5 6 class (StructWrapper, cstruct=.): 7 # ***** Fields ***** 8 ... Typ-Annotationen der Felder ... 9 10 # ***** Constructors / destructor ***** 11 ... Klassenmethoden zur Objekterzeugung ... 12 13 # ***** Properties ***** 14 ... Getter Methoden für die Felder ... 15 16 # ***** Methods ***** 17 ... Verschiedende Methoden ... 18 19 # ***** Operators ***** 20 ... Magic Methods für Operatoren ... Listing 3.9: Aufbau einer Subklasse von StructWrapper Am Anfang stehen wie üblich sämtliche Imports für diese Datei. Es kann hier etwas unterschieden werden, was genau gebraucht wird: • Allgemeine Imports sind zunächst Elemente von ctypes sowie ggf. Typen aus typing für die Annotationen. • Wrapper-Klassen Imports sind alle Imports aus h2libpy.data.*, falls also andere Datentypen verwendet werden. Die Datentypen werden nicht explizit mittels from ... import ... geladen, sondern als gesamtes Paket à la import ... as ..., wieder aus dem Grund, um zirkuläre Imports zu vermeiden. • Lib-Klassen Imports sind dann die einzelnen Dateien aus h2libpy.lib.*, eben- falls im Format import ... as ..., hier allerdings eher aus Gründen der Über- sicht. Damit keine Verwechselungen aufkommen, wird ein Import mit einem Alias versehen, der mit lib beginnt. • Andere Imports sind dann unter anderem der StructWrapper sowie ggf. Metho- den aus h2libpy.base.util oder sonstiges. Die Klassendefinition ist wohl selbsterklärend, wobei die Klasse dann von StructWrapper erbt und als Parameter die entsprechende Lib-Klasse angibt. Es folgen nun bis zu fünf Abschnitte in der Klassendefinition, je nach Umfang der Klasse. Zunächst werden alle Felder, die in der Klasse durch Getter-Methoden zur Verfügung stehen, mit Typ-Annotationen versehen. Dies hat wie bei den Enum-Klassen in den
3.4.3. Submodul h2libpy/data/ 25 Lib-Dateien keinen inhaltlichen Wert (es entspricht auch keiner Deklaration, der Interpreter ignoriert diese Zeilen), allerdings wissen so Editoren mit Auto Completion, dass ein Aufruf . funktioniert. Diese Typ-Annotationen haben auch keine Zusicherung, dass tatsächlich diese Typen verwendet werden, sie dienen eher als Dokumentation. Dies gilt nicht nur hier, sondern generell in Methodensignaturen und anderen Situationen. Darauf folgen die Konstruktoren, wobei in Wirklichkeit hier Klassenmethoden stehen, welche als Rückgabetyp die eigene Klasse haben. In den meisten Fällen existiert eine Methode new mit entsprechenden Parametern, sodass eine Instanziierung mittels x = .new(...) funktioniert. Es können aber natürlich auch mehrere Methoden hier bereitstehen, je nach dem, was die Bibliothek anbietet. Im Abschnitt »Properties« sind dann Getter-Methoden für die Felder der Strukturklasse zu finden. Wie schon bei der Klasse StructWrapper erklärt worden ist, werden diese Methoden dann in __getattr__ genutzt, um die Felder verfügbar zu machen, als wären sie Attribute der Klasse und keine Methoden. Die meisten Getter-Methoden sind auch Einzeiler, da nur das Feld des Lib-Objekts konvertiert werden muss. Abschließend kommen dann alle möglichen Methoden, die dieser Klasse zugehörig sind, ebenfalls abhängig davon, was die Bibliothek anbietet. Darunter ist immer eine Methode delete zu finden, wie schon in im Abschnitt 3.4.2.2 erklärt wurde. In ein paar Dateien gibt es dann noch einen letzten Teil für Operatoren, falls dies überhaupt Sinn ergeben sollte. Aber so bieten zum Beispiel (normal besetzte) Matrizen die Möglichkeit, mittels des +-Operators addiert zu werden, oder Vektoren mit einer Indizierung wie bei Arrays auf dessen Elemente zuzugreifen. Das Ganze ist aber eher eine Demonstration, dass dies generell möglich ist, als ein realer Anwendungsfall, da die Arithmetik mit Operatoren gegenüber der addeval-Methoden einen Overhead zur Folge hat, da jedes Mal neue Objekte erzeugt werden müssen. Inhaltlich bieten die einzelnen Methoden selten etwas Besonderes und beschränken sich auf einige Zeilen Quellcode. In fast allen Fällen werden zum einen Typen umgewandelt, entweder von C nach Python oder umgekehrt, je nachdem ob es Argumente oder Rückgabewerte sind. Zum anderen wird Pythons Fähigkeit von dynamischer Typisierung und optionalen Parametern genutzt, um ähnliche C-Methoden vereinfacht in Python darzustellen bzw. zusammenzufassen. Innerhalb der Submodule befinden sich neben den Dateien für die StructWrapper logischerweise eine __init__.py-Datei sowie unter Umständen eine _enums.py-Datei. Letztere beinhaltet Enumerationen, die innerhalb der Klassen des Submoduls genutzt werden, um ähnliche C-Methoden in Python in einer Methode zusammenzufassen. So gibt es bspw. für Matrizen eine Enumeration ClearType mit den Werten All, Lower, LowerStrict, Upper und UpperStrict, sodass nur jeweils eine clear-Methode existieren muss und ein Wert dieser Aufzählung dann als Parameter mit überreicht wird. Die __init__.py-Datei importiert hier wieder alle Klassen, inklusive der eben genannten Enumerationen, damit diese direkt aus h2libpy.data. importierbar sind.
26 3. Die Schnittstelle Da alle Klassen mehr oder weniger gleich aussehen, soll die Datei h2libpy/data/ matrix/amatrix.py als Beispiel herangezogen werden, um die einzelnen Bestandteile genauer zu erläutern. 3.4.3.1. Beispiel: h2libpy/data/matrix/amatrix.py Die Datei amatrix.py beginnt mit folgenden Imports: 1 from ctypes import c_uint 2 from typing import List, Union 3 4 import h2libpy.data.matrix as mat 5 import h2libpy.data.misc as misc 6 import h2libpy.data.vector as vec 7 import h2libpy.lib.amatrix as libamatrix 8 import h2libpy.lib.clusterbasis as libclusterbasis 9 import h2libpy.lib.h2matrix as libh2matrix 10 import h2libpy.lib.sparsematrix as libsparsematrix 11 from h2libpy.base.structwrapper import StructWrapper 12 from h2libpy.base.util import (cptr_to_list, pylist_to_ptr, try_wrap, Ç verify_type) 13 from h2libpy.lib.settings import field Listing 3.10: Imports Wrapper-Klasse AMatrix Es wird zunächst der c_uint-Typ zur Konvertierung von Werten benötigt, sowie List und Union für Typ-Annotationen. Drei Submodule matrix, misc und vector von data werden ebenfalls importiert, weil dessen Typen innerhalb der Klasse vorkommen. Von den Lib-Klassen werden gleich vier eingebunden, das heißt, dass letztendlich Methoden aus vier verschiedenen C-Dateien hier einen Platz gefunden haben. Abschließend dann drei Zeilen für verschiedene Methoden und Definitionen, die hier gebraucht werden. Als Nächstes folgt die Kopfzeile der Klasse sowie die Typ-Annotationen für die bekannten Felder. 1 class AMatrix(StructWrapper, cstruct=libamatrix.CStructAMatrix): 2 # ***** Fields ***** 3 a: List[List[float]] 4 ld: int 5 rows: int 6 cols: int Listing 3.11: Definition Wrapper-Klasse AMatrix Die Klasse AMatrix bietet die Felder a, ld, rows, cols mit den entsprechenden Typen an, die später durch Getter-Methoden zusammen mit der __getattr__-Methode aus der Basisklasse implementiert werden. Das Feld owner, welche in der C-struct noch definiert ist, ist absichtlich weggelassen, da dessen Datentyp void* zu allgemein ist,
Sie können auch lesen