Der Python-Kurs: Module
Willemers Informatik-Ecke
Vererbung Python-Kurs Python

Nicht jedes Programm benötigt alle mathematischen Funktionen, eine grafische Oberfläche, eine Datenbank und Netzwerkfähigkeiten. Damit der Interpreter nicht übermäßig fett wird, werden solche Fähigkeiten nicht im Interpreter realisiert, sondern in Module ausgelagert.

Bei größeren Projekten können Sie als Python-Programmierer auch eigene Module implementieren, um den Code in übersichtliche Teile zu gliedern.

Bibliotheken importieren

Als Beispiel verwenden wir hier die Bibliothek math, die in jeder Python-Installation vorhanden ist. Hier geht es nicht um den Inhalt von math, sondern vor allem darum, wie Sie eine Bibliothek von Ihrem Programm aus nutzen.

Um Funktionen oder Konstanten von math zu verwenden, muss die Bibliothek zunächst importiert werden.

import math
print(math.sin(math.pi/2))
Alternativ können die Elemente auch schon beim Import gerufen werden. Damit müssen die Elemente nicht mehr explizit mit vorangestelltem Bibliotheksnamen aufgerufen werden.
from math import sin, pi
print(sin(pi/2))
Sollte aus der Bibliothek math mehrere Funktionen benötigt werden, kann auch der Stern als Platzhalter für die einzelnen Namen verwendet werden.
from math import *
print(sin(pi/2))
Nun können alle Funktionen der Bibliothek aufgerufen werden. Es ist auch nicht erforderlich, den Bibliotheksnamen davor zu stellen. Allerdings steigt das Risiko der Namensüberschneidungen zwischen den Namen verschiedener Bibliotheken und des eigenen Programms.

Eigene Module

Wenn die Programme wachsen, sollte man sie in verschiedene Quelltextdateien zerlegen. Dann ist es allerdings notwendig, dass der Interpreter weiß, wo er die ausgelagerten Funktionen findet.

Nehmen wir an, wir wollen eine Klasse Bruch in einer eigenen Datei namens bruch.py ablegen.

class Bruch:
    def __init__(self, z, n):
        self.zaehler = z
        self.nenner  = n
    def add(self, b):
        n = self.nenner * b.nenner
        z = self.zaehler*b.nenner + b.zaehler*self.nenner
        return Bruch(z, n)
    def __add__(self, b):
        n = self.nenner * b.nenner
        z = self.zaehler*b.nenner + b.zaehler*self.nenner
        return Bruch(z, n)
    def __str__(self):
        return "{}/{}".format(self.zaehler, self.nenner)

In einer anderen Datei befindet sich Ihr Hauptprogramm, das nun einen Bruch anlegen möchte. Zunächst muss es angeben, dass die Klasse Bruch außer Haus ist, nämlich in der Datei bruch.py. Dies drückt es durch einen import aus.

import bruch

Die Endung .py wird weggelassen. Die Namensgebung ist weiter eingeschränkt. Einerseits sollte abgesehen von der Endung kein weiterer Punkt im Dateinamen sein. Darüber hinaus sollte das Modul auch nicht math.py heißen, sonst ist die Standardbibliothek math verdeckt.

Damit nun die Klasse Bruch verwendet werden kann, muss diese über die importierte Datei referenziert werden.

import bruch
anteil = bruch.Bruch(1,2)
print(anteil)
Will man die Klasse Bruch so verwenden, als gehörten Sie zur lokalen Datei, dann kam man sie explizit importieren.

from bruch import Bruch
anteil = Bruch(1,3)
print(anteil)
Sollen weitere Elemente, etwa Funktionen, aus der Datei bruch.py importiert werden, dann werden sie mit einem Komma getrennt hinten an die Zeile angehängt.

Wenn es sehr viele Elemente sind, kann man einfach alles durch einen Stern importieren.

from bruch import *
anteil = Bruch(1,3)
print(anteil)
Das erspart natürlich Tipparbeit, sollte man aber vermeiden, wenn man sehr viele Importe hat. Irgendwann verliert man den Überblick, welche Ressource in welcher Datei steckt.

Module in Verzeichnissen

Liegen die Dateimodule in Unterverzeichnissen, was bei größeren Projekten nicht ungewöhlich ist, kann man diese ebenfalls verwenden. Als Trennzeichen zwischen Verzeichnis und Datei dient der Punkt. Das ist der Grund, warum man eigene Moduldateien keinen Punkt im Namen haben dürfen.

import unterverzeichnis.bruch
anteil = unterverzeichnis.bruch.Bruch()

Start-Code eines Moduls

Selbstgeschriebene Module enthalten normalerweise Klassen oder Funktionen, die von anderen Modulen verwendet werden. Sie können aber auch normale Befehle hineinschreiben. Dieser wird beim Import des Moduls automatisch ausgeführt.

In manchen Fällen soll das Programm eines Moduls nur dann ausgeführt werden, wenn das Programm als eigenständiges Programm aufgerufen wird. Das kann beispielsweise für das Testen eines Moduls interessant sein. Dann schreiben Sie vor die Befehle der Hauptebene eine Abfrage, ob dieses Modul als das Hauptmodul ausgeführt wird:

if __name__ == '__main__':
    print("Dies ist ein Test des Moduls")

Pakete

Auch Module können zusammengefasst werden. Dazu werden die Module-Dateien in einem Verzeichnis gesammelt. Pakete können ihrerseits Pakete enthalten, die ebenfalls als Unterverzeichnisse umgesetzt werden.

In dem Unterverzeichnis muss eine Datei namens __init__.py existieren. Es sind übrigens zwei Unterstriche vor und hinter dem init. Diese Datei enthält Programmzeilen für eine eventuell notwendige Initialisierung des Pakets, darf aber auch leer sein.

Mit pytest Module testen

Um verlässliche Programme zu schrauben, müssen sie getestet werden und möglichst auch bei jeder Änderung. Zu diesem Zweck können Testskripten gesprieben werden, die das Modul respektive die zu testenden Funktionen aufruft und prüft, ob das gewünschte Ergebnis herauskommt. Diese Prüfung übernimmt das Programm pytest.

Installation von pytest

Die Installation übernimmt das Installationstool von Python pip.
pip install pytest
Unter Linux kann auch das Paket python3-pytest aus dem Repository installiert werden.
# apt install python3-pytest

Start von pytest

Das Programm pytest ruft alle Skripten des Verzeichnisses auf, die als Endung _test.py haben. Diese Skripten rufen ihrerseits die zu prüfenden Funktionen auf und vergleichen deren Ergebnisse mithilfe des Befehls assert mit den zu erwarteten Ergebnisse.

Das folgende Skript hello_test.py ruft die Funktion hello_world aus der Skriptdatei hello.py und prüft, ob diese die Zeichenkette "Hello World!" zurückgibt:

import hello

def test_hello():
    assert hello.hello_world() == "Hello World!"
Die zu prüfende Funktion steht im Skript hello.py der über den import-Befehl oben eingebunden wurde.
def hello_world():
    return "Hello World!"
In dieser Konfiguration würde pytest alles schick finden. Wenn in der Funktion hello_world allerdings der Rückgabe-String geändert würde, würde der Befehl assert unzufrieden und böse, rote Bemerkungen von sich lassen.

Test des Moduls Bruch

Gehen wir weiter. Die Klasse Bruch im Modul bruch soll getestet werden. Also befindet sich in der Datei bruch.py folgender Inhalt:
class Bruch:
    def __init__(self, z, n):
        self.zaehler = z
        self.nenner  = n
    def __add__(self, b):
        n = self.nenner * b.nenner
        z = self.zaehler*b.nenner + b.zaehler*self.nenner
        return Bruch(z, n)
Als Testmodul wird die Datei bruch_test.py angelegt. Diese enthält Testfunktionen für die Klasse Bruch.
from bruch import Bruch

def test_construct(): # teste, ob der Konstruktor funktioniert
    a = Bruch(1,4)
    assert a.zaehler == 1 and a.nenner == 4

def test_add():
    a = Bruch(3,7)
    b = Bruch(1,2)
    c = a + b
    assert c.zaehler == 13 and c.nenner == 14
Wird in dem Verzeichnis der Befehl pytest gestartet, wird geprüft, ob für eine Moduldatei eine gleichnamige mit _test existiert. Das ist bei bruch.py und bruch_test.py der Fall. Also werden alle Funktionen im Testmodul ausgeführt.
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-7.4.2, pluggy-0.13.0
rootdir: /home/arnold/svn/src/python/pytest
collected 2 items                                                              

bruch_test.py ..                                                         [100%]

============================== 2 passed in 0.01s ===============================
In diesem Fall ist der Test fehlerfrei gelaufen.

Nun ändern wir die Testfunktion test_construct:

def test_construct(): # teste, ob der Konstruktor funktioniert
    a = Bruch(1,4)
    assert a.zaehler == 2 and a.nenner == 4
Dann würde der erste Test scheitern, der zweite ginge klar. Das Ergebnis:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-7.4.2, pluggy-0.13.0
rootdir: /home/arnold/svn/src/python/pytest
collected 2 items                                                              

bruch_test.py F.                                                         [100%]

=================================== FAILURES ===================================
________________________________ test_construct ________________________________

    def test_construct():
        a = Bruch(1,4)
>       assert a.zaehler == 2 and a.nenner == 4
E       assert (1 == 2)
E        +  where 1 = .zaehler

bruch_test.py:5: AssertionError
=========================== short test summary info ============================
FAILED bruch_test.py::test_construct - assert (1 == 2)
========================= 1 failed, 1 passed in 0.12s ==========================