Anfängerfrage: Klasse greift auf Methode anderer Klasse zu

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
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Hallo Foristen,

Ich habe leider nur noch WirrWarr im Kopf. Und je mehr ich lese, desto verwirrter bin ich. Dies ist mein erstes Projekt mit einer objektorientierten Programmiersprache. Folgende Aufgabe:

Die Aufgabe ist ganz grob vereinfacht: Durch ein Kommando von außen (TCPIP-Kommunikation) wird eine GUI geupdatet (es ist ein TKinter-Label).
Nun habe ich für das Graphical User Interface (GUI) und der eigentlichen Kommunikation und Verarbeitung jeweils eine Klasse erstellt und daraus Instanzen im "Main"-Programm erzeugt.

Das Kommando von Außen, zum Beispiel SHOW:TEXT:"ABCD" kommt in der Verarbeitungsklasse an.
Aus dieser Klasse heraus soll nun das Label der GUI den übertragenen Text anzeigen. Aber ich kann nicht aus der Verarbeitungsklasse auf die Methode "ShowText" der GUI-Klasse zugreifen.
Laut meinem Verständnis ist das ja auch nicht möglich, da die Verarbeitungsklasse ja nicht weiß, welche Instanz der GUI Klasse gemeint ist.

In meinem Main-Programm kann ich auf alle Variablen und Methoden der Klassen zugreifen. Können Klassen nicht direkt auf andere Methoden und Variablen einer anderen Klasse bzw, Instanz zugreifen, sondern muss das immer über den Umweg des MAIN-Programms außerhalb der Klassen erfolgen? Wenn die Antwort "Nein" lautet, wie kann man das lösen?

P.S.: Da mein Programm derzeit sehr verbastelt ist, habe ich auf Codebeispiele verzichtet. Kommunikation, TKinter und die Sachen drum herum laufen alle schon ganz gut.
Nur die Kommunikation zwischen den einzelnen Bereichen bekomme ich nicht hin.

Danke Vorab, blutec
Sirius3
User
Beiträge: 18265
Registriert: Sonntag 21. Oktober 2012, 17:20

Instanzen und Methoden sind nichts anderes als Variablen und Funkionen.
Wenn Du also weißt, wie Du eine Funktion mit einer Variable aufrufen mußt, damit Du innerhalb der Funktion auch Zugriff auf die Variable hast, dann weißt Du auch, was Du tun mußt, um innerhalb einer Methode auf eine Methode einer anderen Instanz zuzugreifen.

So wie Du Dein Problem beschreibst, kann ich mir schwer etwas unter einer Verarbeitungsklasse vorstellen. Für GUIs ist es sinnvoll, alles in Klassen zu packen, weil sich diese Zustand über das Ende eines Events hinweg merken können müssen.
Hast Du bei Deiner Verarbeitungsklasse auch Zustand? Und wenn ja, dann steuert ja die GUI die Verarbeitung, also wartet auf ein Socket-Event und verarbeitet dieses dann. Dann frage ich mich aber, was Du damit meinst, dsas das MAIN-Programm Zugriff auf alles hat?

Oder hast Du etwa die Socket-Verarbeitung in einem separaten Thread laufen? Dann darf die "Verarbeitungsklasse" gar nicht direkt auf die GUI zugreifen, sondern die Kommunikation muß über eine Queue laufen, und somit mußt Du nur der Verarbeitungsklasse und der GUI-Klasse beim __init__ jeweils die Queue übergeben.

Zum Schluß noch: niemand außer der GUI-Klasse verändert die GUI. Der Ablauf ist also, dass sich die GUI die Werte aus der Verarbeitungsklasse holt um sie selbst darzustellen. Die Verarbeitungsklasse darf gar nichts von einer GUI wissen.

Diese vielen Fragen lassen sich am besten anhand des Codes beantworten, daher wäre es sinnvoll, dass Du den hier postest.
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Danke für die rasche Antwort. Ich habe mittlerweile noch einiges ausprobiert und den Code etwas aufgeräumt. Er ist etwas umfangreich, deshalb hier nur die relevanten Auszüge, um mein Problem zu verdeutlichen:

Hier die "verkürzte" TKinter GUI mit der Methode "updatelabels"

Code: Alles auswählen


######### GUI TKinter ###########

class GUI(tk.Frame):

    serialnumber = ""
    seriallength = 10
    serialstoredinbuffer = False
    testsamplemodeactive = False
    pf_color = {'green': '#00A587', 'grey': '#425563', 'orange': '#FF6A39'}  # Dictionary of PF color theme

    def __init__(self):
        super().__init__()
        self.initUI()
        print("[STARTING] server is starting...")
        listenthread = threading.Thread(target=SERVERSECTION.startcommunication(self))
        listenthread.start()

    def initUI(self):

        self.seriallabeltext = tk.StringVar()  # Binding variable to display serial number
        self.statuslabeltext = tk.StringVar()  # Binding variable to display status message
        self.master.title("GUI")
        self.pack(fill=tk.BOTH, expand=1)
        ...

        # Buttons
        self.button_repeat = tk.Button(self, command=lambda: self.repeatscan())
        self.button_repeat.configure(image=self.bild, borderwidth=1)
        ...
        
        #Entryfield
        self.eingabe = tk.Entry(self, font=('Arial', 12), bg='white', fg=self.pf_color['grey'], background='white', justify='center', borderwidth=1, relief='solid')
        self.eingabe.focus()
        self.eingabe.bind('<Return>', self.getscan)
        ...
         def updatelabels(self, bufferstatus):

        if bufferstatus == "empty":
            self.label_status.config(fg=self.pf_color['grey'])
            self.statuslabeltext.set("Please scan")
        elif bufferstatus == "stored":
            self.label_status.config(fg=self.pf_color['grey'])
            self.statuslabeltext.set("SERIALNUMBER \nSTORED")
       ...
        else:
            self.label_status.config(fg='red')
            self.statuslabeltext.set("Error: Unkown command")


Und hier die Verarbeitung des über TCPIP empfangenen Kommandos:

Code: Alles auswählen

          # ***********  Command section ****************************************

                    sbcommand = str.split(str.upper(msg), ":")  # Empfangenen String zerlegen

                    # ----------------------------- GET -----------------------------------
                    if sbcommand[0] == "GET":

    			......
    			
                    elif sbcommand[0] == 'SET':
                    
                   # ...... mach was
                    
                        if sbcommand[1] == 'LEN':
                     
                   #  ..... mach was anderes
                   
                    elif sbcommand[0] == 'ACK':
                  
                        
                        #HIER IST DAS PROBLEM: GUI.updatelabels kann nicht aufgerufen werden aus dieser 
                        GUI.serialnumber = "-"
                        GUI.updatelabels("empty")
                    # ----------------------------- DISCONNECT -----------------------------------
                    elif sbcommand[0] == 'END':
                        conn.send(bytes(f"Disconnect {addr}", FORMAT))  #### todo
                        connected == False
                    # ----------------------------- ERROR -----------------------------------
                    else:
                        print("Error")

                    print(sbcommand[0])

                except (ValueError):
                    conn.send(bytes(f"!Length error! Expected {msg_length}", FORMAT))  #### todo

        conn.close()


In der Sektion, in der ich die Kommandos verarbeite, möchte ich die Methode updatelabels aufrufen, um dort eine Veränderung der GUI zu bewirken. Dort sollen die Labels zurückgesetzt werden, wenn von einem externen Gerät das Kommando "ACK" kommt. Fehlermeldung: TypeError: updatelabels() missing 1 required positional argument: 'bufferstatus'
Wenn ich nun aber "self" als Argument vorne ergänze, kommt "NameError: name 'self' is not defined".

Hier noch die "MAIN"

Code: Alles auswählen

def main():
    root = tk.Tk()
    root.geometry("800x480")
    App = GUI()
    Server = SERVERSECTION()
    root.mainloop()

if __name__ == '__main__':
    main()
Wie kann ich also die Methode GUI.updatelabels aus der Klasse Serversection aufrufen?

Danke für eure Hilfe

Gruß
blutec
Sirius3
User
Beiträge: 18265
Registriert: Sonntag 21. Oktober 2012, 17:20

Alle Punkte, die ich befürchtet hatte, bestätigen sich.
Bitte zeige den ganzen Code, damit ich auf die Probleme richtig eingehen kann.

Was man schon sieht:
Nur Konstanten schreibt man komplett GROSS. GUI und SERVERSECTION scheinen Klassen zu sein, schreibt man Gui und ServerSection, wobei die Frage ist, welche Sektion des Servers das ist, oder ob das wohl der komplette Server ist?

Klassenattribute sind kein Ersatz für Instanzattribute. Der Frame bekommt root gar nicht als Attribut, greift aber doch auf ein self.master zu.
Alles was in initUI steht könnte auch direkt in __init__ stehen. SERVERSECTION scheint gar keine richtige Klasse zu sein, weil nur eine statische Methode startcommunication davon benutzt wird. Und wie schon im vorigen Beitrag geschrieben, sollte die Verarbeitungsklasse gar nichts von der GUI wissen, startcommunication also nicht self übergeben bekommen.
Ein Frame sollte nicht den master ändern, und sollte sich auch nicht selbst im master platzieren.

Bei der Verarbeitung der Kommandos benutzt Du recv falsch (obwohl Du das nicht zeigst, bin ich mir sicher, dass Du das von irgendeinem Schrottbeispiel aus dem Internet kopiert hast). Man ruft keine Methoden über die Klasse auf, Du benutzt also str.split und str.upper falsch.

Code: Alles auswählen

sbcommand_parts = msg.upper().split(":")
Benutze keine kryptischen Abkürzungen. Was soll sb bedeuten?
Bei "# HIER IST DAS PROBLEM" ist das Problem, dass Du direkt auf die Klasse GUI zugreifst, und nicht über die Instanz. Würdest Du auf die Instanz zugreifen, wäre das Problem, dass das eine Verarbeitungsklasse gar nicht darf. Und drittens darfst Du aus einem Thread heraus die GUI nicht ändern.
conn.send ist falsch, weil Du auch schon conn.recv falsch verwendest. So funktioniert TCP nicht. Das ist ein low-level-Protokoll, wo man sich exakt an die Regeln halten muß. Da das zu kompliziert ist, benutzt man higher-level-Methoden, die Du per conn.makefile bekommst.
Benutzeravatar
__blackjack__
User
Beiträge: 14030
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@blutec: Ergänzend: Zwischen Worte in Namen (ausser Klassen) gehören Unterstriche. Dennwennmanallessoaneinanderklebtistdasschwerlesbar. `serialstoredinbuffer`, `testsamplemodeactive` & Co sind ich schlecht zu lesen.

Gibt ``SERVERSECTION.startcommunication(self)`` tatsächlich ein aufrufbares Objekt zurück? Oder sollte der Aufruf davon eigentlich im neuen Thread passieren?

Bei ``tk.Button(self, command=lambda: self.repeatscan())`` ist das ``lambda`` überflüssig wenn man einfach direkt ``self.repeatscan`` als `command` übergibt.

``connected == False`` einfach so als Ausdruck in einer eigenen Zeile macht sicher nicht das was Du denkst. Es macht gar nichts, ausser ein bisschen Rechenzeit für den Vergleich zu verbrauchen.

Wenn man dauernd auf ``sbcommand[0]`` zugreift, würde es Sinn machen das gleich beim Aufteilen an einen eigenen Namen zu binden:

Code: Alles auswählen

                    command, *arguments = message.upper().split(":")
                    if command == "GET":
                        ...
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Ohje, erst mal danke für die vielen Anmerkungen und die Analyse meines Code-Auszugs. Entschuldigt die späte Antwort, da ich erst jetzt wieder daran arbeiten kann.
Für mich als Anfänger in Python ist das sehr problematisch, wenn man sich an Beispiele hält und z.B. die Kommunikation so aufbaut wie in einem sehr ausführlichem Video beschrieben, um dann gesagt zu bekommen, dass das "Schrott" ist.

1. Da ich vorher nur "C" programmiert habe, muss ich mich an die neuen Konventionen (GROß-Kleinschreibung, CamelCase etc.) gewöhnen, da doch recht unterschiedlich zu "C".
2. "Klassenattribute sind kein Ersatz für Instanzattribute." -> Darüber muss ich nachdenken, d.h. dass ich am besten alles als Instanzattribute definiere? Von der Klasse GUI gibt es nur eine Instanz. Man hat mir aber geraten, dass ich daraus trotzdem eine Klasse machen soll.
3. Alles was in initUI steht könnte auch direkt in __init__ stehen" -> Das ist wirklich ein komnisches Beispiel aus dem Internet und ich habe es schon umgestellt.
4. "SERVERSECTION scheint gar keine richtige Klasse zu sein..." -> von Serversection gibt es auch nur eine einzige Instanz. Wäre es besser, dies nicht in eine Klasse zu packen sondern einfach als Funktion in das Programm zu schreiben, also in das "MAIN" (ich kann es gerade nicht besser beschreiben, sorry).
5. Im GUI wird ein Eingabefeld ausgelesen und soll per TCPIP an einen PC geschickt werden. Somit dient GUI nicht nur zur Anzeige, sondern muss auch was zurückgeben. Der Inhalt dieses Eingabefelds muss also irgendwie an den Server-Code übermittel werden. Ich bekomme es aber nicht hin.
Es gibt derzeit drei Bereiche: App, Server, und Main. App und Server sind jeweils eine Instanz von einer Klasse (GUI, SERVERSECTION). Der Server übernimmt hier auch die Verarbeitung der Kommandos (die will ich eigentlich noch in einer anderen Klasse auslagern.
HAUPTPROBLEM von mir ist vermutlich, dass ich es nicht raffe, wie die Klassen untereinander Daten austauschen können, also wie die Daten des Eingabefelds aus der GUI-Klasse in die Klasse Server übernommen werden. Mache ich das dann mit App.entryfield.get oder GUI.serialnumber oder oder ... nichts von meinen Versuchen klappt.
Ich finde im Inet zahlreiche Erklärungen zu Klassen, aber nicht, wie man von einer Klasse auf dei Daten einer anderen Klasse zugreift.

6. "Da das zu kompliziert ist, benutzt man higher-level-Methoden, die Du per conn.makefile bekommst." -> Das verstehe ich nun auch nicht. Bitte Details. Meinst du damit das Einbinden einer Bibliothek? Sind die zahlreichen anderen Beispiele im Internet denn ALLE falsch?

Jetzt Versuche ich erst mal, die Sachen zu reparieren, die ich geistig erfassen kann :-) Aber natürlich greift hier eins in das Andere. Sobald ich meinen Code geändert habe, werde ich ihn zur Verfügung stellen.

Ich werde mich jetzt erst mal daran machen, sauber mit den Instanzen zu arbeiten. Vielleicht fällt ja dabei der Groschen. Aber das mit dem Zugriff von einer Klasse auf die andere kann mir vielleicht jemand noch mal anhand eines einfachen Beispiels erklären, das wäre super.

Gruß Blutec
Benutzeravatar
__blackjack__
User
Beiträge: 14030
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@blutec: Ad 1.: Konstanten komplett gross geschrieben und alles andere klein mit Unterstrichen ist doch auch bei sehr vielen C-Projekten so. Da ”unterscheidet” sich nur PascalCase bei Klassen — weil es dieses Sprachkonstrukt in C nicht gibt.

Ad 4.: Die Frage ob es eine Klasse ist oder nicht hängt nicht davon ab wie viele Exemplare es davon gibt, sondern ob das tatsächlich zusammegehörige Daten/Zustand und Funktionen in einem Objekt zusammenfasst.

Ad 5.: Auf Daten von Objekten zuzugreifen ist nichts besonderes. Es gilt ganz einfach das Funktionen und Methoden alles was sie ausser Konstanten benutzen als Argument(e) übergeben bekommen. Wenn man also in einer Funktion oder Methode auf Attribute von einem Objekt zugreifen will, dann muss dieses Objekt direkt oder indirekt als Argument übergeben werden. Bei GUIs ist eine Trennung zwischen GUI und Programmlogik üblich und die Programmlogik sollte nichts von der GUI wissen müssen. Daraus ergibt sich das die GUI die Programmlogik beim erstellen übergeben bekommt und als Attribut speichert. Absenden eines Eingabafeld-Inhalts würde dann also das Eingabefeld auslesen und die entsprechende Methode auf der Programmlogik aufrufen.

Für den umgekehrten Weg der Kommunikation braucht man eine Queue die man dem Thread der Programmlogik übergibt und im GUI-Code regelmässig mit Hilfe von `after()` abfragt ob da etwas angekommen ist was angezeigt werden muss.

Ad 6.: Ich weiss, schwer zu fassen/verdauen aber was Low-Level-Socket-Programmierung angeht ist da draussen wirklich fast alles falsch.

`conn.makefile` meint keine Bibliothek sondern die `makefile()`-Methode auf `socket`-Objekten. Da bekommst Du ein Dateiobjekt mit den üblichen Methoden die Dateiobjekte in Python haben und musst Dich nicht mehr damit herumschlagen Daten zu puffern oder sicherzustellen, dass auch alles Bytes gesendet wurden. Also ungefähr so etwas hier:

Code: Alles auswählen

    def ...(self):
        ...
        with socket(...) as connection:
            out_file = connection.makefile("w", encoding=ENCODING)
            for line in connection.makefile("r", encoding=ENCODING):
                command, *arguments = line.upper().split(":")
                if command == "GET":
                    ...
                elif command == "END":
                    out_file.write(f"Disconnect {address}\n")
                    break
                else:
                    LOG.error(
                        "unknown command %r with arguments %r)",
                        command,
                        arguments,
                    )
        
        self.connected = False
        ...
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Antworten