Methode einer Klasse in einem separaten Modul aufrufen, geht das?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
borsTiHD
User
Beiträge: 10
Registriert: Sonntag 29. April 2018, 08:11

Hallo zusammen,

ich bin noch nicht tief drin in der Python Welt und habe in Zukunft vor ein größeres Projekt, das ich mit AutoIT geschrieben habe, in Python umzusetzen.
In meinem AutoIT Script habe ich sehr viele Funktionen in unterschiedliche Dateien ausgegliedert um die Übersichtlichkeit zu steigern (zb. eine Datei für meine GUI, eine für Berechnung, etc. - entsprechend nach Themen gegliedert).

Eine solche Aufteilung meiner Funktionen würde ich auch gerne in Python übernehmen, damit ich nicht hinterher eine große Datei mit tausend Zeilen habe.
Dazu habe ich schon herrausgelesen das soetwas ein "Modul" in Python ist?
Da ich jetzt in Python auch mit Klassen/Objekten programmieren kann, würde ich das ganze selbstverständlich auch lernen.
Das ist meine Ausgangssituation.

Aber schon jetzt bin ich auf ein Problem gestoßen, dass mein altes Denken wohl komplett umstricken muss.
Ich hoffe ihr könnt mir helfen.

Wieso kann ich, obwohl ich mein Objekt "obj" bereits erstellt habe, über "Modul.ModulClassTest()" nicht auf das Objekt zugreifen?
Wenn ich meine "klassen_test.py" ausführe, erhalte ich folgende Ausgabe:
Traceback (most recent call last):
File "D:\Stuff\Python\klassen_test.py", line 27, in <module>
test.py wird ausgef�hrt
hello, ich bin ein weiterer test...
Obj - obj.VAR_TEST: Ich bin ein Test String.
Ich bin eine Test Methode der Klasse 'Class'.
Ich bin eine Test Funktion vom Modul 'Modul'.
Modul.ModulClassTest()
File "D:\Stuff\Python\includes\test\Modul.py", line 8, in ModulClassTest
print("Modul Test um Variable der Klasse auszulesen - obj.VAR_TEST: " + obj.VAR_TEST)
NameError: name 'obj' is not defined
[Finished in 0.093s]

Vielen Dank schon mal für eure Hilfe. :)
Bitte schaut euch meine drei zum Testen erstellten Dateien an.

VG
borsTiHD


/klassen_test.py:

Code: Alles auswählen

# coding: utf-8

# Bibliotheken
from includes.test import Class
from includes.test import Modul

def test():
    print("hello, ich bin ein weiterer test...")
    return True

# Main Function
if __name__ == '__main__':
    print("test.py wird ausgeführt")
    test()

    # Erstellt ein Test Objekt und gibt die vom Konstruktor erstellte Variable aus
    obj = Class.Class()
    print("Obj - obj.VAR_TEST: " + obj.VAR_TEST)

    # Methode von Class wird aufgerufen
    obj.Print()

    # Funktion aus einem Modul wird aufgerufen
    Modul.ModulFunc1()

    # Prüft ob auf eine Klasse in meinem Modul zugegriffen werden kann
    Modul.ModulClassTest()
/includes/test/Class.py:

Code: Alles auswählen

class Class:

    def __init__(self):
        """ Konstruktor """
        self.__main()

    def __main(self):
        """ Test Variablen """
        self.VAR_TEST = "Ich bin ein Test String."
        self.VAR_TEST2 = "Zweiter Test."

    def Print(self):
        """ Test Methode """
        print("Ich bin eine Test Methode der Klasse 'Class'.")


# Main Function - Wird ausgeführt, wenn die *.py nicht importiert, sondern direkt ausgeführt wurde
if __name__ == '__main__':

    # Bibliothek wurde nicht importiert
    print("Hinweis: Bibliothek importieren und nicht direkt ausführen.")

/includes/test/Modul.py:

Code: Alles auswählen

def ModulFunc1():
    """ Test Methode """
    print("Ich bin eine Test Funktion vom Modul 'Modul'.")

def ModulClassTest():
    """ Test Methode """
    print("Modul Test um Variable der Klasse auszulesen - obj.VAR_TEST: " + obj.VAR_TEST)
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@borsTiHD: Vielleicht fangen die Verständnisprobleme ja schon bei der Namensgebung an: `includes`. Das importieren von Modulen oder Objekten aus Modulen ist etwas anderes als was man üblicherweise unter dem Begriff „includes“ versteht, wo eine Datei sozusagen an der Stelle wo sie inkludiert wird als Quelltext eingefügt wird, so als würde man einfach den kompletten Quelltext an der Stelle per Kopieren und Einfügen zur Verfügung stellen. Module sind selbst Objekte und bilden einen Namensraum. Alles was in einem Modul definiert wird, ist erst einmal nur dort bekannt und nicht auf magische Weise irgendwo anders. Man muss Werte/Objekte explizit aus anderen Modulen importieren, oder die Module importieren und dann über das Modulobjekt auf die Werte zugreifen.

Im Modul `Modul` wird der Name `obj` nirgends definiert, also kann man da auch nicht einfach so drauf zugreifen. Getrennte Namensräume zu haben ist ja gerade der Sinn von Modulen. So dass man zum Beispiel im Modul eine `open()`-Funktion haben kann die nichts mit der `open()`-Funktion im `io`-Modul zu tun hat, und beide können unabhängig von der eingebauten `open()`-Funktion sein. Oder der vom externen `PIL`-Modul um Bilddateien zu öffnen. Und auch Du könntest eine `open()`-Funktion in einem eigenen Modul schreiben, die unabhängig von den ganzen anderen ist.

Der Code ist teilweise verwirrend weil Du Dich nicht an die Namenskonventionen hältst. `Modul` müsste demnach nämlich eine Klasse sein, weil man Module wie fast alles in Python klein_mit_unterstrichen schreibt. Ausnahme sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

Warum gibt `klassen_test.test()` den Wert `True` zurück? Beim Aufruf machst Du mit dem Rückgabewert ja auch gar nichts.

Das was in `klassen_test` im ``if __name__ == …``-Zeig steht, sollte in einer Funktion stehen, denn Du definierst da Variablen die dann auf Modulebene existieren, was nicht sein sollte.

Der Text in `ModulClassTest` ist falsch: Du versuchst da nicht auf eine Klasse zuzugreifen sondern auf ein Objekt (das keine Klasse ist, denn auch Klassen sind in Python Objekte). Die Funktion versucht auf einfach so auf `obj` zuzugreifen. Selbst wenn das auf Modulebene existieren würde – das sollte es nicht. Alles was eine Funktion oder Methode ausser Konstanten verwendet, sollte als Argument(e) übergeben werden. Alles andere ist unübersichtlich und führt zu fehleranfälligem Code.

`__init__()` ist kein Konstruktor. Das wäre `__new__()`. Wird aber sehr selten gebraucht, normalerweise kommt man mit einer Initialisierung über `__init__()` aus.

Vergiss am besten gleich wieder die führenden doppelten Unterstriche wie bei `__main()`. Das ist nicht ”private” aus anderen Programmiersprachen. Wenn Du Lernmaterial verwendest, das das behauptet, schmeiss das weg. Falls es in Papierform ist, verbrenn es am besten. :-)

Implementierungsdetails werden in Python per Konvention mit *einem* führenden Unterstrich gekennzeichnet.

Die `__main()`-Methode macht aber an sich so gar keinen Sinn, das sollte einfach in der `__init__()` stehen.

`Print()` ist nur syntaktisch eine Methode da das Objekt gar nicht verwendet wird. Semantisch ist das einfach nur eine Funktion und man sollte erklären können warum man so etwas macht, statt das einfach als Funktion ausserhalb der Klasse zu definieren. Und als `staticmethod()` dekorieren, damit der Leser das besser sieht, und weiss, dass das so gewollt, und kein Fehler ist.

Die Doctsrings enthalten allesamt keinen Inhalt den man so in Doctsrings schreiben würde.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
borsTiHD
User
Beiträge: 10
Registriert: Sonntag 29. April 2018, 08:11

Wow, erstmal vielen Dank für die Umfangreiche Antwort. :)
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 @borsTiHD: Vielleicht fangen die Verständnisprobleme ja schon bei der Namensgebung an: `includes`. Das importieren von Modulen oder Objekten aus Modulen ist etwas anderes als was man üblicherweise unter dem Begriff „includes“ versteht, wo eine Datei sozusagen an der Stelle wo sie inkludiert wird als Quelltext eingefügt wird, so als würde man einfach den kompletten Quelltext an der Stelle per Kopieren und Einfügen zur Verfügung stellen. Module sind selbst Objekte und bilden einen Namensraum. Alles was in einem Modul definiert wird, ist erst einmal nur dort bekannt und nicht auf magische Weise irgendwo anders. Man muss Werte/Objekte explizit aus anderen Modulen importieren, oder die Module importieren und dann über das Modulobjekt auf die Werte zugreifen.
Oh, ja... tatsächlich hab ich schon irgendwie sowas vermutet. :|
Eigentlich bin ich sehr viel in der PHP Welt unterwegs und scripte sehr viel mit AutoIT.
In Beiden kann ich "includen" und gehe daher, wie du bereits vermutet hast, davon aus das ich die Funktionen etc. dann Global verfügbar habe, also als würde man den Quelltext der Datei an dieser Stelle einfügen.
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Im Modul `Modul` wird der Name `obj` nirgends definiert, also kann man da auch nicht einfach so drauf zugreifen. Getrennte Namensräume zu haben ist ja gerade der Sinn von Modulen. So dass man zum Beispiel im Modul eine `open()`-Funktion haben kann die nichts mit der `open()`-Funktion im `io`-Modul zu tun hat, und beide können unabhängig von der eingebauten `open()`-Funktion sein. Oder der vom externen `PIL`-Modul um Bilddateien zu öffnen. Und auch Du könntest eine `open()`-Funktion in einem eigenen Modul schreiben, die unabhängig von den ganzen anderen ist.
Aus dieser Sicht zu betrachten macht das wirklich Sinn und ist auch sehr hilfreich.
Leider muss ich wohl dennoch meine Denkweise komplett umstricken. :lol:
Daher an dieser Stelle vorsichtig gefragt... kann ich Beide Vorteile nutzen?
Einmal das Importieren von Klassen/Modulen und einmal echtes "includen"?

Ich verstehe zwar dass durch das "includen" vieles verstrickter wird, aber ich hätte gerne ein Hauptfile, über das ich alles steuere, aber möglichst klein gehalten wird.
Entsprechend müssten dann viele Funktionen einzelner includes untereinander auf sich zugreifen können, ohne in jeder alles zu importieren.

Ich merke anhand deiner Antwort auch das ich mich nochmal sehr intensiv mit Klassen beschäftigen muss.

__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Der Code ist teilweise verwirrend weil Du Dich nicht an die Namenskonventionen hältst. `Modul` müsste demnach nämlich eine Klasse sein, weil man Module wie fast alles in Python klein_mit_unterstrichen schreibt. Ausnahme sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).
Werde ich versuchen zu übernehmen.
Ich hab Python nie wirklich gelernt, bzw keine Programmiersprache gezeigt bekommen, vielmehr alles selbst erarbeitet.

__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Warum gibt `klassen_test.test()` den Wert `True` zurück? Beim Aufruf machst Du mit dem Rückgabewert ja auch gar nichts.
Haha... ja das ist mir bewusst. Eigentlich wollte ich damit mehr machen und noch eine If Bedingung prüfen.
Aber hab mich dann doch dagegen entschieden, den Return Wert dennoch stehen lassen. :lol:
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Das was in `klassen_test` im ``if __name__ == …``-Zeig steht, sollte in einer Funktion stehen, denn Du definierst da Variablen die dann auf Modulebene existieren, was nicht sein sollte.
Hm... da fehlt mein Verständis dafür.
Ich hatte vor hier meine eigentliche Programm Routine hinzuschreiben, bzw. hier steuern zu wollen.
Empfand es allerdings auch als recht angenehm, als Nebeneffekt den Code an dieser Stelle dadurch einrücken zu können.
Zudem hatte ich es so entnommen das dieser Teil nur ausgeführt wird wenn man die *.py direkt ausführt.
In meinen Modulen nutze ich das um zu verhindern dass die Module direkt ausgeführt werden sollen und gleichzeitig als eine Art Docu für mich selbst mit Beispielen zu füllen.
Ich poste als nächstes mal was ich bisher wirklich vorbereitet habe, vlt hilft das.
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Der Text in `ModulClassTest` ist falsch: Du versuchst da nicht auf eine Klasse zuzugreifen sondern auf ein Objekt (das keine Klasse ist, denn auch Klassen sind in Python Objekte). Die Funktion versucht auf einfach so auf `obj` zuzugreifen. Selbst wenn das auf Modulebene existieren würde – das sollte es nicht. Alles was eine Funktion oder Methode ausser Konstanten verwendet, sollte als Argument(e) übergeben werden. Alles andere ist unübersichtlich und führt zu fehleranfälligem Code.
Oh, ja... gebe dir Recht. Da hab ich mich verschrieben, ich meinte auch egt Objekt.
Was ich nicht wusste ist, das selbst Klassen egt Objekte schon sind?
Bzgl. der unübersichtlichkeit gebe ich dir Recht, aber ich habe soviel das ich später umsetzen möchte, es wäre für mich selbst noch unübersichtlicher alles in einer Modulebene zu haben.
Mir fehlt hier noch der *klick* im Kopf um das ganze anders zu verwirklichen. :|
Bin derzeit voll im Modus das ich alles "include". :D

__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 `__init__()` ist kein Konstruktor. Das wäre `__new__()`. Wird aber sehr selten gebraucht, normalerweise kommt man mit einer Initialisierung über `__init__()` aus.
Oh... dann muss ich mir Klassen/Objekte in Python wirklich nochmal genauer anschauen.
Wenn es aber mit einer Initialisierung auch soweit geht, sollte das für mich erstmal ausreichen. :)
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Vergiss am besten gleich wieder die führenden doppelten Unterstriche wie bei `__main()`. Das ist nicht ”private” aus anderen Programmiersprachen. Wenn Du Lernmaterial verwendest, das das behauptet, schmeiss das weg. Falls es in Papierform ist, verbrenn es am besten. :-)
Das muss ich mir irgendwo abgeschaut haben und selbst gedacht haben das es private wäre.
Also ist es einfach unnötig an dieser Stelle?
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Implementierungsdetails werden in Python per Konvention mit *einem* führenden Unterstrich gekennzeichnet.

Die `__main()`-Methode macht aber an sich so gar keinen Sinn, das sollte einfach in der `__init__()` stehen.
Hehe, ja gebe ich dir Recht.
Ich habe in meinem bisherigen Teil ein Modul für meine "Settings".
Eigentlich hatte ich vor in diesem Modul sämtliche Modulübergreifenden Variablen in einzelne Katgeorien abzulegen.
Für diese Katgeorien wollte ich Funktionen/Methoden der Klasse missbrauchen, sodass ich dann hinterher ein Objekt aus der Settings Klasse gehabt hätte, mit unterschiedlichen Methoden (Katgeorien für mich).
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 `Print()` ist nur syntaktisch eine Methode da das Objekt gar nicht verwendet wird. Semantisch ist das einfach nur eine Funktion und man sollte erklären können warum man so etwas macht, statt das einfach als Funktion ausserhalb der Klasse zu definieren. Und als `staticmethod()` dekorieren, damit der Leser das besser sieht, und weiss, dass das so gewollt, und kein Fehler ist.
Da kann ich dir nicht ganz Folgen was du meinst.
Aus meinem eigentlichen Projekt habe ich auch solche Sachen, vlt wirds damit gleich deutlicher was ich falsch anwende. :D
Testen wollte ich ob ich so drauf zugreifen kann: "obj.Print()"
__blackjack__ hat geschrieben: Freitag 16. November 2018, 22:39 Die Doctsrings enthalten allesamt keinen Inhalt den man so in Doctsrings schreiben würde.
Wie würdest du die nutzen?
Ich hab bisher nur die Erklärung, dass die eine Art Beschreibung für die entsprechende Funktion/Methode/Klasse, etc genutzt wird.
Hab ich das falsch aufgenommen?

So... ich hoffe ich hab nicht zuviel geschrieben. :D
Vielen vielen Dank für die große Erklärung. :)
Das bringt mich aufjedenfall zum Umdenken... auch wenn ich noch nicht ganz weiß wie ich das auf mein AutoIT Projekt anwenden kann, da ich dort eine komplett andere Logik hatte.

Zum besseren Verständniss, zeige ich vlt einfach mal die Anfänge die ich bisher habe.
Zur übersichtlichkeit versuche ich das in einem weiteren Post.
Benutzeravatar
borsTiHD
User
Beiträge: 10
Registriert: Sonntag 29. April 2018, 08:11

Hier mein eigentliches Projekt, das ich von Grund auf in Python aus meinem AutoIT Projekt entwickeln möchte.

Zur "wirklich kurzen" Erklärung.
Es soll ein Programm mit einer GUI werden (derzeit noch Tkinter, möchte es aber mit Qt umsetzen).
Dieses Programm soll immer im Hintergrund und nicht sichtbar laufen.
Es soll warten bis der User eine Hotkey Kombination drückt ("STRG+<") und daraufhin soll das Programm unter Windows vom User zuvor markierten Text kopieren und analysieren/verarbeiten/auswerten und in der dann erscheinenden GUI aufbereitet anzeigen.
Die Idee ist, auf meiner Arbeit haben wir ein Monitoringprogramm hinter einer Citrix Umgebung, bei der man über das typische "STRG+C" einzelne Datensätze aus einer Eventliste rauskopieren kann.
Anhand dieser Daten sollen über RegEx bestimmte Datensätze erkannt werden und später auch über Threads automatisch zb. angepingt werden, etc.

In Python selbst hab ich bisher erstmal nur experimentiert.
Derzeit habe ich rudimentär meine GUI nachgebaut.
"Kopieren" unter Windows nachgestellt und in der Console ausgegeben (sofern beim Hotkey nutzen vorher Text markiert hatte), sowie das eigentliche Verhalten dass das Programm auf den Hotkey reagiert.
Die "Hotkey.py" nutze ich derzeit nicht, weil ich darüber auf mein Problem gestoßen bin und mich entschlossen habe bei euch nachzufragen. :D

# Meine eigenen Module
/autofan.py
/includes/autofan/Clipboard.py
/includes/autofan/GUI.py
/includes/autofan/Hotkey.py
/includes/autofan/Settings.py

# Externe Bibliotheken
/includes/keyboard/
/includes/pyperclip/




/autofan.py

Code: Alles auswählen

# coding: utf8

# AutoFan Bibliotheken
from includes.autofan import Settings, GUI, Clipboard

# AutoFan Module
# import includes.autofan.Hotkey
# from includes.autofan.Hotkey import *

# Externe Bibliotheken
import keyboard     # https://github.com/boppreh/keyboard

# Hotkey Funktion
def AutoFanHotkey():
    """ Funktion wird durch Hotkey ausgelöst """
    print("======================\nHotkey wurde gedrückt.")
    clip.copy()             # Kopiert den aktuell markierten Text in die Zwischenablage
    AutoFanGUI.GUIShow()    # GUI wird angezeigt

# Main Function
if __name__ == '__main__':
    settings = Settings.Settings()                                  # Lädt Settings

    print("APP_NAME: " + str(settings.APP_NAME))
    print("APP_VERSION: " + str(settings.APP_VERSION))
    print("APP_BUILD: " + str(settings.APP_BUILD))
    print("APP_TITLE: " + str(settings.APP_TITLE))
    print("APP_HOTKEY: " + str(settings.APP_HOTKEY))

    keyboard.add_hotkey(settings.APP_HOTKEY, AutoFanHotkey)         # Hotkey wird gesetzt
    clip = Clipboard.Clipboard()                                    # Erstellt ein Clipboard Objekt

    AutoFanGUI = GUI.MyGUI(settings.APP_TITLE)                      # AutoFan GUI Objekt wird erstellt
    AutoFanGUI.run()                                                # Mainloop der GUI wird gestartet
/includes/autofan/Clipboard.py

Code: Alles auswählen

# coding: utf8

# Bibliotheken
import sys
import os
sys.path.append(os.path.abspath("includes/"))
import keyboard     # https://github.com/boppreh/keyboard
import pyperclip    # https://github.com/asweigart/pyperclip
import time


##########################################################################################################
# --- Clipboard - Klasse -------------------------------------------------------------------------------
class Clipboard:

    def __init__(self):
        """ Konstruktor Funktion mit setzen einer protecteten Variable """
        self.__ClipboardContent = ""

    def copy(self):
        """ Kopiert markierten Text ins interne Clipboard """
        keyboard.press_and_release('ctrl+c')            # Führt ein STRG+C zum kopieren aus
        time.sleep(0.3)                                 # 300 ms Sleep
        self.__ClipboardContent = pyperclip.paste()     # Speichert Zwischenablage ab
        print("Zwischenablage Inhalt:")
        print(self.__ClipboardContent)

    def set(self, var):
        """ Setzt Variable in die Windows Zwischenablage und in die interne des Objekts """
        pyperclip.copy(var)
        self.__ClipboardContent = var

    def delete(self):
        self.__ClipboardContent = ""
        pyperclip.copy(self.__ClipboardContent)

    def get(self):
        """ Holt sich den Clipboard Inhalt """
        return self.__ClipboardContent

# --- Clipboard - Klasse -------------------------------------------------------------------------------
##########################################################################################################


# Main Function - Wird ausgeführt, wenn die *.py nicht importiert, sondern direkt ausgeführt wurde
if __name__ == '__main__':

    # Bibliothek wurde nicht importiert
    print("Hinweis: Bitte Clipboard.py Bibliothek importieren und nicht direkt ausführen.")

    # Beispiele für das Clipboard
    clip = Clipboard()
    print("Zwischenablage Inhalt:")
    print(clip.get())

    clip.copy()
    print("Zwischenablage Inhalt:")
    print(clip.get())

    clip.set("Das ist ein kleiner Test!")
    print("Zwischenablage Inhalt:")
    print(clip.get())

    print("Zwischenablage wird jetzt gelöscht!!!")
    clip.delete()
    print("Zwischenablage Inhalt:")
    print(clip.get())
/includes/autofan/GUI.py

Code: Alles auswählen

# coding: utf8

# Bibliotheken
from tkinter import *
import tkinter.font as font
from ctypes import windll
import random


##########################################################################################################
# --- MyGUI - Klasse -------------------------------------------------------------------------------
class MyGUI:

    def __init__(self, APP_TITEL=""):
        """ Konstruktor der GUI """
        # Variablen werden geladen
        self.__Variablen(APP_TITEL)

        # GUI wird erstellt (Main Frame, Titelbar, X Button)
        self.__buildMainGUI()

        # Test Modul
        self.__GUITestModul()

        # Settings Modul wird erstellt
        self.__GUISettings()

    # ------------------------------------------------------------------------------------------------------------------
    # Initiale Variablen ###############################################################################################
    def __Variablen(self, APP_TITEL):
        # GUI Koordinaten
        self.APP_WIDTH = 620
        self.APP_HEIGHT = 500
        self.APP_XPOS = 100
        self.APP_YPOS = 100
        self.APP_TITEL = APP_TITEL

        # GUI Hintergrundfarbe
        self.APP_BG_TITELBAR = "#202225"
        self.APP_BG_COLOR = "#36393e"

        # Font
        self.APP_FONT_COLOR_TITEL = "#ffffff"
        self.APP_FONT_COLOR_HEADLINE = "#ffffff"
        self.APP_FONT_COLOR_TEXT = "#999b9d"
        self.APP_FONT_COLOR_TEXT_SUCCESS = "#72da83"
        self.APP_FONT_COLOR_TEXT_WARNING = "#f04747"
        self.APP_FONT_COLOR_TEXT_ACTIVE = "#ffffff"
        self.APP_FONT_COLOR_TEXT_nACTIVE = "#636568"

        # Button
        self.APP_BUTTON_COLOR_FONT = "#ffffff"
        self.APP_BUTTON_COLOR_FONT_DARK = "#2f3136"
        self.APP_BUTTON_COLOR_NORMAL = "#5e626c"
        self.APP_BUTTON_COLOR_BLUE = "#7289da"
        self.APP_BUTTON_COLOR_SUCCESS = "#72da83"
        self.APP_BUTTON_COLOR_WARNING = "#f04747"
        self.APP_BUTTON_COLOR_LIGHT = "#b0b4bf"
        self.APP_BUTTON_COLOR_DARK = "#2f3136"

        # Line Colors
        self.APP_LINE_COLOR = "#4a4c51"
        self.APP_LINE_COLOR_DARK = "#222427"

        # Close Button
        self.APP_CLOSE_BUTTON = "X"
        self.APP_CLOSE_FONT = "Helvetica"
        self.APP_CLOSE_FONT_SIZE = 12
        self.APP_CLOSE_FG_COLOR = "#999b9d"
        self.APP_CLOSE_FG_COLOR_HOVER = "#000000"
        self.APP_CLOSE_BG_COLOR = self.APP_BG_TITELBAR
        self.APP_CLOSE_BG_COLOR_HOVER = "#f04747"

        # Weitere Variablen für die GUI
        self.MousPosX = 0
        self.MousPosY = 0

    def __Fonts(self):
        self.APP_FONT_TITEL = font.Font(root=self.rootGUI, family='Consolas', weight='normal', size=16)
        self.APP_FONT_TEXT_HEADLINE = font.Font(root=self.rootGUI, family='Consolas', weight='normal', size=14)
        self.APP_FONT_TEXT = font.Font(root=self.rootGUI, family='Consolas', weight='normal', size=10)
        self.APP_FONT_BUTTON = font.Font(root=self.rootGUI, family='Consolas', weight='normal', size=9)

    # GUI Module #######################################################################################################
    def __buildMainGUI(self):
        # Erstellt GUI mit namen rootGUI
        self.rootGUI = Tk()
        self.rootGUI.iconbitmap("media/autofan_icon.ico")

        # Lädt Fonts
        self.__Fonts()

        # Setzt beim erstellen der GUI die aktuelle Mausposition
        self.SetMousPosition()

        # Erstellt eine GUI ohne Windows Rahmen und bestimmt dessen Größe/Position
        self.rootGUI.geometry("%dx%d+%d+%d" % (self.APP_WIDTH, self.APP_HEIGHT, self.MousPosX, self.MousPosY))
        self.rootGUI.wm_overrideredirect(True)

        # Hintergrundfarbe der GUI
        self.rootGUI.configure(background=self.APP_BG_COLOR)

        # GUI Rahmen wird erstellt
        self.gui_border = Canvas(self.rootGUI, width=self.APP_WIDTH,
                                 height=25, bg=self.APP_BG_TITELBAR,
                                 highlightthickness=0)                          # Erstellt einen Canvas Bereich
        self.gui_border.place(x=0, y=0, width=self.APP_WIDTH, height=25)        # place() kümmert sich um die Plazierung des Rahmens
        self.gui_border.create_text(self.APP_WIDTH/2, 12,
                                    fill=self.APP_FONT_COLOR_TITEL,
                                    font=self.APP_FONT_TITEL,
                                    text=self.APP_TITEL)                        # Erstellt Fenstertitel in dem Canvas für den Rahmen
        self.gui_border.bind("<ButtonPress-1>", self.__StartMove)               # Sorgt für das Verschieben des Fensters
        self.gui_border.bind("<ButtonRelease-1>", self.__StopMove)              # Sorgt für das Verschieben des Fensters
        self.gui_border.bind("<B1-Motion>", self.__OnMotion)                    # Sorgt für das Verschieben des Fensters

        # Erstellt "X" zum schließen der GUI
        self.gui_close_label = Label(self.rootGUI, text=self.APP_CLOSE_BUTTON,
                                     font=(self.APP_CLOSE_FONT, self.APP_CLOSE_FONT_SIZE),
                                     highlightcolor=self.APP_BUTTON_COLOR_WARNING,
                                     fg=self.APP_CLOSE_FG_COLOR, bg=self.APP_CLOSE_BG_COLOR, cursor="hand2")
        self.gui_close_label.place(x=self.APP_WIDTH-20, y=0)
        self.gui_close_label.bind("<Button-1>", self.GUIHide)                   # Sobald das X angeklickt wird, wird die Funktion GUIHide ausgeführt
        self.gui_close_label.bind("<Enter>", self.__CloseButtonMouseOver)
        self.gui_close_label.bind("<Leave>", self.__CloseButtonMouseOver)

        # Sorgt dafür, dass das Taskbar Icon wieder erscheint (wegen wm_overrideredirect(True))
        self.rootGUI.after(10, lambda: self.__set_appwindow())

    def __GUITestModul(self):
        # Test Label
        self.label1 = self.__CreateLabel(5, 30, "Ich bin eine Überschrift!", self.APP_FONT_TEXT_HEADLINE, self.APP_FONT_COLOR_HEADLINE, self.APP_BG_COLOR)
        self.label2 = self.__CreateLabel(5, 65, "Ich bin ein normaler Text.", self.APP_FONT_TEXT, self.APP_FONT_COLOR_TEXT, self.APP_BG_COLOR)

        # Test Line
        self.line1 = self.__CreateLine(5, 60, 250, 1, self.APP_LINE_COLOR)
        self.line2 = self.__CreateLine(0, 100, self.APP_WIDTH, 1, self.APP_LINE_COLOR)

        # Erstellt einen Test Button
        self.button2 = self.__CreateButton(150, 110, 100, 30, "Klick mich!", self.APP_BUTTON_COLOR_BLUE,
                                           self.APP_BUTTON_COLOR_FONT, self.APP_FONT_BUTTON, self.__EventTest)

        # Weiterer Test Button
        self.button3 = self.__CreateButton(10, 110, 80, 50, "Test?", self.APP_BUTTON_COLOR_SUCCESS,
                                           self.APP_BUTTON_COLOR_FONT_DARK, self.APP_FONT_BUTTON, self.__EventTest)

    def __GUISettings(self):
        # Erstellt KeepOnTop Checkbox
        self.keepontopVar = BooleanVar()
        self.keepontop = Checkbutton(self.rootGUI, text="Keep on top", font=self.APP_FONT_TEXT,
                                     bg=self.APP_BG_COLOR, fg=self.APP_FONT_COLOR_TEXT,
                                     activebackground=self.APP_BG_COLOR, cursor="hand2",
                                     variable=self.keepontopVar, command=self.KeepOnTop)
        self.keepontop.place(x=5, y=200)

        # Erstellt KeepInPosition Checkbox
        self.keepinposVar = BooleanVar()
        self.keepinpos = Checkbutton(self.rootGUI, text="Keep in position", font=self.APP_FONT_TEXT,
                                     bg=self.APP_BG_COLOR, fg=self.APP_FONT_COLOR_TEXT,
                                     activebackground=self.APP_BG_COLOR, cursor="hand2",
                                     variable=self.keepinposVar, command=self.KeepInPosition)
        self.keepinpos.place(x=130, y=200)

        # Erstellt ein Button um die App zu beenden
        self.button_close = self.__CreateButton(560, 190, 50, 30, "Quit", self.APP_BUTTON_COLOR_WARNING,
                                                self.APP_BUTTON_COLOR_FONT, self.APP_FONT_BUTTON, self.rootGUI.quit)

        # GUI größe abspeichern und updaten
        self.APP_HEIGHT = 230
        self.rootGUI.geometry("%dx%d" % (self.APP_WIDTH, self.APP_HEIGHT))

    # GUI Create Widgets ###############################################################################################
    def __CreateButton(self, BUTTON_POSX, BUTTON_POSY, BUTTON_WIDTH, BUTTON_HEIGHT, BUTTON_TEXT,
                       BUTTON_BG_COLOR, BUTTON_FG_COLOR, FONT_NAME, BUTTON_FUNCTION):
        button = Button(self.rootGUI, text=BUTTON_TEXT, relief=FLAT, cursor="hand2",
                             bg=BUTTON_BG_COLOR, fg=BUTTON_FG_COLOR,
                             activebackground=BUTTON_BG_COLOR,
                             activeforeground=BUTTON_FG_COLOR,
                             overrelief=RIDGE,
                             font=FONT_NAME,
                             command=BUTTON_FUNCTION)
        button.place(x=BUTTON_POSX, y=BUTTON_POSY, width=BUTTON_WIDTH, height=BUTTON_HEIGHT)
        return button

    def __CreateLabel(self, LABEL_POSX, LABEL_POSY, LABEL_TEXT, LABEL_FONT, LABEL_TEXT_COLOR, LABEL_BG_COLOR):
        label = Label(self.rootGUI, text=LABEL_TEXT,
                                     font=LABEL_FONT,
                                     fg=LABEL_TEXT_COLOR, bg=LABEL_BG_COLOR)
        label.place(x=LABEL_POSX, y=LABEL_POSY)
        return label

    def __CreateLine(self, LABEL_POSX, LABEL_POSY, LABEL_WIDTH, LABEL_HEIGHT, LABEL_BG_COLOR):
        line = Canvas(self.rootGUI, width=LABEL_WIDTH, height=LABEL_HEIGHT, bg=LABEL_BG_COLOR, highlightthickness=0)
        line.place(x=LABEL_POSX, y=LABEL_POSY)
        return line

    # GUI Events #######################################################################################################
    def __EventTest(self):
        r = lambda: random.randint(0, 255)
        self.button2.config(bg='#%02X%02X%02X' % (r(), r(), r()),
                            activebackground='#%02X%02X%02X' % (r(), r(), r()))
        print('#%02X%02X%02X' % (r(), r(), r()))

    def __CloseButtonMouseOver(self, event):
        if event.type == "7":
            self.gui_close_label.config(bg=self.APP_CLOSE_BG_COLOR_HOVER, fg=self.APP_CLOSE_FG_COLOR_HOVER)     # Mouse entering
        else:
            self.gui_close_label.config(bg=self.APP_CLOSE_BG_COLOR, fg=self.APP_CLOSE_FG_COLOR)                 # Mouse leaving

    def __StartMove(self, event):
        """ StartMove() Setzt die APP Koordinaten auf die Eventposition des GUI Elements """
        print("StartMove >> APP PosX: " + str(self.APP_XPOS) + " - APP PosY: " + str(self.APP_YPOS))
        self.APP_XPOS = event.x
        self.APP_YPOS = event.y

    def __StopMove(self, event):
        """ StopMove() Setzt beim stoppen die Klassenwerte für x/y auf die neue Fensterposition """
        self.APP_XPOS = self.rootGUI.winfo_x()
        self.APP_YPOS = self.rootGUI.winfo_y()
        print("StopMove >> APP PosX: " + str(self.APP_XPOS) + " - APP PosY: " + str(self.APP_YPOS))

    def __OnMotion(self, event):
        """ OnMotion() Kümmert sich um die Berechnung der neuen Fensterkoordinaten beim ziehen mit der Maus """
        deltax = event.x - self.APP_XPOS                                        # Errechnet aus der MausPosition und der Bewegung der Maus ein "deltax"
        deltay = event.y - self.APP_YPOS                                        # Errechnet aus der MausPosition und der Bewegung der Maus ein "deltay"
        x = self.rootGUI.winfo_x() + deltax                                     # Addiert den errechneten "deltax" Wert auf die derzeitige GUI Position hinzu
        y = self.rootGUI.winfo_y() + deltay                                     # Addiert den errechneten "deltay" Wert auf die derzeitige GUI Position hinzu
        self.rootGUI.geometry("+%s+%s" % (x, y))                                # Setzt neue GUI Koordinaten
        # print("OnMove >> DeltaX: " + str(deltax) + " - DeltaY: " + str(deltay))
        # print("OnMove >> APP PosX: " + str(x) + " - APP PosY: " + str(y))

    # GUI Methods ######################################################################################################
    def GUIHide(self, event=False):
        """ GUIHide() versteckt die GUI """
        self.rootGUI.withdraw()
        print("GUI wurde versteckt.")

    def GUIShow(self):
        """ GUIShow() zeigt die GUI """
        print("GUI wird angezeigt.")
        self.SetMousPosition()                                                  # Updatet abgespeicherte Mausposition mit den aktuellen Koordinaten
        self.rootGUI.geometry("+%s+%s" % (self.MousPosX, self.MousPosY))        # Setzt neue GUI Koordinaten mit den MausKooridnaten
        self.rootGUI.update()                                                   # Updatet die GUI
        self.rootGUI.deiconify()                                                # Macht die GUI sichtbar
        self.rootGUI.lift()                                                     # Setzt die GUI in den Vordergrund

    # Settings #########################################################################################################
    def KeepOnTop(self, check=""):
        """ KeepOnTop() setzt bei True die GUI permanent in den Vordergrund, bei False nicht"""
        # Wurde die Funktion separat oder über die Checkbox aufgerufen?
        if check is True or False:
            print('KeepOnTop: ' + str(check))
            self.rootGUI.wm_attributes("-topmost", check)                       # Set Keep On Top on
        else:
            print('KeepOnTop: ' + str(self.keepontopVar.get()))
            self.rootGUI.wm_attributes("-topmost", self.keepontopVar.get())     # Set Keep On Top on

    def KeepInPosition(self):
        self.keepinpos.place(x=200, y=200)

    def SetMousPosition(self):
        """ GetMousPosition() updatet Variablen mit der aktuellen Mausposition bei Aufruf """
        self.MousPosX = self.rootGUI.winfo_pointerx()
        self.MousPosY = self.rootGUI.winfo_pointery()

    # GUI Run Functions
    def __set_appwindow(self):
        GWL_EXSTYLE = -20
        WS_EX_APPWINDOW = 0x00040000
        WS_EX_TOOLWINDOW = 0x00000080
        hwnd = windll.user32.GetParent(self.rootGUI.winfo_id())
        style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
        style = style & ~WS_EX_TOOLWINDOW
        style = style | WS_EX_APPWINDOW
        res = windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style)
        # re-assert the new window style
        self.rootGUI.wm_withdraw()
        self.rootGUI.after(10, lambda: self.rootGUI.wm_deiconify())

    def run(self):
        """ run() ist die Hauptschleife der GUI """
        print("GUI Mainloop wird gestartet.")
        self.rootGUI.mainloop()

# --- MyGUI  - Klasse -------------------------------------------------------------------------------
##########################################################################################################


# Main Function - Wird ausgeführt, wenn die *.py nicht importiert, sondern direkt ausgeführt wurde
if __name__ == '__main__':

    # Bibliothek wurde nicht importiert
    print("Hinweis: Bitte GUY.py Bibliothek importieren und nicht direkt ausführen.")
/includes/autofan/Settings.py

Code: Alles auswählen

# coding: utf8

##########################################################################################################
# --- Settings - Klasse -------------------------------------------------------------------------------
class Settings:

    def __init__(self):
        """ Konstruktor Funktion - lädt unterschiedliche Settings """
        self.__mainSettings()

    def __mainSettings(self):
        """ Globale Variablen """
        self.APP_NAME = "AutoFan"
        self.APP_VERSION = "v2.0"
        self.APP_BUILD = "180506"
        self.APP_TITLE = self.APP_NAME + " " + self.APP_VERSION + "." + self.APP_BUILD
        self.APP_HOTKEY = 'ctrl+<'

# --- Settings - Klasse -------------------------------------------------------------------------------
##########################################################################################################


# Main Function - Wird ausgeführt, wenn die *.py nicht importiert, sondern direkt ausgeführt wurde
if __name__ == '__main__':

    # Bibliothek wurde nicht importiert
    print("Hinweis: Bitte Settings.py Bibliothek importieren und nicht direkt ausführen.")

    # Beispiele für Settings
    settings = Settings()

    print("APP_NAME: " + str(settings.APP_NAME))
    print("APP_VERSION: " + str(settings.APP_VERSION))
    print("APP_BUILD: " + str(settings.APP_BUILD))
    print("APP_TITLE: " + str(settings.APP_TITLE))
    print("APP_HOTKEY: " + str(settings.APP_HOTKEY))
/includes/autofan/Hotkey.py

Code: Alles auswählen

# coding: utf8

##########################################################################################################
# --- Hotkey - Modul -------------------------------------------------------------------------------

from includes.autofan import Clipboard
import keyboard     # https://github.com/boppreh/keyboard

def AutoFanHotkey():
    """ Funktion wird durch Hotkey ausgelöst """
    print("======================\nHotkey wurde gedrückt.")
    clip.copy()             # Kopiert den aktuell markierten Text in die Zwischenablage
    AutoFanGUI.GUIShow()    # GUI wird angezeigt

def setHotkey(APP_HOTKEY):
    """ Funktion setzt den Hotkey """
    clip = Clipboard.Clipboard()                                    # Erstellt ein Clipboard Objekt
    keyboard.add_hotkey(APP_HOTKEY, AutoFanHotkey)         # Hotkey wird gesetzt

# --- Hotkey - Modul -------------------------------------------------------------------------------
##########################################################################################################
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Ist ist schrecklich verwirrend, wenn Du Dein Package `includes` nennst. Namensräume und Kapselung sind wichtige Prinzipien der Programmierung, werden aber durch Includieren und globale Variablen durchbrochen. Ersteres gibt es deshalb in Python generell nicht und zweiteres sollte man nicht verwenden. Alles was eine Funktion an Objekten braucht (außer Konstanten und Funktionen, Module, etc), müßen über Parameter in die Funktion gelangen. In AutoFanHotkey komment z.B. clip und AutoFanGUI aus dem Nichts, das widerspricht der Kapselung, nichts was außerhalb der Funktion verändert wird, sollte innerhalb der Funktion einen Einfluß haben. Genauso verhält es sich mit Modulen. Ein Modul ist eine Einheit, die nicht (unbemerkt) von ihrer Umgebung beeinflußt wird. Außerdem sollten Module sich nicht über Kreuz brauchen; dann sind es keine klar getrennten Einheiten und man sollte das Programm umstrukturieren.

Da es keine globalen Variablen geben sollte, muß bei Dir auch alles, was nach »if __name__ == '__main__':« kommt in eine Funktion main wandern, und nur die Funktion wird in main aufgerufen. Damit kommt man erst gar nicht in die Versuchung, in anderen Funktionen diese Variablen zu benutzen.

Die Settings-Klasse ist völlig überflüssig, weil ein Modul schon die Aufgabe übernimmt, Konstanten (und in Settings sollte wirklich nur Konstanten stehen) zusammenzufassen.

sys.path.append sollte nicht in Programmen und vor allem nicht in Untermodulen vorkommen. Das ändert einen globalen Zustand, der an anderer Stelle zu Problemen führen kann.

In GUI.py ist alles was in _Variablen oder _Fonts steht eigentlich konstante Klassenattribute. Methoden sollten auch nach Tätigkeiten benannt werden, wenn die Funktion nichts tut, dass ist es ja keine Funktion. __CreateButton, __CreateLine und __CreateLabel sind Methoden, die eigentlich nur TK-Objekte erzeugen, dabei aber den Aufruf verändern, so dass jemand der Tk kennt erst überlegen muß, was denn die Methoden machen. Hier ist es besser, die Objekte direkt zu erzeugen, statt indirekt über den Methodenaufruf.

Halte Dich an die Namenskonventionen. ALLES_KOMPLETT_GROSS sind ausschließlich Konstanten, Variablen, Methoden und Funktionen schreibt man klein_mit_unterstrich, Klassen MixCase.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@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:

Code: Alles auswählen

if __name__ == '__main__':
    main()
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… :-)
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
borsTiHD
User
Beiträge: 10
Registriert: Sonntag 29. April 2018, 08:11

Haha ohje... ich danke euch Beiden nochmal. :)
Unterm Strich. Ich muss nochmal komplett umdenken und besser gesagt verstehen wie ich das in Python umsetzen kann was ich vor habe.
Vorallem werde ich mir den verlinkten Style Guide anschauen.

Meine offensichtliches Problem derzeit ist... mit diesem Projekt Python zu lernen.
Weil ich habs nicht gelernt und das was ihr oben seht sind quasi meine ersten Schritte in Python.

Eine Frage hätte ich aber noch.
Mein AutoIT Script hat gestripped über 12000 Zeilen (ungestripped allein schon wegen meinen Kommentaren das doppelte :P ), weshalb ich dort mit vielen Includes meiner eigenen Sachen arbeite und einzelne Dateien für unterschiedliche Kategorien nutze (zb. meine GUI, GUI Events, API Abfragen, etc.).

Wie würde man eine solche Menge übersichtlich strukturieren?
Ich würde es gerne umgehen das ich hinterher auf eine *.py Datei mit 12000 Zeilen komme.

Und ja, ihr habt auch richtig bemerkt dass die externen Bibliotheken reinkopiert wurden.
Ich stehe derzeit vor zwei Problemen in dieser Richtung.
Das Programm schreibe ich als "Hobby" für mich und meine Kollegen um bei unserer Arbeit zu unterstützen.
Unsere Rechner haben leider nur Python 2.7 vorinstalliert und selbst wenn ich das nehmen würde, möchte ich nicht auf jeden Rechner bestimmte Bibliotheken nachinstallieren (da wir auch immer an einem neuen Rechner sitzen, müsste jeder User, jeden Tag alles neu installieren). Will damit sagen, das Programm muss hinterher out of the box laufen. Hierfür hätte ich gerne das Python Script als *.exe kompiliert, oder wenn wir es schaffen das ein Python 3 vorinstalliert wird, würde ich statt der *.exe gerne ohne dem nachinstallieren von Bibs die *.py direkt ausführen können.

Eine Virtuelenv wäre da wohl auch eine Lösung (oder sogar DIE Lösung?), aber als Anfänger erstmal zu unübersichtlich dort durchzublicken. :D
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Natuerlich kannst du Code in Python modularisieren. Die Kollegen hier haben einen etwas diffizielen semantischen Unterschied beleuchtet zwischen einem include und einem import. Aber grundsaetzlich ist ein import eigener Module genau das, was du brauchst und willst. Es funktioniert halt nicht so wie ein PHP-include, der so wirkt, als ob Dinge einkopiert werden in eine Datei. Sondern du hievst ein Modul in den Interpreter, und kannst darauf dann Funktionen, Klassen, Konstanten aufrufen.

Python zu einer einzelnen, monolithischen EXE zu kompilieren ist nicht so ohne weiteres moeglich. Es geht, wie hier beschrieben https://github.com/mobiusklein/cython_p ... er_example - aber das erfordert schon einiges an koennen.

Wenn ihr dein Tool als ein ZIP-Archiv verteilen koennt, dann kann man zB ein Einstiegsskript fertigen, das alle im Archiv existenten Pakete vom Interpreter auffindbar macht, und dann die main aufruft.
Benutzeravatar
borsTiHD
User
Beiträge: 10
Registriert: Sonntag 29. April 2018, 08:11

Danke auch dir für die Hilfe. :)
Ich werde mich da erst nochmal weiter in Python generell einlesen.
Auch muss ich noch irgendwie Ideen für mich selbst sammeln, wie ich die Modulaufteilung hinbekomme, ist für mich erstmal etwas schwierig in eine ganz andere Richtung denken zu müssen. :D

Oh und wir haben ein Netzlaufwerk auf das alle zugreifen können.
Mit meinem AutoIT Script hab ich bisher auch die kompilierte EXE dort abgelegt, die jeder von dort aus startet.
Performance einbußen messe ich kaum bis keine. :)
Das würde ich dann auch so mit der Python App bevorzugen.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Naja, dann leg halt einen Ordner an, und darin liegen dann auch 10 oder 20 oder wieviel auch immer notwendige Python-Dateien. Und eine start.bat oder aehnliches, die das Ding dann hochzieht.
Antworten