import random

"""
Explanation: Module
Was sind Module?
Python organisiert Sourcecode in Modulen. Ein Modul ist nichts anderes,
als eine Datei mit der Extension .py.
Module dienen dazu,

große Projekte in mehrere kleinere überschaubare (und logisch
  zusammenhängende) Sourcecode-Dateien zu organisieren
Code besser wiederverwendbar zu machen, da Module selektiv in neuen Code
  importiert werden können

Kleine Skripte kann man ohne weiteres in eine einzige Datei packen.
Sobald ein Programm aber mehr als ein paar hundert Zeilen hat, empfiehlt es
sich, den Code auf mehrere Module aufzuteilen, auch weil der Code dadurch
einfacher zu pflegen und zu testen ist.
Module sind überall
Nicht nur der eigene Programmcode lässt sich in Modulen organisieren, auch
fremder Code wird in Modulen verteilt:

Die Standard-Library von Python ist in Modulen (und Paketen) organisiert
Third-Party-Libraries sind ebenfalls als Module verfügbar

Module müssen importiert werden
Damit ein Modul im eigenen Programm verwendet werden kann, muss das Modul zuerst importiert werden:
~~~
import <Modulname>
~~~
Danach steht das Modul mit der dort definierten Funktionalität zur Verfügung. Das in der mit Python mitinstallierten Standardlibrary vorhandene Modul random stellt eine Reihe von Zufalls-Funktionen bereit. Damit wird diese verwenden können, müssen wir das Modul zuerst importieren:
End of explanation
"""


random.randint(0, 1000000)

"""
Explanation: Danach können wir uns beispielsweise eine Zufallszahl generieren lassen:
End of explanation
"""


import a
import b

a.serialize()
b.serialize()

"""
Explanation: Module und Namensräume
Im letzten Beispiel haben wir nicht irgendeine Funktion mit dem Namen randint verwendet, sondern genau die, die vom Modul random bereit gestellt wird.
Module strukturieren daher nicht nur den Sourcecode, sondern bilden auch Namensräume,
wodurch verhindert wird, dass sich beispielsweise zwei in unterschiedlichen
Modulen definierte, gleichnamige Funktionen gegenseitig überlagern. Um das zu verdeutlichen habe ich im Verzeichnis, in dem dieses Notebook liegt, zwei minimale Module angelegt:

a.py
b.py

In beiden Modulen gibt es eine Funktion serialize(), die wir unter Nutzung der Namensräume beide in unserem Programm nutzen können:
End of explanation
"""


import sys
sys.version

sys.platform

"""
Explanation: Der Namensraum ist einfach der Name der Datei, die das Modul repräsentiert.
Im obigen Beispiel gibt es also ein Modul (d.h. eine Datei) a.py und ein
zweites Modul b.py.
Module importieren
Wir haben bereits gehört, dass wir Module importieren müssen, ehe wir sie verwenden können. Dazu gibt es verschiedene Möglichkeiten. Die einfachste haben wir bereits kennengelernt: Wir importieren das gesamte Modul unter Beibehaltung des Modulnamens. Als Beispiel verwenden wir wieder ein Modul aus der Standard-Library: sys stellt Informationen zur aktuellen Systemumgebung bereit:
End of explanation
"""


from sys import platform
platform

"""
Explanation: Nur einen Teil eines Moduls importieren
Manchmal sind wir nur an einem kleinen Teil eines Moduls interessiert, zum Beispiel wenn wir nur die aktuelle verwendetet Plattform herausfinden wollen:
End of explanation
"""


version = '0.9 beta'
print('Sie verwenden MeinProgramm in Version {}'.format(version))

# Imports sollten immer ganz oben passieren, man kann sie aber überall verwenden
from sys import version 
print('Sie verwenden MeinProgramm in Version {}'.format(version))

"""
Explanation: Achtung: Hier haben wir etwas aus einem Modul in den globalen (bzw. unseren eigenen) Namensraum importiert. Wir ersparen uns dadurch zwar Tipparbeit, handeln uns aber auch einige Probleme ein, weil wir den eigenen Namensraum verschmutzt haben:

Wir haben die Nachvollziehbarkeit unseres Codes erschwert, weil beim Lesen des Codes erst herausgefunden werden muss, was es mit diesem platform auf sich hat - sys.platform ist hier viel klarer.
Wir können uns Seiteneffekte einhandeln, wenn wir uns u.U. unbeabsichtigt eigene Variablen überlagern.

Hier ein Beispiel:
End of explanation
"""


version = '0.9 beta'

# Imports sollten immer ganz oben passieren, man kann sie aber überall verwenden
from sys import *
print('Sie verwenden MeinProgramm in Version {}'.format(version))

"""
Explanation: Noch schlimmer ist diese Version:
End of explanation
"""


import sys
dir(sys)

"""
Explanation: Hier haben wir alles aus dem Modul sys in unseren eigenen Namespace importiert. Wir waren uns möglicherweise gar nicht bewusst, dass es in sys eine Variable version gibt, die unsere eigene Variable überlagert. Schwer zu findende Fehler sind so vorprogrammiert!
Kleiner Exkurs: wenn Sie feststellen wollen, was in einem Modul vorhanden (und was wir beim letzten Beispiel allen in unseren Namensraum importiert haben) ist, können Sie die dir() Funktion verwenden:
End of explanation
"""


import matplotlib.pyplot
matplotlib.pyplot.plot([1, 2, 3, 4, 4, 3, 5, 6, 6, 3, 3, 4])
matplotlib.pyplot.show()

"""
Explanation: Namensräume umdefinieren
Manche Namensräume sind sehr lange und es ist daher mühsam, diese immer einzutippen. Deshalb besteht die Möglichkeit, einem Modul einen eigenen Namen zuzuweisen. pyplot ist ein Modul des mächtigen matplotlib Pakets. (Achtung: Dieses Paket ist nicht in der Standard-Library und muss möglicherweise erst nachinstalliert werden).
Hier zuerst die umständliche Variante:
End of explanation
"""


import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4, 4, 3, 5, 6, 6, 3, 3, 4])
plt.show()

"""
Explanation: Normalerweise schreibt man das allerdings so, um sich Tipparbeit zu sparen:
End of explanation
"""


import mystring
help(mystring)

mystring.reverse('abc')

mystring.distinct_len('Mississippi')

"""
Explanation: Module und Docstrings
So wie eine Funktion durch einen Docstring beschrieben werden kann, funktioniert das auch für Module. Dazu muss man einfach direkt am Anfang der Modul-Datei den entsprechenden Docstring schreiben. Im Verzeichnis dieses Notebooks finden sie eine Datei (d.h. ein Modul) mystring.py. Da dieses Modul über einen DocString verfügt, können diesen auslesen:
End of explanation
"""


sys.path

"""
Explanation: Wie werden Module gefunden?
Module können an unterschiedlichen Stellen im Filesystem liegen. Hier wird kurz beschrieben, wo und wie Python nach Modulen sucht. Dabei kommt eine bestimmte Reihenfolge zum Einsatz. Sobald das (oder zumindest ein gleichnamiges) Modul gefunden wird, wird dieses verwendet. Diese Reihenfolge ist:

Das aktuelle Verzeichnis.
Alle Verzeichnisse, die in der Umgebungsvariable PYTHONPATH definiert sind.
Abhängig von der aufgerufenen Python-Version in bestimmten Verzeichnissen, in denen beispielweise die Standard Library liegt.

Das sys-Modul weiß, wo gesucht wird:
End of explanation
"""


