OOP: Grundsätzlich instanzieren? Nötig oder Stilfrage?

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.
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

Liebe Pythonistas,
ich durchdenke gerade eine grundsätzliche Frage: Ist es sinnvoll, nötig, wichtig ein Objekt einer Klasse zu erstellen, auch dann, wenn es um das "Hauptprogramm" geht und der Code nicht wiederverwehrtet werden soll und keinerlei Vererbung geplant ist?
Es hört sich selbst in meinen Ohren kryptisch an - ich gebe lieber ein Beispiel bzw. zwei im Vergleich. Ich nutze hierbei einmal tkinter, aber es soll mir nicht um ein GUI gehen, sondern einfach meine Frage beleuchten.
Version 1 - Klasse erstellen, Objekt initialisieren:

Code: Alles auswählen

import tkinter as tk

class App:
    """Hauptprogramm ruft tkinter auf und erstellt einen Button"""

    def __init__(self):
        self.root = None
        self.button = None

    def start(self):
        self.root = tk.Tk()
        self.button = tk.Button(master=self.root, text="Klick mich", command=lambda: print("Button geklickt."))
        self.button.pack()
        self.root.mainloop()

if __name__ == "__main__":
    app = App()
    app.start()
In diesem Beispiel wird also die Klasse App angelegt und im Hauptprogramm ein Objekt app instanziert und die Methode start() aufgerufen.

Version 2 - Klasse erstellen und direkt ausfüren, ohne ein Objekt zu erzeugen.

Code: Alles auswählen

import tkinter as tk

class App:
    """Hauptprogramm ruft tkinter auf und erstellt einen Button"""

    root = None
    button = None

    def start():
        App.root = tk.Tk()
        App.button = tk.Button(master=App.root, text="Klick mich", command=lambda: print("Button geklickt."))
        App.button.pack()
        App.root.mainloop()

if __name__ == "__main__":
    App.start()
Hier werden eben die Klassenvariablen genutzt. Beide Programme tun das gleiche, wobei ich die Version 2 "logischer" finde, denn wozu noch ein Objekt erzeugen, wenn ich alles über Klasse regeln kann. Instanzierung erscheint mir nur dort notwendig, wo mehrere Objekte einer Klasse benötigt werden oder vererbt werden soll usw.
In Code-Beispielen findet man eigentlich immer nur die Version 1. Klassenvariablen werden häufig nur mit einem Nebensatz abgetan und "nackte" Klassen (ohne expliziten __init__ Aufruf) offenbar nie genutzt. Aber warum? Ist das "schlechter" Stil? Spricht etwas dagegen, wie in der Variante 2 vorzugehen?

Vielleicht sehe ich den Wald vor lauter Bäumen nicht mehr und mir kann jemand auf die Sprünge helfen?
Benutzeravatar
/me
User
Beiträge: 3555
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Im zweiten Beispiel würdest du ja überhaupt keine Klasse brauchen. Das kann man auch einfach so in eine Funktion packen. Klassen sind kein Selbstzweck.
Benutzeravatar
Dennis89
User
Beiträge: 1154
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

die Frage gabs neulich so ähnlich schonmal (wenn ich es richtig deute).
Dazu hatte @__deets__ das hier geschrieben

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Onomatopoesie: Klassen sind Vorlagen für Objekte, keine Container für einfache Funktionen. Das wären Module. Klassen sind dazu da Exemplare zu erstellen und dafür braucht man eine sinnvolle `__init__()` die auch ausgeführt wird. Die `__init__()` im ersten Beispiel ist auch nicht sinnvoll, weil die im Grunde ja nichts macht wenn alles in der `start()`-Methode steckt. Wobei auch Klassen mit einer sinnvollen `__init__()` und nur einer weiteren Methode die auf jeden Fall aufgerufen werden wird, in der Regel ein „code smell“ sind, dass man da eine einfache Funktion unnötig kompliziert als Klasse formuliert hat. Es gibt Situationen wo das Sinn machen kann, die sind aber nicht der Regelfall.

Das Programm würde man einfach als Funktion schreiben:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk


def main():
    root = tk.Tk()
    tk.Button(
        root, text="Klick mich", command=lambda: print("Button geklickt.")
    ).pack()
    root.mainloop()


if __name__ == "__main__":
    main()
Wenn als Klasse, dann eher so:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        tk.Button(
            self, text="Klick mich", command=lambda: print("Button geklickt.")
        ).pack()


def main():
    App().mainloop()


if __name__ == "__main__":
    main()
Wobei das in dieser ”Ausbaustufe” nicht wirklich Sinn macht solange da keine weiteren Methoden dazu kommen. Und zwar echte Methoden, also welche die auch auf den Zustand des Objekts zugreifen müssen, und nicht einfach nur Funktionen sind, die man in die Klasse steckt.

Es gibt Programmiesprachen wo man Objekte direkt erzeugen kann, ohne den Umweg über eine Klasse als Vorlage, also wo man Singletons ohne Klasse erstellen kann. Vielleicht es ja so etwas was Dir vorschwebt(e). Das gibt es in Python allerdings nicht, da ist der Weg für so etwas über eine Klasse zu gehen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@Dennis89: Danke für den Link zu dem Thread. Das geht noch etwas tiefer, als meine eigenen Gedanken waren, aber genau darauf läuft es letztlich hinaus. Es scheint also eine Designentscheidung zu sein - das Wichtigste ist wohl, dass es durchdacht sein muss, gut zu pflegen und für alle (im Team) einheitlich und verständlich sein muss.

@BlackJack: Du zauberst selbst bei schnellen Code-Beispielen immer einen wunderbar strukturierten und eleganten Code. Danke dafür. Ich versuche, es mir zu Eigen zu machen, auch wenn es wohl noch ein weiter Weg ist. :roll:
Du hast natürlich recht - in diesem Fall wäre es am einfachsten, einfach funktional (oder sagt man auch in Python "prozedural"?) zu arbeiten. Aber gerade bei der Arbeit mit tkinkter stehe ich dann irgendwann vor dem Problem, dass ich eine globale Variable verwenden möchte (was ich aus meinen Basic-Zeiten einfach so gewohnt bin), weil ich keine andere Lösung mehr sehe (außer eben eine Klasse zu erstellen). Ich konstruiere einmal schnell ein Beispiel, um zu verdeutlichen, was ich meine. Ich gehe dabei davon aus, dass der gesamte Code funktional entwickelt wurde - bisher also keine Klasse/Vererbung stattgefunden hat. Desweiteren gehe ich davon aus, dass Funktionen durch Buttons aufgerufen werden, die auf gemeinsame Variablen zugreifen und diese verändern müssen.

Variante mit globaler Variable:

Code: Alles auswählen

import tkinter as tk

# globale Variable (total verpönt - die Gründe kenne ich ...)
summe_klicks = 0

def main():
    root = tk.Tk()
    button_1 = tk.Button(master=root, text="Button 1", command=button1_funktion).pack()
    button_2 = tk.Button(master=root, text="Button 1", command=button2_funktion).pack()
    button_zeige_klicks = tk.Button(master=root, text="Zeige klicks", command=button_zeige_klicks_funktion).pack()
    root.mainloop()

def button1_funktion():
    global summe_klicks
    summe_klicks += 1
    # return nicht möglich, da durch command in button aufgerufen?

# die zweite Funktion tut das Gleiche wie bei button1 - es geht mir hier nur um das Prinzip, ich weiß, dass es so nicht sinnvoll ist
def button2_funktion():
    global summe_klicks
    summe_klicks += 1

def button_zeige_klicks_funktion():
    print(f"Es wurden {summe_klicks} Klicks auf verschiedene Buttons getätigt")


if __name__ == "__main__":
    main()

Variante mit Klasse, um einfach "globale" Variablen zu speichern:

Code: Alles auswählen

import tkinter as tk

# eine Klasse, um die globale Variable zu vermeiden
class SummeKlicks:
    summe_klicks = 0

def main():
    root = tk.Tk()
    button_1 = tk.Button(master=root, text="Button 1", command=button1_funktion).pack()
    button_2 = tk.Button(master=root, text="Button 1", command=button2_funktion).pack()
    button_zeige_klicks = tk.Button(master=root, text="Zeige klicks", command=button_zeige_klicks_funktion).pack()
    root.mainloop()

def button1_funktion():
    SummeKlicks.summe_klicks += 1

def button2_funktion():
    SummeKlicks.summe_klicks += 1

def button_zeige_klicks_funktion():
    print(f"Es wurden {SummeKlicks.summe_klicks} Klicks auf verschiedene Buttons getätigt")


if __name__ == "__main__":
    main()

Hier eine Klasse zu "missbrauchen" gefällt mir natürlich nicht, aber wie soll man Variablen in eine "funktionalen Umgebung" integrieren, die vielen Funktionen zur Verfügung stehen sollen, wenn man kein return nutzen kann? Ich sehe da gerade nur den Weg, auf globale Variablen auszuweichen. Oder was übersehe ich hier?
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Globale Variablen sind schlecht.
Und auch das im zweiten Beispiel ist eine globale Variable. Auch wenn dein Kommentar das anders suggeriert. Sie wird ja keine, nur weil du sie in eine (hier überflüssige) Klasse steckst. Du missbrauchst die Klasse als Datencontainer. Dafür ist sie nicht da. Mit der Argumentation, dass das möglicherweise in Ordnung ist, kannst du ja auch ein dict auf Modulebene nehmen und sagen, das ist keine globale Variable, weil du ja nur einen Wert im dict änderst.

Richtig wäre den Wert dort aufzubewahren, wo er auch hin gehört - also Teil des Objektes ist, das diesen Wert auch braucht.
Wenn du mehr als einen Button in einem Fenster hast und die einen Wert hochzählen sollen, dann wäre es sinnvoll, die beiden Buttons in ein Objekt zu stecken, dass eben diese Variable hat. Keine Ahnung wie das tk ist, aber in pyQt würdest du eben von QWidget (oder QMainWindow) erben die Klasse entsprechend erweitern. Sprich: das Fenser oder das Widget, in dem die Buttons platziert sind, ist das Objekt, das den Wert hat.
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@sparrow: Das ist mir klar. Hier liegen ja die Vorteile von OOP. tkinter lässt sich aber funktional programmieren und bei kleinen Projekten sehe ich auch nicht den Zwang, grundsätzlich auf OOP zu gehen. Die Frage ist aber die, wie ich solche Probleme lösen kann, ohne globale Variablen zu nutzen oder Klassen zu "missbrauchen", denn natürlich ist es dann letztlich auch eine globale Variable (wenn auch hübsch verpackt mit Schleife). Und ja - alle "komplexen" Objekte wie Listen, Dictionaries oder eben eigene Objekte könnten herhalten, weil sie global funktionieren. Aber die Frage ist eben, ob ich solche Probleme dann rein funktional (ohne globale Variablen) sinnvoll lösen kann.
Benutzeravatar
Dennis89
User
Beiträge: 1154
Registriert: Freitag 11. Dezember 2020, 15:13

Das würde dann wohl eher so aussehen:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        tk.Button(self, text="+ 1", command=self.add_button_click).pack()
        tk.Button(self, text="Zeige klicks", command=self.show_clicks).pack()
        self.label = tk.Label(self, text="")
        self.label.pack()
        self.clicks = 0

    def add_button_click(self):
        self.clicks += 1

    def show_clicks(self):
        self.label.configure(text=self.clicks)


def main():
    App().mainloop()


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@Dennis89: Klar - das wäre dann mit OOP und Vererbung der Tk-Klasse umgesetzt. Die Frage ist aber, ob ich so etwas funktional umsetzen kann, ohne auf globale Variablen zurückzugreifen.

(Für den Moment gehe ich einmal davon aus: Nein. :D )
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Statt globaler eben die Variablen in einer Instanz zu benutzen. Das ist schon ein starker Grund fuer ein Objekt. Denn dadurch ist das gekapselt, und der Nachteil der globalen Variable, von ueberall erreichbar zu sein, entfaellt.

Deine Frage ist ja durchaus berechtigt, und wenn man ein bisschen stringenter an das Ganze herangeht, dann wird an so einer Stelle ein "Singleton-Pattern" eingesetzt. Ich halte das ueblicherweise fuer ueberkandidelt, aber es drueckt aus, was dir hier Unbehagen bereitet: es sollte eigentlich nur eine Instanz geben, kann man das erzwingen?

Ich wuerde also *nicht* deinen Weg hier gehen, weil es ein Missbrauch der Klasse als Namnesraum darstellt, und das wiegt fuer mich schwerer, als der doch sehr theoretische Fall, dass da jemand X App-Objekte erzeugt.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Onomatopoesie: Du hast in Deinem Beispiel übrigens den Wert `None` an die Namen die mit `button_` beginnen gebunden. `pack()` gibt nicht das `Button`-Objekt zurück, sondern ”Nichts”.

Man kann das simple Beispiel mit Tk noch ohne eigene Klasse lösen weil Tk Objekte bietet die genau einen Wert kapseln (und netterweise auch das „observable“-Entwurfsmuster implementieren was an anderer Stelle (`Label`) auch gleich genutzt wird und man sich die Funktion zur Anzeige des Zählerstands sparen kann):

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial


def button1_funktion(klickzaehler_var):
    klickzaehler_var.set(klickzaehler_var.get() + 1)


#
# Die zweite Funktion tut das Gleiche wie bei button1 - es geht mir hier nur um
# das Prinzip, ich weiß, dass es so nicht sinnvoll ist
#
def button2_funktion(klickzaehler_var):
    klickzaehler_var.set(klickzaehler_var.get() + 1)


def main():
    root = tk.Tk()
    klickzaehler_var = tk.IntVar()
    for nummer, aktion in enumerate([button1_funktion, button2_funktion], 1):
        tk.Button(
            root,
            text=f"Button {nummer}",
            command=partial(aktion, klickzaehler_var),
        ).pack()

    frame = tk.Frame(root)
    tk.Label(frame, text="Es wurden ").pack(side=tk.LEFT)
    tk.Label(frame, textvariable=klickzaehler_var).pack(side=tk.LEFT)
    tk.Label(frame, text=" Klicks auf verschiedene Buttons getätigt.").pack(
        side=tk.LEFT
    )
    frame.pack()

    root.mainloop()


if __name__ == "__main__":
    main()
Eine saubere funktionale Lösung ohne globale Variablen würde man in anderen Sprache mit Closures machen. Das geht in Python noch gar nicht so lange ohne komische Notlösungen sondern erst seit es ``nonlocal`` gibt:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial


def erstelle_zaehler():
    stand = 0

    def hole():
        nonlocal stand
        return stand

    def erhoehe():
        nonlocal stand
        stand += 1

    return hole, erhoehe


def button1_funktion(label, hole_zaehlerstand, erhoehe_zaehlerstand):
    erhoehe_zaehlerstand()
    label["text"] = hole_zaehlerstand()


#
# Die zweite Funktion tut das Gleiche wie bei button1 - es geht mir hier nur um
# das Prinzip, ich weiß, dass es so nicht sinnvoll ist
#
def button2_funktion(label, hole_zaehlerstand, erhoehe_zaehlerstand):
    erhoehe_zaehlerstand()
    label["text"] = hole_zaehlerstand()


def main():
    root = tk.Tk()
    hole_zaehlerstand, erhoehe_zaehlerstand = erstelle_zaehler()

    frame = tk.Frame(root)
    tk.Label(frame, text="Es wurden ").pack(side=tk.LEFT)
    zaehlerstand_label = tk.Label(frame, text=hole_zaehlerstand())
    zaehlerstand_label.pack(side=tk.LEFT)
    tk.Label(frame, text=" Klicks auf verschiedene Buttons getätigt.").pack(
        side=tk.LEFT
    )

    for nummer, aktion in enumerate([button1_funktion, button2_funktion], 1):
        tk.Button(
            root,
            text=f"Button {nummer}",
            command=partial(
                aktion,
                zaehlerstand_label,
                hole_zaehlerstand,
                erhoehe_zaehlerstand,
            ),
        ).pack()

    frame.pack()

    root.mainloop()


if __name__ == "__main__":
    main()
Der Zähler hat nur zwei Funktionen die ihn beeinflussen, aber auch da ist es schon ein bisschen nervig, dass man zwei Werte für den einen Zähler an die Button-Funktionen übergeben muss. Die meisten funktionalen Programmiersprachen haben auch einen Verbunddatentyp wo man einzelne Werte zusammenfassen kann und den Bestandteilen Namen geben kann. Also so etwas wie `collections.namedtuple()`:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from collections import namedtuple
from functools import partial

Zaehler = namedtuple("Zaehler", "hole erhoehe")


def erstelle_zaehler():
    stand = 0

    def hole():
        nonlocal stand
        return stand

    def erhoehe():
        nonlocal stand
        stand += 1

    return Zaehler(hole, erhoehe)


def button1_funktion(label, zaehler):
    zaehler.erhoehe()
    label["text"] = zaehler.hole()


#
# Die zweite Funktion tut das Gleiche wie bei button1 - es geht mir hier nur um
# das Prinzip, ich weiß, dass es so nicht sinnvoll ist
#
def button2_funktion(label, zaehler):
    zaehler.erhoehe()
    label["text"] = zaehler.hole()


def main():
    root = tk.Tk()
    zaehler = erstelle_zaehler()

    frame = tk.Frame(root)
    tk.Label(frame, text="Es wurden ").pack(side=tk.LEFT)
    zaehlerstand_label = tk.Label(frame, text=zaehler.hole())
    zaehlerstand_label.pack(side=tk.LEFT)
    tk.Label(frame, text=" Klicks auf verschiedene Buttons getätigt.").pack(
        side=tk.LEFT
    )

    for nummer, aktion in enumerate([button1_funktion, button2_funktion], 1):
        tk.Button(
            root,
            text=f"Button {nummer}",
            command=partial(aktion, zaehlerstand_label, zaehler),
        ).pack()

    frame.pack()

    root.mainloop()


if __name__ == "__main__":
    main()
Nur würde man das in Python so nicht schreiben, weil das für Python-Verhältnisse eine sehr ungewöhnliche Art wäre etwas zu schreiben was man mit einer Klasse löst:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial


class Zaehler:
    def __init__(self):
        self.stand = 0

    def erhoehe(self):
        self.stand += 1


def button1_funktion(label, zaehler):
    zaehler.erhoehe()
    label["text"] = zaehler.stand


#
# Die zweite Funktion tut das Gleiche wie bei button1 - es geht mir hier nur um
# das Prinzip, ich weiß, dass es so nicht sinnvoll ist
#
def button2_funktion(label, zaehler):
    zaehler.erhoehe()
    label["text"] = zaehler.stand


def main():
    root = tk.Tk()
    zaehler = Zaehler()

    frame = tk.Frame(root)
    tk.Label(frame, text="Es wurden ").pack(side=tk.LEFT)
    zaehlerstand_label = tk.Label(frame, text=zaehler.stand)
    zaehlerstand_label.pack(side=tk.LEFT)
    tk.Label(frame, text=" Klicks auf verschiedene Buttons getätigt.").pack(
        side=tk.LEFT
    )

    for nummer, aktion in enumerate([button1_funktion, button2_funktion], 1):
        tk.Button(
            root,
            text=f"Button {nummer}",
            command=partial(aktion, zaehlerstand_label, zaehler),
        ).pack()

    frame.pack()

    root.mainloop()


if __name__ == "__main__":
    main()
Wenn man mehr als einen Wert hat, der im Closure gekapselt wird, sieht man wie unhandlich das werden kann, weil man in jeder ”Methode” alle benutzen ”Attribute” als ``nonlocal`` deklarieren muss. Also wenn man beispielweise das Label für die Anzeige des Zählerstandes mit in den Zähler integriert:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from collections import namedtuple
from functools import partial

Zaehler = namedtuple("Zaehler", "label hole erhoehe")


def erstelle_zaehler(master):
    stand = 0
    label = tk.Label(master, text=stand)

    def hole():
        nonlocal stand
        return stand

    def erhoehe():
        nonlocal stand, label
        stand += 1
        label["text"] = stand

    return Zaehler(label, hole, erhoehe)


def button1_funktion(zaehler):
    zaehler.erhoehe()


#
# Die zweite Funktion tut das Gleiche wie bei button1 - es geht mir hier nur um
# das Prinzip, ich weiß, dass es so nicht sinnvoll ist
#
def button2_funktion(zaehler):
    zaehler.erhoehe()


def main():
    root = tk.Tk()

    frame = tk.Frame(root)
    tk.Label(frame, text="Es wurden ").pack(side=tk.LEFT)
    zaehler = erstelle_zaehler(frame)
    zaehler.label.pack(side=tk.LEFT)
    tk.Label(frame, text=" Klicks auf verschiedene Buttons getätigt.").pack(
        side=tk.LEFT
    )

    for nummer, aktion in enumerate([button1_funktion, button2_funktion], 1):
        tk.Button(
            root, text=f"Button {nummer}", command=partial(aktion, zaehler)
        ).pack()

    frame.pack()

    root.mainloop()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@BlackJack: Danke für die ausführliche Antwort und die Beispiele. "nonlocal" kenne ich als Konzept gar nicht und werde es mir nun noch einmal in aller Ruhe ansehen.
Vielen Dank für den Hinweis mit pack(), das natürlich kein Objekt zurückgibt. Tja, so ist es, wenn man ein Konzept aus einem fremden Code übernimmt, ohne darüber nachzudenken. Ich dachte: "Hey. Cool. Klar - du kannst das pack() doch auch einfach anhängen, spart dir glatt eine Zeile Code." :oops:

Code: Alles auswählen

class Zaehler:
    def __init__(self):
        self.stand = 0

    def erhoehe(self):
        self.stand += 1
Dieses Beispiel bedingt wieder, dass zaehler=Zaehler() ausgeführt werden muss, also eine Instanz von Zaehler gebildet wird. Dann habe ich genau ein Objekt, das die Funktionalität bietet, die ich ja brauche. Und das war ja genau der Kern, dass ich das nicht unbedingt sinnvoll finde. OOP hin oder her - ein einziges Objekt von einer Klasse abgeleitet erscheint mir nicht sinnvoll.

Code: Alles auswählen

class Zaehler:
    stand = 0
erfüllt doch letztllich denselben Zweck. Ich kann von überall zugreifen und den Wert ändern. Irgendwie riecht es nach globaler Variable, aber das wird dem auch nicht ganz gerecht, denn der Umgang mit globalen Variablen ist ja vor allem deswegen "gefährlich", weil ich in einer Blackbox, der Funktion, Werte verändere, die sich im äußeren Bereich (z. B. auf Modulebene) befinden. Das kann zu Fehlern führen und der Code ist schwer zu warten. Wenn nun aber Funktionen auf eine gemeinsame (bekannte) Klasse zugreifen und hier die Klassenvariable ändern, so liegt der Fall doch etwas anders, oder nicht? Ich sehe hier eigentlich keine großen Gefahren, der Code ist kürzer und m. M. nach auch nicht unleserlich. Übersehe ich denn hier etwas Wesentliches?
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das habe ich doch schon erwaehnt: der Fall liegt genau *nicht* anders, als bei globalen Variablen. Jeder kann diesen Zustand von aussen erreichen. Wohingegen du nicht einfach an deine Zaehler-Instanz(!) von irgendwo rankommst. Alles, was du hier gemacht hast, ist eine Klasse als Namensraum zu missbrauchen. Statt ein Modul "Zaehler" (das dann zaehler heissen sollte) zu benutzen. Sonst ist das exakt aequivalent.

Code: Alles auswählen

class Zaehler:

    mieser_count = 0

    def __init__(self):
        self._count = 0

    def inc(self):
        self._count += 1
        return self._count


def main():
    inc = Zaehler().inc
    inc()
    # Und jetzt zeig mir mal, wie du an count kommst? (Es geht, aber ist *sehr* umstaendlich.
    # Wohingegen das bei dir sofort geht - weil's eine globale Variable ist.
    Zaehler.mieser_count += 1

if __name__ == '__main__':
    main()
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

So - nonlocal verstanden. Ich habe bisher nie mit Closures gearbeitet - das sind interessante Konzepte, die du angeboten hast, BlackJack. Ich freue mich schon aufs Herumtüfteln. 8)
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

@__deets__: Ja, ich verstehe. Danke für den Hinweis. Ich denke wohl noch nicht "modular" genug. Ich weiß, dass ich so keinen Code schreibe, der wiederverwertbar ist und gewiss "angreifbar". Nun war es ja so gedacht, dass das Programm funktional strukturiert ist und auch nicht als Modul verwendet werden muss/soll. Ich dachte, dass da der Weg, eine Klasse zu "missbrauchen" vielleicht charmanter wäre als auf "global" zurückzugreifen, aber da habe ich mich offensichtlich getäuscht. Also gut. Ich bin überzeugt. Immer instanzieren, um einen sicheren, OOP-konformen Weg zu beschreiten. :wink:
Benutzeravatar
DeaD_EyE
User
Beiträge: 1020
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Onomatopoesie hat geschrieben: Mittwoch 4. Mai 2022, 09:39 Die Frage ist aber, ob ich so etwas funktional umsetzen kann, ohne auf globale Variablen zurückzugreifen.
Wenn man Klassen und Closures (Funktion in einer Funktion) verstanden hat, wird man sich eher für Klassen, also OOP entscheiden, wenn man eine GUI entwickelt.
Die Instanzen der Klassen haben Methoden und Attribute. Die Methoden machen irgendwas und Attribute bilden den aktuellen "Status" des Programms/GUI ab.

Kann man auch mit Closures erreichen, finde ich aber schwieriger umzusetzen.
Closures sind für andere Sachen sehr nützlicher, aber um eine GUI zu entwickeln, halte ich das für den falschen Weg.
Dann kanste auch gleich aufhören mit Python und mit Haskell weiter machen.
Soweit ich weiß ist Haskell eine Funktionale Sprache, wo man erst mal um 1000 Ecken denken muss.

BTW: nonlocal gibt es schon seit einer halben Ewigkeit: https://docs.python.org/3/whatsnew/3.0. ... t=nonlocal
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Closures are a poor mans objects.
Objects are a poor mans closures.

Beides hat seine Anwendungen, die in GUIs immanent wichtige Moeglichkeit, an Ereignisbehandler (also command in tkinter, signals in Qt) extra Argumente randzudekorieren ist gut mit einem Closure darstellbar. Da eine Klasse fuer all diese ad-hoc Faelle einzufuehren, ist uebertrieben.

Aber klar, man kann closures natuerlich auch missbrauchen, nicht zuletzt, weil sie sehr implizit sind.
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

Onomatopoesie hat geschrieben: Mittwoch 4. Mai 2022, 09:35 @sparrow: Das ist mir klar. Hier liegen ja die Vorteile von OOP. tkinter lässt sich aber funktional programmieren und bei kleinen Projekten sehe ich auch nicht den Zwang, grundsätzlich auf OOP zu gehen.
Alle mir bekannten GUI-Frameworks lösen so etwas über Klassen und Vererbung: Qt, WxWidgets, Kivy, BeeWare, und -- im Rahmen seiner Möglichkeiten -- sogar GTK (früher GTK+). Gerade für diesen Anwendungsfall ist OO ideal geeignet, und zudem folgt eine OO-Implementierung dem Prinzip der geringsten Überraschung (POLA) auch für Entwickler, die keine Python- und tkinter-Profis sind.
Onomatopoesie hat geschrieben: Mittwoch 4. Mai 2022, 09:35 Die Frage ist aber die, wie ich solche Probleme lösen kann, ohne globale Variablen zu nutzen oder Klassen zu "missbrauchen", denn natürlich ist es dann letztlich auch eine globale Variable (wenn auch hübsch verpackt mit Schleife). Und ja - alle "komplexen" Objekte wie Listen, Dictionaries oder eben eigene Objekte könnten herhalten, weil sie global funktionieren. Aber die Frage ist eben, ob ich solche Probleme dann rein funktional (ohne globale Variablen) sinnvoll lösen kann.
Es erscheint mir immer noch ausgesprochen merkwürdig, daß in einem Forum zu einer strikt objektorientierten Programmiersprache von der Verwendung objektorientierter Kernfeatures abgeraten wird, auch dieser Thread ist wieder ein Beispiel dafür. IMHO führt das zu genau solchen Fragen wie Deiner: Du willst einen Aufwand betreiben, um den Aufwand einer Klasse zu vermeiden. WTF?!
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

@LukeNukem: Niemand rät davon ab, irgendwelche Features zu verwenden. Aber eine Klasse zu verwenden, nur weil man gerne eine Klasse verwenden möchte ist falsch. Hier - und das wurde ausdrücklich gesagt - ist es natürlich mehr als sinnvoll eine Klasse zu verwenden. Die javagroteske Art, wie du hier in der Vergangenheit versucht hast, _alles_ in Klassen zu stopfen ist halt in Python unüblich. Man braucht es halt nicht.
Onomatopoesie
User
Beiträge: 41
Registriert: Montag 12. August 2019, 07:52

Oweia. Ich wollte hier keinen Glaubenskrieg heraufbeschwören. Ich bin ein NOOB in Python, habe bisher Basic programmiert und muss das Konzept der OOP erst einmal verdauen, was mir eigentlich ganz gut gelingt. Und nun bin ich genau an dem Punkt angekommen: Was packe ich (sinnvoll) in meine Klassen, lohnt es sich zu instanzieren (ja! habe ich nun gelernt) und was entwickle ich dann überhaupt noch als "normale" Funktionen. Ich habe viele Anregungen erhalten und dafür bin ich euch sehr, sehr dankbar. Ich werde den Code meines aktuellen Projektes nun komplett umstrukturieren und möglichst alles in eine Klasse packen und diese instanzieren. Ich bin dann wahrscheinlich auf der sicheren Seite. :wink:
Antworten