Modularisieren eines Programms

Fragen zu Tkinter.
Antworten
MissMapeL
User
Beiträge: 22
Registriert: Mittwoch 30. Oktober 2019, 10:26

Hallo,
ich habe eine Frage zur Modularisierung. In C# Modularisiert man oft größere Projekte.
Wie geht das aber in Python bei der tkiner-Klasse?

Annahme
Ich will einen Kollegen damit beauftragen Buttons mit eigener Funktionalität zu erstellen.
Diese eigenen Buttons will ich dann als CButtonEigen ansprechen.
In einer Funktion sieht das z.B. (simplifiziert) so aus.

import tkinter

class CButtonEigen:
def __init__(self, bez = ""):
self.text = bez
self = tkinter.Button(main, text = bez, command = ende)
self.pack()

def ende():
main.destroy()

main = tkinter.Tk()
b1= CButtonEigen("Stop")
b2= CButtonEigen("Ende")
main.mainloop()

Wegen der Modularisierung will ich jetzt den CButtonEigen in einem eigenen Modul haben.
Also:
import tkinter
from CButton import CButtonEigen

main = tkinter.Tk()
b1= CButtonEigen("Stop")
b2= CButtonEigen("Ende")
main.mainloop()

ruft auf:
import tkinter

class CButtonEigen:
def __init__(self, bez = ""):
self.text = bez
self = tkinter.Button(main, text = bez, command = ende)
self.pack()

def ende():
main.destroy()

... und das geht nicht. Fehlermeldung ist: "name 'main' is not defined". Was mache ich falsch? Danke für die Hilfen!
MissMapeL
User
Beiträge: 22
Registriert: Mittwoch 30. Oktober 2019, 10:26

... warum löscht es bei mir eigentlich immer die Leerzeichen???
War alles so schön eingerückt und wurde mit Copy und Paste direkt aus Thonny übernommen .....
__deets__
User
Beiträge: 14542
Registriert: Mittwoch 14. Oktober 2015, 14:29

Dazu musst du die code-tags verwenden, dass ist der </>-Knopf im vollstaendigen Editor.
Sirius3
User
Beiträge: 17753
Registriert: Sonntag 21. Oktober 2012, 17:20

Bevor Du anfängst, zu modularisieren, mußt Du zuerst lernen, wie man Programme ohne globalen Zustand schreibt.
Was soll denn das C in CButtonEigen? Hoffentlich bedeutet das nicht Class, denn dass es eine Klasse ist, sieht man schon an der Schreibweise.
Die Klasse macht an sich keinen Sinn, da sie den ursprüngliche Button gar nicht erweitert.
Ein `self =` ist eigentlich immer falsch, da `self` die Referenz auf die Instanz ist, und die möchte man nicht wegwerfen.
Eine Widget-Klasse sollte sich nichts selbst positionieren, das `pack` gehört da nicht rein.
Alles was eine Funktion (hier __init__) braucht, muß sie über ihre Argumente bekommen, hier main und ende. Benutze keine Abkürzungen, wenn Du Bezugsdauerüberschreitungsgebührenverordnung meinst, dann schreib das auch und kürze das nicht mit bez ab.

Wenn man das dann so umgeschrieben hat:

Code: Alles auswählen

import tkinter as tk
from functools import partial

class ButtonEigen:
    def __init__(self, parent, bezeichnung, ende):
        self.text = bezeichnung
        self.button = tk.Button(parent, text=bezeichnung, command=ende)
        
    def pack(self):
        self.button.pack()

def ende(main_window):
    main_window.destroy()

def main():
    main_window = tk.Tk()
    b1 = ButtonEigen(main_window, "Stop", partial(ende, main_window))
    b1.pack()
    b2 = ButtonEigen(main_window, "Ende", partial(ende, main_window))
    b2.pack()
    main_window.mainloop()

if __name__ == '__main__':
    main()
dann kann man das auch in verschiedene Module unterteilen, soweit dies sinnvoll ist.
MissMapeL
User
Beiträge: 22
Registriert: Mittwoch 30. Oktober 2019, 10:26

Hallo __deets__ und Sirius3,
vielen Dank für eure schnelle Hilfe.
Ich wollte das von mir gegebene Beispiel möglichst einfach halten, deshalb hat der Button auch keine weitere Funktionalität. Es ging mir ja vor allem um die Modularisierung.

Zur Kritik, dass ich CButtonEigen schreibe und damit eine Class (ungarische Notation) andeute ...
Mit einem: x = xxx(..., ..., ...) kann doch sowohl eine Funktion mit Rückgabeparmeter als auch die Initialisierung eines Objekts gemeint sein? In C# und VB meine ich gelernt zu haben, dass eine entsprechende Schreibweise sinnvoll ist.

self = ... ist wirklich blöde gewesen. Vermutlich habe ich beim xten Mal was ausprobieren den Rest des Codes versehentlich entfernt.

Zum "... mußt Du zuerst lernen, wie man Programme ohne globalen Zustand schreibt". Ich lerne gerade mit dem Buch Theis, Thomas: Einstieg in Python. Dieses befasst sich nur auf 20 Seiten mit der Objektorientierung, wobei eine Einbindung einer Grafik fehlt. Welches Buch oder welche Seiten würdet Ihr mir hier empfehlen.

Last but not least: ein Problem meines Programnms war ja offensichtlich das Fehlen des "from functools import partial".
Woher wisst ihr eigentlich immer, welches Modul zu importieren ist? Ist das einfach langjährige Erfahrung oder habt ihr alle Module in https://docs.python.org/3.3/library/ind ... rary-index auswendig gelernt?

Noch einmal - vielen Dank für die Hilfen!!!
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

Namen schreibt man in Python sprechend, klein_mit_unterstricht. Ausnahmen sind die Namen von Klassen (nicht die von deren Instanzen!), diese werden GroßGeschrieben. Konstanten KOMPLETT GROSS.
Anhand deines Beispiels kann man also durchaus unterscheiden, ob es sich um die Initialisierung eins Objekts oder um den Aufruf einer Funktion handelt. x = Xxx() und x=xxx().
Der Typ eines Objekts gehört nicht in den Namen.
Und Namen sollten ausgeschrieben und leserlich sein. "ButtonEigen" erschließt sich für mich erst auf den zweiten Blick als Klingelknopf für eine Eigentumswohnung.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MissMapeL: Warum willst Du an der Stelle überhaupt wissen ob das eine Funktion oder eine Klasse ist? (Könnte übrigens auch eine Methode sein, die jemand in dem Kontext an den Namen `xxx` gebunden hat.) Was bringt Dir dieses Wissen? Da in Python beides, Funktionen und Klassen, aufrufbare Objekte sind, sind die an der Stelle austauschbar. Klassen sind halt ”Funktionen” die (in aller Regel) Objekte von ihrem Typ erstellen. Meistens neu erstellte Objekte. Muss aber nicht sein. Also weder das neu, noch das es der Typ sein muss, denn man kann ja den Konstruktor (`__new__()`) überschreiben.

In C# und VB scheint diese „hungarian notation“ an vielen Stellen noch durch weil Microsoft das früher praktiziert hat. Die sind mittlerweile aber davon abgerückt.

In Python gehören keine Grunddatentypen in Namen. Und Klassen sind ein ziemlich grundlegender Datentyp in Python. Allerdings müsste man dann `TButtonEigen` schreiben, denn Klassen haben den Typ `type`.

Das nächste Problem mit dem Namen, egal ob da nun ein C oder ein T am Anfang steht: Name sollten keine kryptischen Abkürzungen enthalten. Und IMHO sollten die auch nicht nach Yoda klingen. Also nicht `TypeButtonEigen` sondrn `EigenButtonType`. Man sollte Code in der Regel laut vorlesen können ohne das nacher alle „Hey, Yoda“ zu einem sagen. 😉

Das sparrow das für einen Klingelknopf einer Eigentumswohnung hält, wo das doch ganz klar etwas mit einer Matrix und Eigenwerten zu tun hat, zeigt das Problem mit Abkürzungen recht gut. 😉

Man muss nicht die Standardbibliothek auswendig lernen, sondern nur so ungefähr wissen was im `builtins`-Modul steckt. Denn alles andere muss ja von woanders kommen. Und wenn man es braucht, muss man es importieren.

Dazu muss man dann natürlich wissen was es so gibt und wo das steckt, und natürlich: das muss man lernen. Das kommt mit der Zeit, weil man bestimmte Sachen wie `functools.partial()` öfter braucht. Es gibt so ein paar Module die sehr allgemeine Probleme lösen und quasi allgemein nützliche Bausteine/Werkzeuge zur Verfügung stellen. Dazu gehören beispielsweise in der Standardbibliothek `functools`, `itertools`, `contextlib`, und `operator`.

Ansonsten reicht es einen Überblick zu haben was die Standardbibliothek so enthält, damit man das Rad nicht noch mal selbst erfindet und sich beispielsweise eine Queue oder eine Heap-Datenstruktur nicht selber bastelt. Die Teile der Standardbibliothek die für die eigenen Projekte von Bedeutung sind, lernt man durch Verwendung besser kennen.

Wobei es nicht schaden kann sich tatsächlich mal durch die Standardbibliothek zu arbeiten und mit den ganzen Sachen dort mal herum zu spielen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
MissMapeL
User
Beiträge: 22
Registriert: Mittwoch 30. Oktober 2019, 10:26

Hallo sparrow und __blackjack__,
danke, das hilft mir weiter.
... Aber die nächste Frage kommt bestimmt ...
MissMapeL
User
Beiträge: 22
Registriert: Mittwoch 30. Oktober 2019, 10:26

... und noch ein Nachklapp ...
Im Grunde macht doch PAGE etwas ganz ähnliches. In einer Funktion werden die grafischen Elemente definiert und in einem anderen die Objekt-Funktionen.
Und bei https://www.codespeedy.com/tic-tac-toe- ... g-tkinter/ wird eine Funktion zur Erstellung von Buttons verwendet ...
__deets__
User
Beiträge: 14542
Registriert: Mittwoch 14. Oktober 2015, 14:29

Schon in den ersten Zeilen Sternchen Importe, dann NOCHMAL einen Namen importiert, den man durch * schon hatte, und ein global in der ersten Funktion. Ist schon zum wegschmeißen.
Sirius3
User
Beiträge: 17753
Registriert: Sonntag 21. Oktober 2012, 17:20

Ja es kann sinnvoll sein, ähnliche Funktionalität in eine Funktion auszulagern, z.B. wenn man immer Knöpfe mit einem bestimmten Layout erstellen will. Der Rest vom Code ist dann eher nicht als Vorlage geeignet, weil da viel mit Globalem Zustand herumgemurkst wird, und Variablennamen scheinbar nicht mehr als einen Buchstaben haben dürfen.
Mir fehlt bei Deinem letzten Beitrag aber irgendwie der Zusammenhang zu Deiner ursprünglichen Frage.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Zu dem Tic-Tac-Toe-Quelltext: Eingerückt wird mit vier Leerzeichen.

Es fehlt Leerraum um das lesbarer zu gestalten. Leerzeilen zwischen Funktionsdefinitionen und Leerzeichen um Operatoren, nach Kommas, und um Gleichheitszeichen bei Zuweisungen ausserhalb von Argumentlisten.

Die Zeilen in der `check()`-Funktion sind zu lang. Das ist nur ganz schwer lesbar.

Daten sollte man wie Code, möglichst nicht wiederholen. Da besteht die Gefahr, dass man Fehler macht und es macht Veränderungen aufwändiger und fehleranfälliger. Die Liste mit den beiden Symbolen (``["O", "X"]``) steht mehrfach im Quelltext.

Eine literale Liste mit drei leeren Listen ist nicht wirklich flexibel. Man würde da eher eine leere Liste erstellen und die dann in den Schleifen mit Listen füllen. Dann hängt das alles nur von der Anzahl der Schleifendurchläufe ab.

`button` ist ein passender Name für ein `Button`-Objekt, aber nicht für eine Funktion die solche Objekte erstellt. Funktionen werden üblicherweise nach der Tätigkeit benannt die sie durchführen.

Bei ``if not(i == a)`` sind die Klammern überflüssig und statt ``not`` und ``==`` würde man einfach ``!=`` schreiben.

`i` ist ein wirklich schlechter Name für eine Laufvariable die *keine* ganze Zahl ist.

In `reset()` sind die Schleifen über die Indexwerte ein „anti pattern“ in Python. Man kann direkt über die Elemente der Listen iterieren, ohne den Umweg über einen Indexwert.

`check()` macht deutlich mehr als nur zu prüfen. Das sollte nur prüfen und einen Rückgabewert liefern.

Die Zufallsauswahlt vom aktuellen Spieler und das aktualisieren des Labels wer gerade dran ist, steht beides zweimal im Quelltext, sollte da aber jeweils nur einmal stehen.

Statt `click()` die Indexwerte für Zeile und Spalte des Buttons zu übergeben, könnte man auch einfach das betreffende Button-Objekt selbst als Argument übergeben.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
#
# TODO Class to get rid of `StringVar` and passing so many arguments.
#
# TODO Don't mix UI and program logic.
#
import random
import tkinter as tk
from enum import auto, Enum
from functools import partial
from tkinter import messagebox

#
# TODO Use `Enum` to combine symbol and color into one object.
#
SYMBOLS = ["O", "X"]
SYMBOL_TO_COLOUR = {"O": "deep sky blue", "X": "lawn green"}

assert all(symbol in SYMBOL_TO_COLOUR for symbol in SYMBOLS)
assert len(SYMBOLS) == len(SYMBOL_TO_COLOUR)


class CheckResult(Enum):
    NOT_WON = auto()
    WON = auto()
    DRAW = auto()


def create_button(master):
    return tk.Button(
        master,
        width=3,
        bd=10,
        bg="papaya whip",
        font=("arial", 60, "bold"),
        padx=1,
        relief="sunken",
    )


def update_label(label, current_player_symbol_var):
    label["text"] = f"{current_player_symbol_var.get()}'s Chance"


def reset_game(label, buttons, current_player_symbol_var):
    for row in buttons:
        for button in row:
            button.config(text=" ", state=tk.NORMAL)
    current_player_symbol_var.set(random.choice(SYMBOLS))
    update_label(label, current_player_symbol_var)


def change_player(current_player_symbol_var):
    assert len(SYMBOLS) == 2
    for symbol in SYMBOLS:
        if symbol != current_player_symbol_var.get():
            current_player_symbol_var.set(symbol)
            break
    else:
        raise ValueError(
            f"unexpected symbol {current_player_symbol_var.get()!r}"
        )


def check_for_win(buttons, current_player_symbol):
    #
    # TODO Rewrite this to be independent of actual size/shape of `buttons`.
    #
    for i in range(3):
        if (
            buttons[i][0]["text"]
            == buttons[i][1]["text"]
            == buttons[i][2]["text"]
            == current_player_symbol
            or buttons[0][i]["text"]
            == buttons[1][i]["text"]
            == buttons[2][i]["text"]
            == current_player_symbol
        ):
            return CheckResult.WON

    if (
        buttons[0][0]["text"]
        == buttons[1][1]["text"]
        == buttons[2][2]["text"]
        == current_player_symbol
        or buttons[0][2]["text"]
        == buttons[1][1]["text"]
        == buttons[2][0]["text"]
        == current_player_symbol
    ):
        return CheckResult.WON

    if (
        buttons[0][0]["state"]
        == buttons[0][1]["state"]
        == buttons[0][2]["state"]
        == buttons[1][0]["state"]
        == buttons[1][1]["state"]
        == buttons[1][2]["state"]
        == buttons[2][0]["state"]
        == buttons[2][1]["state"]
        == buttons[2][2]["state"]
        == tk.DISABLED
    ):
        return CheckResult.DRAW

    return CheckResult.NOT_WON


def on_click(label, buttons, current_player_symbol_var, button):
    symbol = current_player_symbol_var.get()
    button.config(
        text=symbol,
        state=tk.DISABLED,
        disabledforeground=SYMBOL_TO_COLOUR[symbol],
    )

    result = check_for_win(buttons, symbol)
    if result in [CheckResult.WON, CheckResult.DRAW]:
        if result is CheckResult.WON:
            titel, text = "Congrats!!", f"{symbol!r} has won"
        elif result is CheckResult.DRAW:
            titel, text = "Tied!!", "The match ended in a draw"
        else:
            assert False, f"unexpected result {result!r}"

        messagebox.showinfo(titel, text)
        reset_game(label, buttons, current_player_symbol_var)

    change_player(current_player_symbol_var)
    update_label(label, current_player_symbol_var)


def main():
    root = tk.Tk()
    root.title("Tic-Tac-Toe")
    current_player_symbol_var = tk.StringVar()

    label = tk.Label(font=("arial", 20, "bold"))
    label.grid(row=3, column=0, columnspan=3)

    buttons = list()
    for i in range(3):
        row_buttons = list()
        for j in range(3):
            button = create_button(root)
            button["command"] = partial(
                on_click, label, buttons, current_player_symbol_var, button
            )
            button.grid(row=i, column=j)
            row_buttons.append(button)
        buttons.append(row_buttons)

    reset_game(label, buttons, current_player_symbol_var)
    root.mainloop()


if __name__ == "__main__":
    main()
Das sollte man noch als Klasse umschreiben, damit man das `StringVar`-Objekt loswerden kann, und das so viele einzelne Argumente durch die Gegend gereicht werden müssen. Dann kann man auch Symbol und Farbe in jeweils einem Objekt zusammenfassen.

Die Prüffunktion enthält eine Menge magischer, fester Indexwerte. Das sollte man so umschreiben, dass es von den Dimensionen von `buttons` abhängt und nicht von hart kodierten Zahlen.

@__deets__: Der zweite ``import`` ist notwendig weil `messagebox` ein eigenes (Unter)Modul ist das durch den *-Import nicht unbedigt erfasst wird. Das hängt wohl von der Python-Version ab ob `tkinter.messagebox` von `tkinter` importiert wird, und damit durch das * mit kommt, oder nicht.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten