@borsTiHD: Beide Vorteile kannst Du nicht nutzen weil es so etwas wie ``include`` nicht gibt in Python. Was IMHO auch nachteilig wäre. Namensräume zu haben ist toll. In Programmiersprachen die das nicht, sondern nur ``include`` haben, vermisse ich das und würde da liebend gerne komplett mit ``import`` tauschen. C zum Beispiel. Da behilft man sich ja auch wie bei klassischem PHP damit das man den Funktionen und Strukturen einen Namenspräfix verpasst um die fehlenden Namensräume zu ”simulieren”. Modernes PHP kennt ja mittlerweile auch ``namespace`` als Mittel um Code besser zu strukturieren als alles in einen grossen Namensraum zu kippen.
Wenn Du zu viele Importe in Deinen Modulen hast, kann das ein Hinweis darauf sein, dass der Code falsch aufgeteilt wurde. Insbesondere wenn Module sich gegenseitig importieren oder ringförmig, dann ist ziemlich sicher etwas falsch. Und das kann dann auch tatsächlich zur Laufzeit probleme machen wenn ein Modul etwas aus einem anderen Modul verwendet bevor dessen Code auf Modulebene komplett abgearbeitet wurde und deshalb noch gar nicht alles zur Verfügung steht.
Zum ``if __name__ …``-Test: Der soll ja nicht weg, nur das was da in dem ``if`` steht sollte in einer Funktion stehen damit `obj` beispielsweise nicht einfach so im Modul zur Verfügung steht. Der typische Code sieht an der Stelle so aus:
Und in der `main()`-Funktion steht dann das Hauptprogramm. So ist sichergestellt, dass man nicht aus versehen irgendwelche (modul)globalen Variablen hat, weil alles in der `main()`-Funktion lokal ist.
Nicht nur Klassen sind Objekte, sondern auch Module, Funktionen, und Methoden. Ganz generell ist alles was man an einen Namen binden kann ein Objekt und kann an andere Namen gebunden werden, als Argument übergeben werden, in Datenstrukturen gesteckt werden – halt alles was man so mit Werten/Objekten allgemein machen kann.
Zwei führende doppelte Unterstriche sind dazu da um Namenskollisionen in tiefen Vererbungshierarchien und bei Mehrfachvererbung zu verhindern. Denn diese beiden Unterstriche sorgen dafür, dass das Attribut im Hintergrund umbenannt wird, so das mehr als eine Klasse in einer Vererbungshierarchie den ”gleichen” Namen verwenden kann. Das braucht man so gut wie nie weil wegen dem „duck typing“ tiefe Vererbungshierarchien selten sind, und Mehrfachvererbung kaum eingesetzt wird, weil man da schnell Kopfschmerzen bekommt.
So eine `Settings`-Klasse müsste ja ein Singleton sein. Das ist aber im Grund schon wofür Module da sind, denn aus OOP-Sicht ist ein Modul ein Singleton, das beim ersten ``import`` erstellt wird und bei jedem weiteren ``import`` dann wieder das selbe Objekt geliefert wird. Für so etwas ist in Python einfach ein Modul mit lauter Konstanten üblich. Falls man die noch irgendwie in eine Namensraumhierarchie organisieren möchte, macht man das klassisch mit Namenspräfixen (wie in C oder PHP) oder man erstellt dort ein Wörterbuch. Syntaktisch etwas schöner kann man das mit `argparse.Namespace` aus der Standardbibliothek gestalten, oder mit dem `addict.Dict` wenn es auch etwas ausserhalb der Standardbibliothek sein darf.
Die Docstrings in Deinem ersten Beispiel hatten alle keinen wirklichen Mehrwert. Also bei einer `__init__()` als Dokumentation ”Konstruktor” rein zu schreiben bringt dem Leser nichts. Denn was eine `__init__()` ganz allgemein macht, weiss man ja schon. Ansonsten ist natürlich schon klar das in dem ersten Beispielcode von Dir nicht wirklich etwas sinnvolles dort stehen kann, weil es ja alles nur Tests waren die nicht wirklich etwas sinnvolles gemacht haben.
Für Docstrings gibt's ein PEP. Ansonsten kann man sich auch an vorhandenen orientieren. Die halbe Python-Dokumentation besteht ja auch den Texten die aus den Docstrings im Quelltext gezogen wurden. Üblich ist das die erste Zeile einer Funktion/Methode in einem Satz beschreibt was sie macht. Und dann eventuell im folgenden noch weitere Erklärungen und Beschreibungen der Argumente, Ausnahmen, und dem Rückgabewert.
Zum realen Beispiel: `includes` ist kein wirklich guter Name für ein Package, das würde besser direkt `autofan` oder `autofan_lib` heissen. Das die externen Bibliotheken da auch liegen ist nicht gut. Das sieht dann ja auch so aus als wären die gar nicht ordentlich installiert worden und wurden da einfach hin kopiert. Das macht man in Python so eher nicht und das funktioniert auch nicht mit allen Bibliotheken. Externes installiert man entweder systemweit oder für den aktuellen Benutzer, oder – und das ist am sichersten/flexibelsten – man erstellt eine virtuelle Umgebung mit `venv` (bei aktuellen Python 3-Versionen schon dabei) oder `virtualenv`, oder der letzte Schrei: `pipenv`. Das verbindet virtuelle Umgebungen und ``pip`` wie man das von anderen Programmiersprachen wie beispielsweise ``npm`` bei JavaScript/Node.js kennt.
Wenn man sich die Verzeichnisstruktur so anschaut, würde man vermuten dass `autofan.py` ein ordentlich installiertes `keyboard` importiert und nicht das `includes.keyboard`. Diesen Irrtum bemerkt man nur wenn man sich `includes.autofan.Clipboard` aufmerksam anschaut weil Du dort plötzlich ein Verzeichnis *in* einem Package zum Modulsuchpfad hinzfügst. Das ist ganz ungünstig weil nun plötzlich alle Deine Module mit und ohne `includes` importierbar sind und das zu unterschiedlichen Modulen mit gleichen Namen und (meistens) Inhalt führen kann, die aber nicht die *selben* sind. Das kann zu komischen und schwer zu findendenen Fehlern führen wenn gleiche Objekte nicht die selben sind. Beispielsweise Klassen und damit auch Ausnahmen.
Zudem steht nun plötzlich alles was im Package `includes` ist, mit allen anderen Modulen/Packages in Konkurrenz. Packages sind ja auch Module und haben ebenfalls die Funktion von Namensräumen, das eben nicht alles auf oberster Ebene einfach so zur Verfügung steht und man aufpassen muss das alles anders heisst als irgend etwas was sonst noch so installiert wird.
Wenn man ein Projekt auf mehrere Module aufteilt, sollte man deshalb alles in ein Package stecken das möglichst ”ungenerisch” heisst, so das man damit keiner anderen Bibliothek in die Quere kommt.
Was bei Programmen auch üblich ist, ist das Programm selbst auch mit in das Package zu schreiben, beispielsweise als `__main__.py` und für Benutzer maximal ein kleines Startprogramm zu schreiben. Dafür kann man in der `setup.py` auch Einstiegspunkte definieren und muss dann teilweise nicht einmal mehr ein Startprogramm selbst schreiben und unter Windows wird beim Installieren dafür eine kleine EXE erstellt.
Eine Datei pro Klasse ist unüblich. Python ist nicht Java. Das Modul ist in Python das was in Java die Package-Ebene ist in der die Klassen liegen. Wenn man Module nicht verwendet um mehrere Sachen zusammen zu fassen, ignoriert man Module als Organisationseinheit komplett. So Namen wie `Settings.Settings`, `Clipboard.Clipboard`, und `GUI.MyGUI` was ohne das unsinnige `My` ja auch `GUI.GUI` heissen würde, sind ein Hinweis das man da was komisches macht. Das kann mal vorkommen, aber wenn das die Regel ist, dann verwendet man Module wahrscheinlich nicht richtig.
Die ”Funktion“ `AutoFanHotkey()` greift auf `clip` und `AutoFanGUI` zu ohne die als Argumente übergeben zu bekommen. Die sind auf Modulebene definiert, was wie gesagt nicht sein dürfte. Wenn eine Klasse keinen Sinn macht, weil die neben der `__init__()` nur eine weitere Methode hätte, ist `functools.partial()` ein passendes Werkzeug.
Der Docstring der Funktion beschreibt nicht was die Funktion macht, sondern wann/wodurch sie aufgerufen wird. Das wäre eventuell auch interessant, aber eben nicht so sehr wie was sie denn eigentlich macht. Wann sie aufgerufen wird, ist an der Stelle auch nicht so ganz sicher, denn es ist gar nicht so selten das man Rückruffunktionen hat, die man auch in anderen Kontexten sinnvoll aufrufen/verwenden kann.
Die URL von Repositories von externen Bibliotheken jedes mal beim ``import`` als Kommentar dahinter zu setzen ist unüblich. Wenn sich das mal ändert muss man das überall im Code anpassen. Das gehört eher in die Dokumentation. So minimal eine `readme.*` hat man ja eigentlich immer. Noch besser wäre eine `requirements.txt` für ``pip`` und statt Github, was technisch bei ``pip`` auch möglich ist, besser den PyPI-Namen des Pakets. Oder eine `Pipfile`-Datei statt `requirements.txt` wenn man ``pipenv`` verwendet.
Die Kommentare vor den Impoten sind nicht wirklich nötig wenn man sich an die Konvention hält das man die Importe nach Standardbibliothek, externe Bibliotheken, eigene Module gruppiert. Ich glaube den
Style Guide for Python Code hatte ich noch gar nicht verlinkt.
An Kommentaren ist da generell zu viel. Diese Verzierungen wie Trennlinien machen nicht wirklich Sinn. Vor einem ``class Clipboard`` den Kommentar ``Clipboard - Klasse`` braucht auch kein Mensch. Kommentare sollten nicht beschreiben *was* gemacht wird, denn das steht da bereits als Code, sondern *warum* der Code das macht. Das aber auch nur sofern das nicht offensichtlich ist. Bei einem `time.sleep()` zu kommentieren das da geschlafen wird macht keinen Sinn. Interessanter wäre eventuell *warum* da zwischen zwei Anweisungen etwas gewartet wird. Die Kommentare in `Clipboard.copy()` sind jedenfalls so wie sie da stehen alle überflüssig. Wer das was da steht nicht am Code erkennen kann, dem ist auch mit den Kommentaren nicht mehr zu helfen.
Wenn man allgemein zeigen möchte wie man etwas verwendet, kann man das übrigens auch in den Docstring schreiben. Und zwar so, dass man es mittels `doctest` aus der Standardbibliothek auch testen kann ob das was man als Beispiel zeigt auch wirklich so funktioniert wie beschrieben. Also zum Beispiel das was Du im ``if __name__…`` im `Clipboard`-Modul machst, könnte sich als Beispiel in der Dokumentation der `Clipboard`-Klasse ganz gut machen.
Weiter zur GUI: Sternchen-Importe sind Böse™. Damit entwertet man wieder Module als Namensräume und schmeisst alle definierten Namen aus einem Modul in ein anderes, ohne wirklich eine Kontrolle darüber zu haben was man sich da alles einfängt. Denn das muss ja nicht nur das sein was dokumentiert ist für das Modul und man holt sich damit auch alles was dieses Module selbst importiert hat. Es kann auch die Gefahr von Namenskollisionen geben. Beispiel aus meinem letzten Beitrag: ``from os import *`` und schon kann man die eingebaute `open()`-Funktion nicht mehr benutzen, weil die im aktuellen Modul nun durch `os.open()` verdeckt wird. Beim `Tkinter`-Modul sind das ca. 190 Namen von denen nur ein Bruchteil tatsächlich benutzt wird. Üblich ist hier ein ``import tkinter as tk`` und dann das Referenzieren des Modulinhalts über den Namen `tk`. Also Beispielsweise ``tk.Button(…)`` statt nur ``Button(…)``.
Die Klasse ist mir jetzt etwas zu umfangreich um da ins Detail zu gehen, aber da sieht einiges sehr merkwürdig aus. Angefangen bei den ganzen doppelten Unterstrichen die mindestens mal nur jeweils einer sein sollten. Aber selbst das würde ich sein lassen. Es gibt in Python keinen Zugriffsschutz und man muss den Programmierer auch nicht vor jeder Dummheit mit Stacheldraht und Selbstschussanlagen schützen wollen. Wenn etwas nicht wirklich sehr ”privat” ist, oder die reale Gefahr von Problemen besteht wenn jemand der programmieren kann/nicht völlig deppert ist, darauf zugreift, dann ist das nichts was ich als Implementierungsdetail kennzeichne. Wenn etwas dokumentiert ist, dann ist es teil der öffentlichen API, es sei denn es steht in der Dokumentation das man das normalerweise nicht selbst aufruft. Ansonsten erwarte ich das man den Verstand einschaltet und darüber nachdenkt was man aufruft, verwendet, oder setzt. „Consenting adults“ ist da das Stichwort im Python-Umfeld.
Die Aufteilung von `__init__()` auf mehere Methoden wurde ja schon angesprochen. Ist unüblich sofern das kein Code ist der auch anderweitig verwendet werden kann. Macht es IMHO auch unübersichtlicher denn ich sehe in der MyGUI nicht so leicht was da alles zur Initialisierung gehört und in welcher Reihenfolge ausgeführt wird, als wenn das alles normal in der `__init__()` stehen würde.
Die Kommentare/Beschreibungen sind teils komisch. Was da alles so als Modul bezeichnet oder ”geladen” wird. Module sind das was bei ``isinstance(sys, types.ModuleType)`` den Wert `True` liefert. Bei allem anderem sollte man vorsichtig sein das als Modul zu bezeichnen oder deutlich machen das man etwas anderes als Python-Module damit meint. Und bei „laden“ denke ich an Daten die von irgendwo aussen (Hintergrundspeicher, Datenbank) in den Speicher kommen, nicht an simple Attributzuweisungen von (teilweise konstanten) Werten.
Selbstbestimmen der Fenstergrösse und -position, `place()`, und die Fensteverwaltung umgehen, dann aber teilweise deren Funktion selbst in Python noch mal nachzuprogrammieren ist auch eher schräg. Das macht alles viel unnötige Arbeit, ist unflexibel, und funktioniert auf anderen Rechnern, mit anderen Einstellungen womöglich anders/schlechter bis hin zur Unbenutzbarkeit der GUI.
Sooo, nun muss ich leider los…