Implementierung einer Python-Schnittstelle für das Softwarepaket H2Lib - Uni Kiel

Die Seite wird erstellt Volker Haas
 
WEITER LESEN
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