ubuntuusers.de

Python Programmstruktur

Status: Ungelöst | Ubuntu-Version: Nicht spezifiziert
Antworten |

AMDUser

Anmeldungsdatum:
12. August 2005

Beiträge: 96

Wohnort: Leichlingen

Hallo zusammen,
ich schreibe sehr oft Python Programme, die auf Dauer mit immer mehr Funktionen ausgestattet werden sollen. Ich habe aber nicht unbedingt immer Lust, mir vorher genau zu überlegen, wie ich das genau aufbaue.

Mal ein Beispiel: Ich möchte einen Assistenten in PyQt4 (man will ja aktuell sein 😉 ) schreiben. Also ein Fenster mit ein paar Einstellmöglichkeiten, weiter Button, gleiches Fenster, aber andere Einstellmöglichkeiten. Sollte man jetzt jede Einzelne Seite dieses Assistenten separat Designen und bei einem Druck auf Weiter die alte zerstören und die neue laden, oder doch lieber die Widgets von Hand löschen und neu platzieren?
Oder ein anderes: Ich habe ein Programm mit einem Hauptfenster und einem Einstellungsdialog. Beide habe ich mit dem Qt-Designer erstellt und in separaten Datei abgelegt. Von diesen Beiden Klassen werden dann 2 andere in der main.py abgeleitet, welche meine Funktionen enthalten, welche sich auf den jeweiligen Dialog beziehen (z.B. Speichern bei den Einstellungen). Erstmal: Ist das so sinnvoll? Und ist es jetzt besser die Variablen in denen die Einstellungen stehen in der Einstellungsklasse abzulegen oder Global?

Oder einmal ein paar generellere Fragen:
- Wie kann man sinnvoll Funktion von GUI trennen, ohne zwei verschiedene Programme zu schreiben (Ich find system Aufrufe einfach "uncool")
- Was gibt es für generelle Ansätze OOP schön zu strukturieren?
- Welche davon bevorzugt ihr?

Danke für die (hoffentlich) vielen Antworten 😉
Alex

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

Die Frage hat erstmal nicht speziell mit Python zu tun, sondern mit GUI-Programmierung und OOP im allgemeinen.

Zum ersten Beispiel: Da würde ich die Seiten einzeln entwerfen und dem Assistentenfenster dann immer nur ein Containerelement mit den Seiteninhalten austauschen. So sind die Seiten unabhängiger, man muss nicht soviel entfernen und kann auch nicht etwas auf einer Seite beim Wechsel "vergessen". Ausserdem kann man alle Seiten so wie sie sind auch "behalten", falls man vielleicht auch noch einen "Zurück"-Knopf in den Assistenten einbauen möchte.

Beim zweiten Beispiel fehlt mir ein bisschen die Trennung zwischen GUI und Programmlogik. Wenn die Einstellungsklasse von einer GUI-Klasse erbt, dann funktioniert das Programm nicht ohne GUI und nur mit deutlichen Änderungen mit einem anderen GUI-Toolkit. Die übliche OOP-Antwort darauf heisst "Model, View, Controller" (MVC). Bei den Einstellungen wäre das "Model" also ein Objekt, dass nichts mit GUI zu tun hat, sondern nur die Einstellungen speichert. "View" ist das, was Du mit dem QtDesigner entworfen hast und die abgeleitete Klasse mit Funktionen, die beides verbindet, könnte man als "Controller" sehen.

Globale Daten sind ja immer ein bisschen "böse", wobei Einstellungen, die programmweit benötigt werden, die Ausnahme von der Regel sind. So etwas wird in Python oft in einem Modul abgelegt, das man überall importieren kann.

AMDUser

(Themenstarter)

Anmeldungsdatum:
12. August 2005

Beiträge: 96

Wohnort: Leichlingen

Hallo Marc und danke für die ausführliche Antwort!
Das klingt doch schonmal ganz interessant. Aber ich hab trotzdem nocheinmal 2 Fragen:
1. Was genau meinst du mit Containerelement?

und die zweite ist schon wieder ein bisschen länger:
das MVC klingt super, wenn ich wüsste, wie man das genau anwenden soll. Mal ein konkretes Beispiel von einem Projekt an dem ich gerade arbeite (2. Beispiel).
Es ist ein Tool, was ich für meine Schule schreibe, um die Schüleraccounts zu verwalten. Selbige liegen in einem LDAP-Tree sortiert nach Klassen. Dieses Tool soll also Alle Schüler mit Namen, Vornamen und Klasse aus einer Datei einlesen und dann entweder Anlegen, Verschieben oder Löschen. Das ganze hab ich schon fast fertig, nur bin ich mit dem Aufbau nicht so ganz zufrieden (siehe Beispiel 2).
Das ganze Besteht aus 3 Dateien: main.py (Mein Code), mainui.py (UI generiert mit pyuic4), confui.py (Einstellungsdialog, wie 2)

Ich hab eine größtenteils funktionsfähige Fassung mal Beigelegt. Ich frage mich jetzt nur, wie man das umschreiben sollte, damit dieses MVC Konzept zum tragen kommt.
Noch einmal vielen Dank für deine (oder eure) Hilfe

Alex

confui.py (13.0 KiB)
Download confui.py
mainui.py (5.1 KiB)
Download mainui.py
main.py (16.3 KiB)
Download main.py

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

Mit Containerelement meine ich ein Element das alle anderen enthält, so dass man nur dieses eine Element austauschen braucht.

Um Code und GUI zu trennen solltest Du am besten mal die ganze Logik ohne die GUI programmieren. Einlesen der CSV-Datei, das erstellen eines Benutzernamens usw. hat nichts mit GUI zu tun. Einiges davon ist auch in einer Klasse fehl am Platz. Wenn self nicht benutzt wird, sollte man sich immer fragen, ob das überhaupt eine Methode sein sollte.

Der Quelltext ist mit Tabs eingerückt, statt mit vier Leerzeichen pro Ebene. Ausserdem gibt es ein paar Zeilen, die länger als 80 Zeichen sind.

Bei ``except`` sollte man immer konkrete Ausnahmen angeben, sonst wird dort wirklich alles "verschluckt". Beim Importieren würde ich die Ausnahmebehandlung einfach weglassen. Dann sieht man auch was genau fehlt.

Dann solltest Du das Programm mal nach Instanzvariablen absuchen, die keine sind. self.fileobject und self.filecontent in SchoolTool zum Beispiel. Wenn Attribute nur in einer einzigen Methode verwendet werden, sollten es keine Attribute sondern lokale Namen sein.

Das Einlesen der CSV-Datei ist recht umständlich. Datei-Objekte sind "iterable", d.h. man kann direkt in einer Schleife über die Zeilen iterieren. self.filecontent ist ein überflüssiger Zwischenschritt.

        lines = open(self.filename)
        self.userarray = [line.strip().split(';') for line in lines]
        lines.close()

Wenn Du bei Python auch so aktuell sein willst wie beim GUI-Toolkit, also nur das neueste voraussetzt, kannst Du in Python 2.5 auch das ``with``-Schlüsselwort aktivieren und verwenden. Zum aktivieren muss als erster Import ``from __future__ import with_statement`` im Quelltext stehen. Dann sieht das Einlesen so aus:

        with open(self.filename) as lines:
            self.userarray = [line.strip().split(';') for line in lines]

Für CSV-Daten die aus Tabellenkalkulationen oder ähnlichem stammen, würde ich allerdings das csv-Modul benutzen. Das CSV-Format ist nämlich gar nicht so simpel.

SchoolTool.startoperation() ist zu lang. Das lässt sich sicher auf mehrere Funktionen aufteilen.

simulate lässt sich einfacher setzen, die Methode aus der GUI gibt doch schon einen Wahrheitswert zurück:

        simulate = self.ui.checkBox.isChecked()

Und beim Testen dann nicht explizit mit 0 oder 1 vergleichen, sondern simulate einfach als Wahrheitswert verwenden.

Ob man die LDAP-Verbindung (self.conn) unbedingt an das Objekt binden muss, ist fraglich. Wenn die Verbindung fehlschlägt, dann wird das zwar an der GUI ausgegeben, aber der Code läuft einfach weiter!?

self.sambaAlgoBase und self.sambaSID sind eigentlich lokale Namen.

Mit cleardata und realnames werden zwei parallele Datenstrukturen aufgebaut deren Inhalt eigentlich zusammengehört. Zusammen mit der "Indexerei" die dann in der ``for``-Schleife folgt, ist der Quelltext viel unverständlicher als er sein müsste. Wenn man so etwas schreibt…

for i in range(len(container)):
    do_something(container[i])

…kann man sich in 99% der Fälle den Index i sparen und gleich über die Elemente von container iterieren:

for element in container:
    do_something(element)

Und konstante Indexe sollte man nach Möglichkeit vermeiden und stattdessen einen aussagekräftigen Namen verwenden. Die beiden temporären Listen würde ich ganz weglassen und die ``for``-Schleife so beginnen:

        for user_class, surname, given_name in self.userarray:
            username = create_username(given_name, surname)
            surname = clear_name(surname)
            given_name = clear_name(given_name)
            userrname = '%s %s' % (surname, given_name)

Wobei username und userrname viel zu ähnlich sind, als das sie keine Verwirrung stiften würden.

random.seed() muss man eigentlich nicht aufrufen. Das wird beim ersten Import des random-Moduls automatisch gemacht.

Bei createusername() hat der "Block" mit der ersten Schleife keinen Effekt, weil das Ergebnis von der nächsten Schleife wieder "überschrieben" wird.

Kompakter liesse sich die Funktion so schreiben:

def createusername(fn, sn):
    fn_tmp = clearname(fn.split(' ', 1)[0].split('-', 1)[0])
    sn_tmp = clearname(''.join(sn.split('-')))
    return ('%s.%s' % (fn_tmp, sn_tmp)).lower()

Bei clearname(), was auch eine Funktion sein sollte, gibt's eine ähnliche Schleife die keinen Effekt hat. Wenn ich das richtig verstanden habe, kann man diese Schleife mit einer Zeile ersetzen:

    name = '-'.join(s.strip().capitalize() for s in name.split('-') if s) + '-'

In checkstart() kann man ein wenig Quelltext einsparen, wenn man eine Schleife über Attribute und Meldungen und nur eine ``if``-Abfrage benutzt.

Die ganzen "Getter" in SchoolConfig kann man sich sparen. Das ist "unpythonisch".

AMDUser

(Themenstarter)

Anmeldungsdatum:
12. August 2005

Beiträge: 96

Wohnort: Leichlingen

ganz ehrlich: geil ☺
Ich hätte mir nicht im Traum gedacht, dass du dir so viel Mühe gibst, aber bin dir unglaublich dankbar.
Ich bin im Vergleich zu anderen Programmiersprachen noch ziemlich neu in Python, aber trotzdem total begeistert. Dieser Post hat noch einmal erheblich dazu beigetragen. Der schlechte Programmierstil rührt wahrscheinlich von meinen alten Gewohnheiten als PHP-Programmierer, aber jetzt wurde hier mal Klarheit geschaffen 😉 .

Das hat mich doch noch einmal erheblich weitergebracht und ich werde die Vorschläge bestimmt umsetzen (Diese Eleganz von Python beeindruckt mich immer wieder, man (ich) muss sie nur noch zu nutzen lernen).
Also noch einmal: Vielen Dank für deine Mühe und ich werde spätestens berichten, sobald das Programm für dieses Jahr seine Arbeit getan hat ☺

Alex

P.S. Gleich mal den Thread hier auf die Externe Festplatte pumpen, falls ich in der Gegend nicht doch irgendwo ein WLAN finden sollte 😉

Antworten |