Module

Inhalt

Das fünfte Kapitel befasst sich mit dem Modulsystem von Python sowie der Sichtbarkeit von Variablen und deckt die folgenden Konzepte ab:

  • Module

  • Gültigkeit und Sichtbarkeit von Variablen

  • Namespaces und Gültigkeit

  • Scopes

  • Packages

  • Modulpfad

Module

Für das Arbeiten der Module müssen wir uns zuerst direkt mit dem Interpreter beschäftigen. Danach könnten wir PyCharm nutzen. Was wir bisher über den Interpreter gelernt haben ist:

  • Der Interpreter kann Befehle interaktiv ausführen.

  • Man kann auch ganze Dateien als Übergabeparameter ausführen.

  • Im Hintergrund von Jupyter Notebooks läuft ein Interpreter, so dass sich alle Zellen den gleichen Interpreter teilen.

  • Zugewiesene Variablen bleiben bis zum Beenden des Interpreters gültig.

Was wir bisher noch nicht haben, ist ein Konzept für die Wiederverwendung von Funktionen. Zur Wiederholung hier die Funktion zum Berechnen der Quadratwurzel aus Kapitel 4.

my_sqrt.py
==========
def my_sqrt(x):
   """Returns the square root of x"""
   guess = 1
   while(abs(guess*guess-x)>0.0001):
       guess = (1/2)*(guess+x/guess)
   return guess


Mit unserem bisherigen Wissen müsste man diese Funktion immer direkt im Interpreter ausführen, bevor man Sie benutzen kann. Auch wenn das mit einer einzigen Funktion noch praktikabel erscheint, skaliert dies nicht mehr, wenn viele Funktionen oder Klassen wiederverwendet werden sollen. Die Dateien würden schnell riesig werden. Für die Wartbarkeit wäre dieses auf Copy & Paste basierende Programmieren auch ein Alptraum. Stellen Sie sich vor, Sie verwenden die Wurzelfunktion in 100 Programmen. Um etwas an dieser Funktion zu ändern, müssten Sie diese in allen 100 Programmen ändern. Das ist weder handhabbar, noch sinnvoll. An dieser Stelle treten die Module auf den Plan.

Mit Hilfe von Modulen ist es möglich Funktionen in Dateien zu definieren, und in anderen Pythonskripten zu importieren. Hierzu muss man zuerst eine Datei anlegen, welche die zu importierenden Funktionen zur Verfügung stellt.

Nehmen wir an, dass die oben genannte Datei "my_sqrt.py" an folgendem Ort gespeichert wurde: ~/python_examples/my_fonctions/my_sqrt.py

Den Inhalt dieser Datei kann man jetzt als Modul in anderen Dateien laden. Damit dies im PyCharm Reibungslos funktioniert, muss zunächst die folgende Zelle ausgeführt werden. Der Inhalt dieser Zelle wird am Ende dieses Kapitels erklärt.

import os
import sys
sys.path.append(os.getcwd()+"/python_examples/my_fonctions")


Um die my_sqrt Funktion jetzt in anderen Dateien benutzen zu können, muss man Sie importieren. Hierzu gibt es in Python das Schlüsselwort import.

test_module.py
==============
import sqrt      # executes the sqrt.py file in the current interpreter to load the my_sqrt function

sqrt.my_sqrt(42) # uses the function my_sqrt from the sqrt module
Ausführung vom Programm
>>> python3 test_module.py
6.480740727643494

Der Aufruf der my_sqrt Funktion wurde mit dem Namen des Moduls sqrt eingeleitet, wobei der Funktionsname und der Modulname von einem . getrennt sind. Um zu verstehen, warum das Nötig ist, müssen wir uns zuerst mit der Sichtbarkeiten von Variablen befassen.

Gültigkeit und Sichtbarkeit von Variablen

(In diesem Abschnitt bezeichnen wir alles, was einen Namen bekommen kann, als Variablen, also auch Funktionen und Klassen.)

Ein wichtiges Thema, welches wir bisher vernachlästigt haben, ist die Sichtbarkeit von Variablen. Bisher haben wir immer so getan, als ob auf jede Variable immer und überall einfach über ihren Namen zugegriffen werden kann. Das dies nicht Stimmt, zeigt das folgende Beispiel.

sichtbarkeit.py
===============
my_variable = "Outside"

def func():
    my_variable = "Inside"
    print(f"my_variable in func: {my_variable}")
    
func()
print(f"my_variable outside of func: {my_variable}")
Ausführung vom Programm
>>> python3 sichtbarkeit.py
my_variable in func: Inside
my_variable outside of func: Outside

Die Variable my_variable wird innerhalb der Funktion neu definiert. Dennoch behält die Variable außerhalb der Funktion den gleichen Wert. Der Grund für dieses Verhalten sind Namespaces und die damit zusammenhängenden Scoping Rules. Vereinfacht gesagt, hat die Funktion func ihren eigenen Namespace und die Scoping Rules legen fest, dass innerhalb der Funktion eine zweite Variable my_variable erstellt wird, weshalb die außerhalb der Funktion definierte Variable my_variable nicht verändert wird.

Namespaces und Gültigkeit

Um die Interaktionen zwischen Variablen mit dem gleichen Namen zu verstehen, muss man zuerst die Namespaces verstehen. Grundsätzlich gibt es drei Arten von Namespaces in Python:

  1. built-in
    Dieser Namespace ist jederzeit gültig und verfügbar. Er beinhaltet die von Python zur Verfügung gestellten Sprachkonstrukte, zum Beispiel die Klassen für die Datentypen list, set, etc.

  2. global
    Er bezieht sich auf ein Modul. Jedes Modul hat seinen eigenen globalen Namespace. Der globale Namespace ist erst dann verfügbar, wenn ein Modul geladen wird und bleibt gültig, solange das Modul geladen bleibt. Dies ist üblicherweise der Fall, bis der Interpreter beendet wird.

  3. local
    Der local Namespace wird für jede Funktion bei ihrem Aufruf erstellt und besteht nur solange, bis die Ausführung der Funktion beendet ist.

Scopes

Auch wenn die Variablen in den einzelnen Namespaces unabhängig voneinander sind, sind es die Namespaces selbst nicht. Stattdessen gibt es eine hierarchische Beziehung. Der built-in Namespace beinhaltet die Namespaces der Module. Die Module beinhaltet die Namespaces der in Ihnen definierten Funktionen. Werden Funktionen innerhalb anderer Funktionen definiert, befindet sich der Namespace innerhalb des lokalen Namespaces der äußeren Funktion. Man spricht in der Python Terminology auch vom innermost Namespace, um den aktuellen innersten Namespace zu beschreiben.

Der Scope sind alle Variablen, auf die man direkt über ihren Namen zugreifen kann. Dies sind nicht nur die Variablen aus dem innermost Namespace, sondern auch alle Variablen aus den umschließenden Namespaces, die nicht von Variablen gleichen Namens aus weiter innenliegenden Namespaces "überdeckt" werden. Um den Scope zu bestimmen gilt die Regel das die Namespaces von innen nach außen durchsucht werden. Der Scope ist jedoch nur für den Zugriff auf Variablen wichtig. Definiert man eine Variable, wird diese im aktuellen innermost Namespace zugewiesen.

scope.py
========
outer = "Outer"
# namespaces from outer to inner: built-in, global
# scope: outer + builtins
def func():
    # namespaces from outer to inner: built-in, global, local of func
    # scope: outer (from global namespace), func (from global namespace) + built-ins
    inner = "Inner"
    # Scope: outer (from global namespace), func (from global namespace), inner (from local namespace) + built-ins
    outer = "Inner"
    # Scope: func (from global namespace), inner (from local namespace), outer (from local namespace) + built-ins

# namespaces from outer to inner: built-in, global
# scope: outer (from global namespace), func (from global namespace) + built-ins
func()


Jetzt ist auch klar, was in unserem Beispiel am Anfang passiert ist. my_variable wurde im local Namespace von func definiert. Dadurch ist my_variable aus dem globalen Namespace beim Aufruf von print in func nicht mehr Scope von func und es wird “Inside” ausgegeben.

1. global und nonlocal

Will man eine Variable, die zu einem umschließenden Namespace gehört, zuweisen, geht das nicht ohne weiteres. Wie wir oben gelernt haben, ist diese zwar eventuell im Scope und kann damit gelesen werden. Bei einer Zuweisung würde jedoch automatisch eine neue Variable in aktuellen innermost Namespace angelegt. Dies kann man mit den Schlüsselwörtern global und nonlocal umgehen. Mit global <name> legt man fest, dass die Variable aus dem globalen Namespace sowohl im aktuellen Scope ist und auch das Zuweisungen nicht im aktuellen Namespace stattfinden, sondern im globalen. Analog kann man mit nonlocal auf Variablen einer umschließenden Funktion von einer inneren Funktion zugreifen.

global_und_nonlocal.py
======================
outer = "Outer"

def func():
    global outer
    outer = "Inner"
    
print(outer)
func()
print(outer)
Ausführung vom Programm
>>> python3 global_und_nonlocal.py
Outer
Inner

2. Namespaces von Modulen

Jetzt, da wir Namespaces kennen, ist auch klar, warum der Aufruf unserer Quadratwurzelfunktion sqrt.my_sqrt sein muss und my_sqrt nicht ausreichend ist (Weil jedes Modul seinen eigenen globalen Namespace hat). Die Funktion my_sqrt ist nur im Namespace des sqrt Moduls bekannt. Durch sqrt.my_sqrt gibt man an, dass man auf my_sqrt im Namespace des Moduls sqrt zugreifen möchte.

3. Initialisierung von Modulen

Module werden durch die import Anweisung geladen. Dieses Laden geschieht dadurch, dass die komplette Datei, in der ein Modul definiert ist, vom Interpreter ausgeführt wird. Ein Modul wird nur beim ersten Mal, wenn es importiert wird, initialisiert. Das in der Datei sqrt2.py definierte Modul verdeutlich dies.

~/python_examples/my_fonctions/sqrt2.py
=======================================
print("intializing module sqrt2...")
def my_sqrt(x):
    """Returns the square root of x"""
    guess = 1
    while(abs(guess*guess-x)>0.0001):
        guess = (1/2)*(guess+x/guess)
    return guess

answer = 42

print("initialization finished!")


scope_module.py
===============
import /home/deameni/python_examples/my_fonctions/sqrt2   # executes sqrt2.py and creates module namespace
print(sqrt2.answer)
sqrt2.answer=43
import /home/deameni/python_examples/my_fonctions/sqrt2   # nothing happens, module already imported
print(sqrt2.answer)
Ausführung vom Programm
>>> python3 scope_module.py
intializing module sqrt2...
initialization finished!
42
43

Packages

Durch das Einbinden eines Moduls hat man die Möglichkeit, den Quelltext einer einzelnen anderen Datei einzubinden und auszuführen. Doch häufig gibt es Gründe, Funktionalitäten in verschiedene Dateien aufzuteilen und ggf. sogar innerhalb von unterschiedlichen Ordnern zu organisieren. Hierzu gibt es in Python Packages. Um ein Package zu definieren, muss man einfach verschiedene Module in den selben Ordner packen. Zusätzlich muss der Ordner noch eine Datei __init__.py beinhalten, die einfach leer sein kann. Durch die __init__.py wird aus einem normalen Ordner aus Sicht von Python ein Package. Packages können auch Subpackages enthalten. Dies sind Unterordner, die ebenfalls Module und eine __init__.py beinhalten.

Der Beispielquelltext beinhaltet ein Modul Namens sample_module mit der folgenden Struktur.

sample_package/    # Top-level package
|---  __init__.py  # Initializes the package
|---  sqrt.py      # module on top-level of the package
|---  util/        # subpackage
   |--- io.py      # module in subpackage


Man kann die Module dieses Packages jetzt mit import laden und benutzen.

Import vom Package
>>> import sample_package.sqrt
>>> import sample_package.util.io
>>> print(sample_package.sqrt.my_sqrt(42))
6.480740727643494

>>> sample_package.util.io.print_with_linenumbers("Hello", "from","the","subpackage")
000: Hello
001: from
002: the
003: subpackage

Zugriffe auf Module, insbesondere solche, die in Subpackages definiert sind, können mit import sehr lang werden. Daher kann man Importierte Module mit Hilfe des Schlüsselworts as mit Aliasen versehen, die stattdessen verwendet werden können.

Import vom Package mit as
>>> import sample_package.sqrt as sq
>>> import sample_package.util.io as util
>>> print(sq.my_sqrt(42))
6.480740727643494

>>> util.print_with_linenumbers("Hello", "from","the","subpackage")
000: Hello
001: from
002: the
003: subpackage

1. from ... import

Eventuell will man gar kein ganzen Modul importieren, sondern benötigt nur Teile eines Moduls. Dies wird durch das Schlüsselwort from ermöglicht. Man gibt einfach an, welche Inhalte aus einem Modul oder Package importiert werden sollen. Dies kann sowohl eine Liste von Modulen aus einem Package sein, als auch eine Liste von Variablen/Funktionen aus einem Modul. Der Vorteil hiervon ist, dass man keine Präfixe mehr benötigt, sondern direkt den Namen des Importierten verwenden kann.

Import vom Package mit from
>>> from sample_package import sqrt
>>> from sample_package.util.io import print_with_linenumbers
>>> print(sqrt.my_sqrt(42))
6.480740727643494

>>> print_with_linenumbers("Hello", "from","the","subpackage")
000: Hello
001: from
002: the
003: subpackage

2. Importieren ganzer Packages

Will man alle Module eines Packages laden, kann dies bedeuten, dass man sehr viele import Anweisungen benötigt. Wünschenswert wäre es, mit Hilfe von from package import alle Module eines Packages laden zu können, ggf. oder auch die der Submodule. Dies ist in Python zwar möglich, muss aber vom Package unterstützt werden. Hier kommt die __init__.py Datei ins Spiel. In dieser Datei wird eingetragen, welche Module geladen werden sollen, wenn from package import aufgerufen wird. Hierzu definiert man die Module als Liste in einer Variable __all__.

Importieren ganzer Packages
>>> import os
>>> os.system('cat ~/python_examples/my_fonctions/__init__.py')
__all__ = ["sqrt"]

>>> from sample_package import *
>>> from sample_package.util import *

>>> print(sqrt.my_sqrt(42))
6.480740727643494

>>> io.print_with_linenumbers("Hello", "from","the","subpackage")
000: Hello
001: from
002: the
003: subpackage

3. Lokale Imports

Man kann in Python auch Module im lokalen Namespace einer Funktion laden. Das Modul ist dann auch nur innerhalb dieser Funktion verfügbar.

lokale Imports
>>> def func():
...     import sqrt
...     print(f"the root of the answer is {sqrt.my_sqrt(42)}")
...
>>> func()
the root of the answer is 6.480740727643494

Modulpfad

Kommen wir zum Abschluss dieses Kapitels nochmal zu der nicht erklärten Zelle vom Anfang zurück:

import os
import sys
sys.path.append(os.getcwd()+"/python_examples/my_fonctions")


Die Bedeutung der ersten beiden Zeilen ist jetzt klar: Die Module os und sys werden geladen. Beides sind Module aus der Python Standard Library, einem Paket von Modulen, welches "ab Werk" bei Python verfügbar ist, also nicht extra installiert werden muss. Die dritte Zeile ist etwas schwieriger zu verstehen. Gucken wir uns einmal die einzelnen Teile genauer an.

Pfad
>>> os.getcwd() # fetches the current working directory, i.e., the location of this prompt
'/home/deameni/python_examples/my_fonctions'
>>> sys.path    # a list of paths in the system
['', '/usr/lib/python310.zip', '/usr/lib/python3.10', '/usr/lib/python3.10/lib-dynload',
'/usr/local/lib/python3.10/dist-packages', '/usr/lib/python3/dist-packages',
'/home/deameni/python_examples/my_fonctions']

Wir fügen also der Liste sys.path den Ordner mit dem Beispielquelltext hinzu. Schaut man in der Pythondokumentation die Bedeutung von sys.path nach, findet man heraus, dass dies genau die Orte sind, wo nach Modulen gesucht wird. Jetzt ist auch klar, warum wir den Ordner mit dem Beispielquelltext hinzufügen mussten: Weil wir die Module sonst nicht hätten laden können. Will man also Module benutzen, die nicht in einem der Standardpfade sind, muss man diese dem Modulpfad hinzufügen. Wir haben das hier über den Quelltext gemacht. Man kann den Modulpfad auch über die Umgebungsvariable PYTHONPATH anpassen.