Wandernder Unsinn (Sagt nichts gegen den Namen!)

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
ctex

Ich habe ein lustiges Spiel für lange Abende mit Freunden programmiert:
http://www.waun.codeplex.com

Hoffe es gefällt euch!

PS: Tut mir leid, dass es noch keine Dokumentation gibt. Bin noch nicht dazu gekommen!
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@ctex: hier ein paar Bemerkungen, was mir beim Durchlesen Deines Codes aufgefallen ist.

Eingerückt wird per Konvention immer mit 4 Leerzeichen pro Ebene und nicht mal 3 und mal 4. Konvention ist auch, Variablennamen mit Unterstrich statt Binnengroßbuchstaben zu schreiben: ist_satz.

Der Unbekannte Fehler in Zeile 14 kann nie auftreten, da die elif-Bedingung immer exakt das Gegenteil der if-Bedingung ist. Alles weitere in der Funktion wird nicht mehr ausgeführt, weil das break die While-Schleife verläßt. Rückt man den Rest also korrekterweise aus, ist in Zeile 16 das explizite prüfen auf True überflüssig, es reicht »if istAdj:«:. In Zeile 17 prüft man besser mit in »" " in eingabe«. Zeile 22 macht nichts. Zeile 95 schreibt man kürzer mit not in »eingabe[-1] not in (".", "?", "!")«.

Warum gibst Du in clearc ein Zeilenumbruch explizit aus und setzt dann end auf ""? Warum nicht einfach ein leeres print? Was soll der Rückgabewert von clearc?

Zeile 48: solche langen Defaultwerte definiert man am besten als Konstanten vorneweg.
Zeile 49: Warum konvertierst Du werte in eine Liste?
Zeile 50ff: Wenn Du anfängst, Variablennamen durchzunummerieren willst Du eigentlich eine Liste verwenden.
Zeile 63ff: replace liefert einen neuen String zurück.
Zeile 75: warum setzt Du Deine eigene Email-Addresse als BCC? Glaubst Du, das merkt keiner?
Zeile 78: Importe sollten immer ganz am Anfang der Datei stehen.
Zeile 80: Dateien öffnet man am besten mit dem with-Statement. Pfade setzt man mit os.path.join zusammen.
Zeile 82: der Wert der Stringkonkatenation steht schon in Alles
Zeile 89: der FileNotFoundError tritt nie auf, weil "a" eine neue Datei anlegt, falls sie nicht existiert.

Zeile 99: Was soll denn diese while-Schleife, da wird doch jedesmal in die selbe if-Bedingung gesprungen. Eine while-Schleife für die Eingabeprüfung und eine für die Ausführung, wobei man die beiden Bedingungen "ja" und "nein" einfach zusammenfassen kann: »Ausgabe(ausfuehren(), DateiAusgabe == "ja")«
BlackJack

@Sirius3: Die aktuelle Version gibt's wohl nur im Downloadbereich. Die verwendet Tk zur Darstellung.
Citral
User
Beiträge: 12
Registriert: Dienstag 7. Juni 2016, 04:27
Kontaktdaten:

Link geht net und wenn ich nach deinem Spiel suche, darf ich nur eine .exe runterladen und ich als Linuxnutzer hab da net viel von :). Zudem wo findet man denn Quellcode?
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@BlackJack: vieles von dem Gesagten stimmt zum Glück noch :? Nur die Zeilennummern sind etwas verschoben.

@ctex: Dein Link sollte wohl http://waun.codeplex.com heißen.

@Citral: Unter Downloads gibt es auch den Source zum Downloaden.
BlackJack

@ctex: Ein paar Anmerkungen zu dem Quelltext mit GUI:

`time` wird importiert, aber nicht verwendet.

`DbMd` wird definiert, aber nirgends verwendet. Das hat auch einen *sehr* schlechten Namen. Ich könnte nicht einmal sinnvoll *raten* was das wohl bedeuten mag. Abkürzungen in Namen sollte man vermeiden, solange sie nicht allgemein bekannt sind.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Du verdeckst mit dem Namen `Hauptfenster` im Hauptprogramm den Namen der Klasse `Hauptfenster`.

Das Argument `mwort` wird in `ueberpruefen()` nicht verwendet.

Die ``while``-Schleife in der Funktion macht keinen Sinn weil die wegen de, ``break`` am Ende höchstens einmal durchlaufen wird.

Die Zeichenkette 'Wandernder Unsinn' kommt mehrfach vor und sollte deshalb als Konstante definiert werden.

Explizites Vergleichen mit literalen `True`- oder `False`-Werten ist schlechter Stil weil da nur wieder ein Wahrheitswert bei heraus kommt, den man ja ohne den Vergleich schon hatte. Entweder will man den gleich verwenden, oder dessen Negation mit ``not``.

Es gibt drei ``if``\s bei denen `istAdj` Teil der Bedingung ist. Das kann man zusammenfassen.

Falls nur das letzte 'e' entfernt werden soll, ist `rstrip()` die falsche Methode. Falls alle 'e' am Ende entfernt werden sollen ist `rstrip()` richtig, aber die `if`-Abfrage vorher überflüssig.

Die Veränderung der Eingabe falls der Name `Moritz` und Schimpfwörter vorkommen ist fehlerhaft. Zum einen gibt `find()` keinen Wahrheitswert zurück der angibt ob das Wort enthalten ist oder nicht. Im Gegenteil, das gibt immer einen wahren Wert *ausser* wenn das zu suchende Wort an erster Stelle steht. Wenn man den Test mit dem ``in``-Operator ausdrückt, kommt das nächste Problem: ``and`` bindet stärker als ``or``, die Bedingung sagt also nicht das aus was Du anscheinend haben möchtest, ohne das Du Klammern setzt. Ausserdem kann man das mit `any()` und einem Generatorausdruck etwas schöner/lesbarer/kompakter schreiben.

Da mit dem Begriff 'Arschloch' dann doch nichts gemacht wird, kann man den auch weglassen.

`verarbeiten()` kann man kürzer ausdrücken.

Widgets layouten sich in ihrer `__init__()` nicht selbst. Das macht keines der vorhandenen Widgets, denn damit nimmt man dem Aufrufer die Entscheidung was genau er mit dem Widget machen will.

Bei der Frage nach der Dateiausgabe ist eine unschöne Code-Wiederholung. Da würde man eher eine ”Endlosschleife” schreiben, die durch ein ``break`` verlassen wird. Ausserdem würde ich das 'yes'/'no' zu einem Wahrheitswert machen.

`self.eingabe` wird nirgends verwendet.

Attribute sollten nach der `__init__()` alle existieren, damit man leicht sieht welche Attribute ein Objekt hat, und damit nicht die Reihenfolge der Methodenaufrufe bestimmt welche Attribute es gibt und welche nicht.

Statt die ganzen Einzelteile vom Benutzer an einzelne Attribute zu binden und dann hinterher in eine Liste zu stecken, könnte man die auch gleich in einer Liste sammeln.

Um ein Textfeld komplett zu leeren nimmt man nicht irgendeine grosse Zahl als Ende sondern das tatsächliche Ende: `tkinter.END`.

Für das was Du mit `eAlles` veranstaltest gibt es eine Funktion irgendwo unter `urllib`.

Weder `eAlles` noch `datei` gehören als Attribute an das Objekt.

Bei den Dateien würde ich eine Kodierung angeben.

Es kann passieren das weder im ``try`` noch im ``except``-Block der Name `datei` gebunden wird. Dann führt das ``datei.close()`` im ``finally``-Block zwangsläufig zu einem `NameError`.

`sys.exit()` ist eine einschneidende Funktion. Die sollte man nicht irgendwo im Programm aufrufen, das nimmt anderem Code die Möglichkeit noch irgend etwas zu tun. Es reicht hier völlig aus die GUI-Hauptschleife mit der `quit()`-Methode zu verlassen.

Ich lande dann als Zwischenergebnis ungefähr hier:

Code: Alles auswählen

# coding: utf8
import sys
import webbrowser
import tkinter
import tkinter.messagebox
import tkinter.filedialog
from urllib.parse import quote

PROGRAMM_TITEL = 'Wandernder Unsinn'


def ueberpruefen(eingabe, ist_adjektiv=False):
    # 
    # TODO Die API ist schlecht — die Rückgabewerte sollten nur einen
    #   (Duck)Typ haben, und nicht mal `str` und mal `bool` sein.
    #   Für Fehler gibt es Ausnahmen!
    #   
    #   Ausserdem sollte die Benutzerinteraktion hier nicht drin sein.
    #   
    #   Und der Name `ueberpruefen()` passt auch nicht, die Funktion
    #   tut mehr als nur etwas zu überprüfen.
    # 
    if eingabe.stript() == '':
        tkinter.messagebox.showerror(PROGRAMM_TITEL, 'Bitte gebe etwas ein!')
        return False

    if ist_adjektiv:
        if eingabe.endswith('e'):
            eingabe = eingabe[:-1]

        if ' ' in eingabe:
            tkinter.messagebox.showerror(
                PROGRAMM_TITEL, 'Du darfst nur ein Wort eingeben!'
            )
            return False

        eingabe = eingabe.lower()

    bad_words = ['dumm', 'doof']
    if 'Moritz' in eingabe and any(w in eingabe for w in bad_words):
        for bad_word in bad_words:
            eingabe = eingabe.replace(bad_word, 'superschlau')

    return eingabe.strip(' "-:').lstrip('.?!')


def verarbeiten(werte):
    return (
        'Der {}e {} traf die {}e {} {} und sagte: "{}" und sie antwortete:'
        ' "{}" Und deshalb {}.'.format(*werte)
    )


class Hauptfenster(tkinter.Frame):

    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.dateipfad = None
        while True:
            self.dateiausgabe = tkinter.messagebox.askquestion(
                PROGRAMM_TITEL, 'Datei-Ausgabe?'
            ) == 'yes'
            if not self.dateiausgabe:
                break

            self.dateipfad = tkinter.filedialog.asksaveasfilename(
                initialfile='Wandernder Unsinn',
                title='Datei-Pfad zur Ausgabe',
                defaultextension='.txt',
                filetypes={'Text-Datei {.txt}' : '.txt'},
            )
            if self.dateipfad != '':
                break

        self.anzeige_label = tkinter.Label(self)
        self.anzeige_label.pack(expand=True)

        self.eingabe_entry = tkinter.Entry(self)
        self.eingabe_entry.pack(expand=True)
        
        self.fertig_button = tkinter.Button(self, command=self.on_fertig)
        self.fertig_button.pack(fill=tkinter.BOTH, pady=20)
        self.fertig_button.focus_set()

        self.schritt = None
        self.restart()

    def restart(self):
        self.eingaben = list()
        self.schritt = 'Mann_Wie'
        self.anzeige_label['text'] = "Der <Adjektiv>e ..."
        self.fertig_button['text'] = "Okay"

    def on_fertig(self):
        if self.schritt == 'Mann_Wie':
            eingabe = ueberpruefen(self.eingabe_entry.get(), ist_adjektiv=True)
            if eingabe:
                self.eingaben.append(eingabe)
                self.schritt = 'Mann_Wer'
                self.anzeige_label['text'] = '... <Name von Mann> traf die ...'
                self.eingabe_entry.delete(0, tkinter.END)
            
        elif self.schritt == 'Mann_Wer':
            eingabe = ueberpruefen(self.eingabe_entry.get())
            if eingabe:
                self.eingaben.append(eingabe)
                self.schritt = 'Frau_Wie'
                self.anzeige_label['text'] = '... die <Adjektiv>e ...'
                self.eingabe_entry.delete(0, tkinter.END)
            
        elif self.schritt == 'Frau_Wie':
            eingabe = ueberpruefen(self.eingabe_entry.get(), ist_adjektiv=True)
            if eingabe:
                self.eingaben.append(eingabe)
                self.schritt = 'Frau_Wer'
                self.anzeige_label['text'] = '... <Name von Frau> ...'
                self.eingabe_entry.delete(0, tkinter.END)

        elif self.schritt == 'Frau_Wer':
            eingabe = ueberpruefen(self.eingabe_entry.get())
            if eingabe:
                self.eingaben.append(eingabe)
                self.schritt = 'Wo'
                self.anzeige_label['text'] = '... <Wo> und sagte: ...'
                self.eingabe_entry.delete(0, tkinter.END)

        elif self.schritt == 'Wo':
            eingabe = ueberpruefen(self.eingabe_entry.get())
            if eingabe:
                self.eingaben.append(eingabe)
                self.schritt = 'Satz'
                self.anzeige_label['text'] = (
                    '... und sagte: "<Satz>" und sie antwortete: ...'
                )
                self.eingabe_entry.delete(0, tkinter.END)

        elif self.schritt == 'Satz':
            eingabe = ueberpruefen(self.eingabe_entry.get())
            if eingabe:
                self.eingaben.append(eingabe)
                self.schritt = 'Antwort'
                self.anzeige_label['text'] = (
                    '... und sie antwortete: "<Antwort>" )und deshalb ...'
                )
                self.eingabe_entry.delete(0, tkinter.END)

        elif self.schritt == 'Antwort':
            eingabe = ueberpruefen(self.eingabe_entry.get())
            if eingabe:
                self.eingaben.append(eingabe)
                self.schritt = 'Deshalb'
                self.fertig_button['text'] = 'Abschließen'
                self.anzeige_label['text'] = '... <Deshalb>.'
                self.eingabe_entry.delete(0, tkinter.END)

        elif self.schritt == 'Deshalb':
            eingabe = ueberpruefen(self.eingabe_entry.get())
            if eingabe:
                self.eingaben.append(eingabe)
                self.eingabe_entry.delete(0, tkinter.END)

                ergebnis = verarbeiten(self.eingaben)

                if tkinter.messagebox.askquestion(
                    PROGRAMM_TITEL,
                    'SPIEL ENDE!\n'
                    '{}\n\n'
                    '--------------------\n'
                    'Willst du das Ergebnis per E-Mail versenden?\n'.format(
                        ergebnis
                    )
                ) == 'yes':
                    webbrowser.open(
                        quote(
                            'mailto:?subject=Lustige Geschichte ERSTELLT MIT'
                            ' Wandernder Unsinn - VON MORITZ RUTH'
                            '&body=Lustig:\x0a\x0a' + ergebnis
                        )
                    )

                if self.dateiausgabe:
                    datei = None
                    try:
                        datei = open(self.dateipfad, 'x', encoding='utf8')
                        datei.write('------------ERSTE RUNDE------------\n')
                        datei.write(ergebnis)
                    except FileExistsError:
                        datei = open(self.dateipfad, 'a', encoding='utf8')
                        datei.write(
                            '\n\n\n------------NEUE  RUNDE------------\n'
                        )
                        datei.write(ergebnis)
                    finally:
                        if datei is not None:
                            datei.close()

                if tkinter.messagebox.askquestion(
                    PROGRAMM_TITEL, 'Neue Runde?'
                ) == 'yes':
                    self.restart()
                else:
                    self.quit()
        else:
            assert False, 'Unbekannter Schrittwert {0!r}'.format(self.schritt)


def main():
    root = tkinter.Tk()
    hauptfenster = Hauptfenster(root)
    hauptfenster.pack(padx=40, pady=50, expand=False)
    root.mainloop()


if __name__ == '__main__':
    main()
Ich habe bei `ueberpruefen()` das Gefühl, das da eigentlich zwei verschiedene Funktionen in einer stecken und das man das trennen sollte.

Der Satz mit den Platzhaltern und die einzelnen Werte sind viel zu stark über das gesamte Programm verteilt und vor allem hart als Code modelliert, statt als Datenstruktur. Wenn man da etwas ändern möchte, muss man das gesamte Programm durchgehen. Wenn man die Vorlage für eine andere ”Geschichte” verändern möchte, muss man sogar viel Code neu schreiben.

Die einzelnen Zweige in der `on_fertig()` machen aber fast alle das gleiche. Spricht wieder dafür da Daten in einer Struktur heraus zu ziehen und Code zu schreiben der das dann generisch abarbeitet.

Die `on_fertig()` ist auch viel zu lang. Die ganzen Zweige könnte man auf eigene Methoden aufteilen und dann kann man diese Methoden direkt als `schritt` verwenden und spart sich die Indirektion über diese unschönen Zeichenketten.

Das versenden einer E-Mail würde ich in eine eigene Funktion auslagern. Insgesamt wäre eine Trennung von GUI und Geschäftslogik keine schlechte Idee.

Von der Benutzung her sind diese Fragen ohne dass man das Gesamtbild sieht, IMHO auch nicht so schön. Da könnte man mit einem `Text`-Widget und Eingabefeldern *dort* drin, sicher eine bedienerfreundlichere Oberfläche für den Benutzer erstellen.
BlackJack

Mal ein Ansatz wie man die Geschichte mit den Platzhaltern als Daten angeben kann, statt sie fest in den Code zu schreiben, über das ganze Programm verteilt, und mit Wiederholungen.

Code: Alles auswählen

#!/user/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import re
import Tkinter as tk

STORY_TEMPLATE = (
    'Der <Adjektiv|a>e <Name von Mann> traf die <Adjektiv|a>e <Name von Frau>'
    ' <Ort> und sagte: "<Satz>" und sie antwortete: "<Antwort>"'
    ' und deshalb <Grund>.'
)


class Literal(object):

    def __init__(self, text):
        self.text = text

    def __str__(self):
        return self.text

    render = __str__

    @staticmethod
    def get_error():
        return None


class Placeholder(object):

    def __init__(self, description, type_=None, value=''):
        if type_ not in [None, 'a']:
            raise ValueError('unknown type {0!r}'.format(type_))
        self.description = description
        self.type = type_
        self.value = value

    def __str__(self):
        type_ = '|' + self.type if self.type else ''
        return '<{0.description}{1}>'.format(self, type_)

    def render(self):
        result = self.value
        if self.type == 'a' and result.endswith('e'):
            result = result[:-1]
        return result

    def get_error(self):
        if not self.value.strip():
            return 'leer!'
        if self.type == 'a' and ' ' in self.value:
            return 'nur ein Wort!'
        return None


class Story(object):

    REGEX = re.compile(
        r'\<(?P<placeholder>.+?)(\|(?P<type>.))?\>'
        r'|(?P<literal>[^<]*)'
    )

    def __init__(self, parts):
        self.parts = parts

    def __str__(self):
        return ''.join(map(str, self.parts))

    def __len__(self):
        return len(self.parts)

    def __getitem__(self, index):
        return self.parts[index]

    def render(self):
        return ''.join(part.render() for part in self)

    @classmethod
    def parse(cls, string):
        parts = list()
        for match in cls.REGEX.finditer(string):
            description = match.group('placeholder')
            if description is not None:
                parts.append(Placeholder(description, match.group('type')))
            else:
                literal = match.group('literal')
                assert literal is not None, repr(match.groupdict())
                parts.append(Literal(literal))
        return cls(parts)


class PlaceholderUI(tk.Frame):

    def __init__(self, parent, placeholder):
        tk.Frame.__init__(self, parent, relief=tk.RAISED, borderwidth=1)
        self.placeholder = placeholder
        tk.Label(
            self, text=self.placeholder.description, font='Helvetica 8'
        ).pack()
        self.entry = tk.Entry(self, width=10)
        self.entry.pack()
        self.entry.bind('<KeyRelease>', self.on_key)
        self.entry.bind('<FocusOut>', lambda _: self.validate())
        self.error_label = tk.Label(self, foreground='red')
        self.error_label.pack()

    def on_key(self, _event):
        self.placeholder.value = self.entry.get()

    def set_focus(self):
        self.entry.focus()

    def validate(self):
        error = self.placeholder.get_error()
        self.error_label['text'] = error if error else ''
        return not error


class StoryUI(tk.Frame):

    def __init__(self, parent, story):
        tk.Frame.__init__(self, parent)
        self.story = story
        self.placeholder_uis = list()
        text = tk.Text(self, font='Helvetica 14')
        for part in self.story:
            if isinstance(part, Placeholder):
                placeholder_ui = PlaceholderUI(self, part)
                text.window_create(tk.END, window=placeholder_ui)
                self.placeholder_uis.append(placeholder_ui)
            else:
                text.insert(tk.END, part)
        text.pack()
        tk.Button(self, text='Fertig', command=self.on_finished).pack()

    def on_finished(self):
        first_invalid_placeholder_ui = None
        for placeholder_ui in self.placeholder_uis:
            if not placeholder_ui.validate():
                if not first_invalid_placeholder_ui:
                    first_invalid_placeholder_ui = placeholder_ui
        if first_invalid_placeholder_ui:
            first_invalid_placeholder_ui.set_focus()
        else:
            print(self.story.render())


def main():
    root = tk.Tk()
    story_ui = StoryUI(root, Story.parse(STORY_TEMPLATE))
    story_ui.pack()
    root.mainloop()


if __name__ == '__main__':
    main()
Antworten