Ordnungsgemäß Coden

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
TrayserCassa
User
Beiträge: 97
Registriert: Donnerstag 2. Mai 2013, 19:11

Schönen Abend alle miteinander,
ich bin mal wieder dabei ein wenig mit python zu programmieren. Mittlerweile auch an der richtigen Schule dafür. :)

Wir haben Bereits mit MySql programmiert und ich persönlich mag es sehr. Nun habe ich mir vorgenommen ein Klassenkassen-System zu programmieren (Als erstes nur in der Console, dann mit einer GUI verschönern). Das ist ein eigenes Projekt und möchte dies gerne mit einer MySQL-Datenbank (Die auf meinem Raspberry PI läuft) realisieren.

Da mein Code in Python nicht so "sauber" ist wollte ich wieder ein paar Anfänger fragen stellen.

Das ist fürs erste mein Code:

Code: Alles auswählen


import pymysql


class DBMS:


	def __init__(self):

		try:
			self.dbms = pymysql.connect(host="192.168.178.24",
								user="bxt",
								passwd="***",
								db="bxt",
								autocommit=True)
			self.cur = self.dbms.cursor()
			
		except Exception as e:
			print("Kann keine Verbindung zur Datenbank aufbauen!")
			print(e)
		

	def Search(self, name, vorname):
		x = self.cur.execute("SELECT * FROM schueler WHERE name LIKE \"%%%s%%\" AND vorname LIKE \"%%%s%%\"" %(name, vorname)) 
		print(x)
		
		for i in self.cur:
			print(i)

	def Save(self):
		pass


if __name__ == "__main__":
	session = DBMS()
	session.Search("Derks", "Patrick")

Das die Variable "x" einen vernünftigen Namen bekommt ist schon vorrausgesetzt ^^ Es geht nur um den Code-Style. :)

Leider dauert es noch ein halbes Jahr bis wir mit C# soweit sind, dass wir mit Fenstern/GUI Anwendungen arbeiten und mit OOP anfangen. Deshalb helft mir nur wenn Ihr wirklich langeweile habt ^^ hat keine Eile mir zu helfen :)


Fragen:
1. Soll ich die Variable "Session" auf der Modulebene so lassen? Ich meine das Variablen auf modularer Ebene ein Sicherheitsrisiko dastellen.
2. Wenn ich nun eine Config Datei schreibe, die eine Passwort abfrage beinhaltet, so würde ich ConfigParser nutzen. Wie gehe ich dann vor? Soll ich eine neue Klasse schreiben und darin die Config einlesen?
3. Außerdem müsste ich auch so programmieren, dass ich mit Tkinter einfach eine GUI in das Programm einarbeiten kann. Ergo habe ich eine Hauptklasse. Ich versteh nicht ganz was meine Hauptklasse ist. Sind es die Informationen die gesammelt werden in der Klasse DBMS oder ist es die TK klasse, die die Informationen benötigt?
4. Ich bin auf diese Youtube-videos gestoßen (https://www.youtube.com/watch?v=pxbdnrjf-Uc). Liege ich richtig, dass ich so programmieren muss das die "BaseClass" z.B. "ConfigParser" oder auch "Tkinter" ist und in meiner Klasse "InheritingClass" ich die Methoden der "BaseClass" nutze?
5. Ich möchte gerne mit MySQL arbeiten. Ich weiß das PgSql oder Sqllite besser wäre, aber die Daten aus der DB wird vermutlich auch in einem PHP script benutzt. Ist "pymySQL" das richtig Modul oder gibt es bessere?

Wie schon geschrieben .. Es hat keine Eile mir zu helfen, ich möchte halt nur nicht "falsch" Coden. Bzw brauche ich einfach ein wenig wissen über OOP, speziell das anwenden von anderen Klassen die ich mir mit "import" geladen habe. :)

Erstmal Danke fürs Lesen und die damit geopferte Zeit :)

MfG
Trayser
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

TrayserCassa hat geschrieben:Leider dauert es noch ein halbes Jahr bis wir mit C# soweit sind, dass wir mit Fenstern/GUI Anwendungen arbeiten
Mein Beileid. Ich arbeite gerade ein C# Programm mit GUI um, und bin am verzweifeln, wieviel Copy-and-Paste der Code enthält, und wie schwierig es ist, den zu strukturieren. Warum braucht jeder Radio-Button einen eigenen Namen und kann nicht in eine Liste gesteckt werden?

Als erstes solltest Du Dir mal angewöhnen, mit 4 Leerzeichen einzurücken. Vor lauter weißer Fläche findet man ja bei Dir den Code gar nicht mehr. Cursor sind etwas kurzlebiges und sollten nicht über mehrere Methodenaufrufe existieren. Dein Execute ist mehr als ein riesiges Sicherheitsloch. Das passt sogar vom Thema her genau auf Dein Problem damit.
Bevor Klassen Sinn machen, muß Dein Problem noch deutlich komplexer werden. Nur soviel: Vererbung wird in Pythons OOP fast nie gebraucht, und so wie Du das beschreibst, macht es auch überhaupt keinen Sinn.
TrayserCassa
User
Beiträge: 97
Registriert: Donnerstag 2. Mai 2013, 19:11

@Sirius3

Oh ja .. copy paste. Hab ich in der Klasse auch. Fragt man den "super" Programmierer nach was die einzelnen Zeilen machen und er: "Das weiß ich nicht." Naja muss ja nicht mit denen zusammen Arbeiten.

Einrückung:
In Sublime Editor sind es genau 4-Leerzeichen (Tab). Außer von Zeile 11-15 natürlich. Meintest du das?

Cursor:
Also sollte ich in "Search" die Zeile 16 nutzen und sobald ich in der Methode Save einen "INSERT" Befehl setzten möchte wieder die Zeile 16 nutzen?

Sicherheit:
Ich würde mit einer if-Abfrage den String überprüfen, ob dort ein ";" vorhanden ist. Würde das schon reichen? Vermutlich nicht... ich stöber gleich mal im Forum danach :3

Klassen:
Das Programm wird ja noch wesentlich umfangreicher und ich möchte es üben in OOP zu programmieren. Außerdem finde ich Klassen übersichtlicher als ein "Ablauf"-Code.. Zudem wenn ich mit tkinter programmiere (wo ich mich noch schlau lesen muss), nutze ich doch ein Event z.B. ein Klick auf ein Button und damit müsste ich dann eine Methode ausführen, wie z.B. "Safe" mit einem "INSERT" Befehl... Soll ich dann den TK code nach der Zeile 34 schreiben?

Danke schonmal für die schnelle Antwort :)

MfG
Trayser

Edit:

So habe ich jetzt Bespiele gefunden, um ein execute zu Sichern. (Docu --> https://www.python.org/dev/peps/pep-0249/#paramstyle

Code: Alles auswählen

x = cur.execute("SELECT * FROM schueler WHERE name LIKE %s AND vorname LIKE %s", (name, vorname))
Zuletzt geändert von TrayserCassa am Donnerstag 16. April 2015, 20:56, insgesamt 1-mal geändert.
BlackJack

@TrayserCassa: Du verwendest 1 Tab und nicht 4 Leerzeichen. Das ist ein Unterschied. Ein Tab hat keine Länge beziehungsweise ist die in verschiedenen Umgebungen unterschiedlich, wie man im ersten Beitrag ziemlich deutlich sehen kann. Darum ist die Konvention bei Python vier Leerzeichen pro Ebene zu verwenden.

Zeile 16 bindet den Cursor ja an das Objekt, das ist nicht mehr notwendig wenn man ihn nur für die lokale Verwendung erzeugt.

Vergiss irgendwelche selbst gebastelten Überprüfungen von Werten die in SQL eingefügt werden sollen und überlass das dem Datenbankmodul. In die SQL-Anweisung gehören für Werte Platzhalter und die Werte werden bei `execute()` als zweites Argument übergeben. Das ist nicht nur sicher sondern kann auch deutlich effizienter sein, weil die Datenbank dann für verschiedene Werte immer das gleiche SQL sieht und wenn sie schlau ist cached sie den Ablaufplan für die Abfrage so dass das SQL nicht jedes mal neu geparst und übersetzt werden muss.

Ich persönlich gehe fast immer eine Abstraktionsschicht weiter und verwende SQLAlchemy statt SQL manuell zu schreiben.

OOP üben heisst auch zu lernen wann man *keine* Klassen schreibt. Klassen müssen Sinn machen und sind kein Selbstzweck. Ich weiss jetzt nicht was Du mit einem ”Ablaufcode” meinst. Im Moment hast Du eine Klasse mit einer `__init__()` und *einer* Methode die etwas tut. Das ist in aller Regel eine überkompliziert geschriebene Funktion und macht keinen Sinn und auch nichts übersichtlicher. Und die Klasse kapselt genau einen Wert, den hätte man auch in einer Funktion erzeugen können. Und ob man dann ``session.search('Peter', 'Pan')`` oder ``search(session, 'Peter', 'Pan')`` schreibt, macht erst einmal keinen grossen Unterschied.

Bei GUIs fängt eine Klasse dann schon eher an Sinn zu machen weil man dort mehrere GUI-Elemente zusammenfasst und Operationen hat die darauf zugreifen. Und das ist in Python eine Aufgabe für Klassen, weil man mit Closures schnell an Grenzen stösst.
TrayserCassa
User
Beiträge: 97
Registriert: Donnerstag 2. Mai 2013, 19:11

@BlackJack

Erstmal Dankeschön für die Hilfreichen Tipps :) Viele schreiben das du "meckerst" oder was ansprichst, was nicht zum Ziel führt, sondern mehr was Sicherheit und Code-Style betrifft. Dadurch lernen Anfänger wie auch ich, dass Pep-8 kennen und gleich vernünftig zu coden :)
(Beispiel die String formatierung mit "%" und nicht mit Verkettungsoperator "+")
Das gilt übriegends für eigentlich alle alt eingefleischten User hier :)

Tabulator und Leerzeilen:
Hmm dann müsste ich das im Editor mal umstellen ...

"Code-Ablauf":
Z.B. eine Batch-datei wäre sowas für mich. (Wir haben das goto in C# gelernt) Dessen Anwendung hier vermutlich einigen die Haare zu Berge stehen lassen. Der Code läuft von oben nach unten, ohne "Sprünge" (z.b. mit goto oder auch Methoden).

Das Programm:

Code: Alles auswählen

Datenbank:

    Tabelle: "Schueler"

        Attribute sind:
            Primary Key email
            vorname 
            name

    Tabelle: "schulden"

         Attribute sind:
            Primary Key ticket_id
            Foreign Key email
            betrag
            comment
Das Programm soll nun mit "search" einen User aussuchen. Dann wird der Primary Key email in "self.email" gespeichert. Dann kann man schulden eintragen. Bin grade dabei ein wenig zu testen.

Sobald das funktioniert kümmer ich mich um die GUI. Danach möchte ich mit einer Konfigurationsdatei Hashwerte speichern und das Programm mit einem Login und Passwort sichern. Bis dahin habe ich aber noch Zeit um erstmal den Code für die Datenbank zu schreiben.

Code Nicht fertig und ungetestet! Soll nur zur verdeutlichung des Ablaufes dienen.:

Code: Alles auswählen

import pymysql
import sys

class DBMS:
	def __init__(self):
		try:
			self.dbms = pymysql.connect(host="192.168.2.102",
								user="bxt",
								passwd="***",
								db="bxt")
			
		except Exception as e:
			print("\n Kann keine Verbindung zur Datenbank aufbauen! \n %s" % (e))
			sys.exit()		


	def Search(self, name, vorname):
		# Hier wird der Jeweilige schüler ausgesucht und der Primär schlüssel wird gespeichert (email)

		cur = self.dbms.cursor()
		result = cur.execute("SELECT * FROM schueler WHERE name LIKE %s AND vorname LIKE %s", (name, vorname)) 

		if 	result == 1:
			for i in cur:
				print(i)
				self.email = i[0]
		else:
			print("Mehr als eine Person Gefunden! Bitte versuche es erneut!")

	def Save(self, betrag, comment):
		# Hier werden seine Schulden eingezahlt. Vorher muss der Primary Key self.email existieren.
		
		cur = self.dbms.cursor()
		result = cur.execute("INSERT schulden (email, betrag, comment) VALUES (%s, %s, %s)", (self.email, betrag, comment))
		self.dbms.commit()

		

	def End(self):
		self.dbms.rollback()
		self.dbms.close()

if __name__ == "__main__":
	session = DBMS()
	while(True):

		name = input("Name: ")
		vorname = input("Vorname: ")
		session.Search(name, vorname)
		
		if name == "1":
			break;

	session.End()
Frage:

Man erkennt das ich auf Modulebene Variablen habe. Soll ich das so machen oder soll ich eine main() Methode schreiben und das dort erledigen?

MfG
Trayser
BlackJack

@TrayserCassa: Du erwähnst ja selber PEP8 — die Methodennamen halten sich da nicht dran. :-)

DOS-Batch-Dateien verwenden in der Regel keine Funktionen beziehungsweise wurde da ja mit Labels, GOTO, SETLOCAL, und ENDLOCAL so etwas ähnliches wie rudimentäre Funktionen reingebastelt. Solchen Code möchte man natürlich nicht haben. Aber zwischen so etwas und Klassen gibt es ja noch prozedurale und funktionale Programmierung.

Die E-Mail-Adresse als Primärschlüssel ist eher ungewöhnlich. Das ist zwar theoretisch ein guter Schlüsselkandidat, aber aus zwei Gründen nicht als Prämerschlüssel geeignet:

1. Es ist als Zeichenkette ineffizienter beim verbinden von Tabellen weil Zeichenketten vergleichen aufwändiger ist als ganze Zahlen zu vergleichen.

2. Die E-Mail-Adresse ist nicht in Stein gemeisselt und wenn man die ändern will, dann muss man sie nicht nur in der `schueler`-Tabelle ändern sondern auch in jeder Tabelle wo der Wert als Fremdschlüssel gespeichert ist.

Es ist üblich einen künstlichen Schlüssel zu verwenden, weil man nur ganz selten einen natürlichen Schlüssel hat der sich tatsächlich nie ändert, wenig Speicher benötigt und über den man effizient ”joinen” kann.

Ich weiss nicht so recht was man mit dem Quelltext anfangen soll wenn Du selbst schon dazu schreibst das der unfertig und ungetestet ist. Das die Klasse nicht gut ist wurde ja schon geschrieben. Mit `self.email` wird sie nicht besser denn das hat IMHO also Attribut nichts auf der Klasse zu suchen. Von einer `search()`-Methode erwartet man das die ein Suchergebnis an den Aufrufer zurückliefert und nicht ein neues Attribut einführt. Attribute sollten in der Regel nur in der `__init__()` neu eingeführt werden.

Wenn man da etwas objektorientiertes machen möchte könnte man die Such*funktion* beispielsweise ein `Schueler`-Exemplar zurückgeben lassen von dem man die Schulden abrufen und neue Einträge machen kann.

Ich würde da aber wohl gleich das ORM von SQLAlchemy verwenden bevor ich das noch mal selber erfinde. :-)

Die API hat auch ein schwerwiegendes Problem. Mal angenommen ich Suche nach Peter Pan, der wird gefunden und seine E-Mail wird im Attribut gespeichert. Ich trage einen Betrag für ihn ein. Dann suche ich nach Klaus Meier. Den gibt's aber mehr als einmal. Das wird zwar auf dem Bildschirm ausgegeben, aber im Code habe ich keine Möglichkeit festzustellen das trotz neuer Suche immer noch die E-Mail-Adresse von Peter Pan an das `email`-Attribut gebunden ist! Wenn ich jetzt in der Erwartung einen Eintrag für Klaus Meier zu speichern `save()` aufrufe, dann speichere ich noch mal neue Schulden für Peter Pan. Der ist dann bestimmt nicht glücklich. ;-)

``while`` ist keine Funktion, das sollte man also auch nicht so schreiben das es wie ein Funktionsaufruf aussieht. Die Klammern gehören da sowieso nicht hin, denn sie sind überflüssig.

Und Variablen auf Modulebene sind in der Tat ein Problem.
TrayserCassa
User
Beiträge: 97
Registriert: Donnerstag 2. Mai 2013, 19:11

Zu der Datenbank:

Ich habe meinen Datenbank Lehrer drüber schauen lassen und er möchte gerne eine DUMP haben. Ich habe im übrigem die User schon angelegt. Falls ich den Primary Key mit "Alter" in MySQL so ändern kann, so werde ich das noch tun :).

Edit:
Soweit ich das sehe müsste ich die DB neu erstellen.. Ergo behalte ich die Email als Primary Key :)

Wir haben es nur so gelernt, da ich die e-mail sowieso speichern muss und die sich in unsere Klasse nicht ändern darf. Falls dies der Fall ist gibt es den User noch, erhält aber keine Updates via E-mail. Hier ist nur ausschlaggebend, dass der User seine schulden tilgt ;)

Zum Programm:

Also mit dem Problem das mit dem Speichern versteh ich .. Also sollte ich den User, wenn ich die Klasse initialisiere, gleich in der __init__ funktion speichern. So habe ich dann mit self.email den aktuellen User und kann dann mit den Methoden auf den User "einwirken". Im Übrigem möchte ich, wenn ich ein User gefunden habe, ihn in einer Liste speichern. ich habe den ablauf ungefähr so geplant:

1. Ein User wird gesucht.
2. Falls 1 erfolgreich, dann kann man den User über Methoden "Löschen", "schulden eintragen".

Wenn ich richtig überlege müsste ich den User, wenn ich einen rausgesucht habe, in der Klasse speichern. Also muss ich schon in der __init__ funktion den User raussuchen, da es nur dort sinn macht Variablen zu speichern. Liege ich da richtig?

Mir fehlen da leider Fachbegriffe ._.

Pep8 meinst du vermutlich die Methoden mit dem ersten Parameter "cls" zu nennen? zumindest habe ich das so gesehen... In der __init__ funtkion soll man als ersten Parameter self verwenden und bei Methoden cls .. Ist mir auch neu ^^ Kann aber auch nicht alles nach Pep-8 programmieren. Das wäre für ein Anfänger doch zu viel des guten ;)

Die Variablen auf Modulebene bekomme ich ja nur weg, wenn ich innerhalb der Klasse "Input" aufrufe. Aber ich glaube das ist vermutlich schlimmer als wie es jetzt schon ist. Da bin ich leider überfragt.

Ich merk schon.. Ich soll wohl das Modul SQLAlchemy verwenden ^^ Nur wenn ich jetzt schon an dem Prinzip für OOP scheitere hilft es mir auch nichts wenn ich ein anderes Modul benutze.. Ich änder erstmal die Struktur der Datenbank. Dann teste ich an den Code noch rum. Bis dahin hat bestimmt schon jemand geschrieben und fügt ein neues Kuchenstück in das Projekt ein :)

Aber danke für dein Einsatz und Hilfe :)

MfG
Trayser
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

@TrayserCassa: Du wirst früher oder später noch öfters Deine Datenbank migrieren müssen. Also mach Dein Model sinnvoll, also mit richtigem Primärschlüssel, dann lernst Du das ändern von Tabellen gleich mit. Du hast jetzt eine Klasse DBMS, die soweit ich das verstanden habe den Datenbankzugriff kapselt. In einer dieser Instanzen Daten eines einzelnen Datensatzes (email) zu speichern, macht da keinen Sinn. Also hast Du Methoden, die als Parameter eben die volle Information brauchen, oder Du machst eine zweite Klasse, die z.B. einen Schüler repräsentiert. Die Variablen auf Moduleebene bekommst Du weg, indem Du eine Funktion schreibst. Du hast schon jetzt Verarbeitungs- und Anzeigelogik gemischt: Du gibst Fehlermeldungen auf dem Bildschirm aus, anstatt Exceptions zu werfen und die Fehlerbehandlung im Hauptprogramm zu machen.
BlackJack

@TrayserCassa: Du könntest ein Programm schreiben das die Änderungen an den Tabellen vornimmt.

Wenn die Schüler über die E-Mail-Adresse über ihre Ausstände informiert werden, dann ist das schon irgendwie wichtig wenn die veränderbar ist damit sie auch bei einem E-Mail-Wechsel noch informiert werden das und wieviel sie noch zahlen müssen. Eine E-Mail-Adresse die sich nicht ändern darf ist sehr eigenartig. Das hat der Benutzer doch gar nicht selber 100% unter Kontrolle. Viele haben eine Adresse beim Internetanbieter, oder bei Microsoft, oder Yahoo, oder Google. Solche Konten können gehackt und/oder aus den verschiedensten Gründen vom Anbieter gesperrt werden. Oder ein Benutzer wechselt den E-Mail-Anbieter aus irgendwelchen anderen Gründen.

Den ”aktuellen” Benutzer solltest Du in der `DBMS`-Klasse gar nicht speichern und auch in der `__init__()` noch nicht suchen. Dann *muss* man ja beim Erstellen des Objekts einen Schüler suchen, auch wenn man das gar nicht möchte, weil man Beispielsweise einen neuen Schüler anlegen möchte. Dazu muss man ja vorher keinen suchen. In einer `Schueler`-Klasse könnte man die Daten für einen Schüler speichern den man abgefragt hat wenn man Schüler als Objekte im Programm darstellen möchte.

`cls` ist der Name des ersten Arguments bei *Klassenmethoden* (`classmethod()`), sonst `self` oder bei statischen Methoden gar nichts. Das meinte ich nicht, ich meinte die Schreibweise der Namen, die ist `klein_mit_unterstrichen`. Und man kann sich schon an PEP8 halten. Bei Namensschreibweisen, insbesondere bei `self` (und `cls`) und beim Einrücken sollte man das tun.

Wieso müsstest Du in der Klasse `input()` aufrufen? Das Hauptprogramm kann doch einfach in eine Funktion verschoben werden. Was aus der `DBMS`-Klasse noch verschwinden sollte sind die `print()`-Ausgaben und vor allem auch das `sys.exit()`.
BlackJack

@TrayserCassa: Noch mal was zum ersten Beitrag:

Ad 1. Variablen auf Modulebene sind kein Sicherheitsrisiko sondern es besteht die Gefahr das man die irgendwo benutzt ohne das sie als Argument übergeben wurden und damit würde man sich unübersichtliche Abhängigkeiten schaffen die das verstehen und warten von dem Code schwerer machen.

Ad 2. Wozu eine Klasse? Ich habe ein bisschen das Gefühl Du meinst OOP wäre wenn man alles irgendwie in eine Klasse steckt und dann hätte man einen OOP-Ansatz. Dem ist nicht so. Bei OOP geht es um Objekte und wie die miteinander kommunizieren. Es gibt OOP-Sprachen die haben nicht einmal ein eigenes Sprachkonstrukt für Klassen. Da werden neue Objekte durch das ”Klonen” von vorhandenen Objekten erstellt, die man dann abändern kann, zum Beispiel in dem man Methoden hinzufügt, und dann wieder durch ”Klonen” weitere Objekte erstellen kann die dann auch diese Methoden haben.

Ad 3. Was Deine ”Hauptklasse” ist musst Du schon selber wissen. Was ist denn das wichtigste in der Geschäftslogik von Deinem Programm? Gibt es dort überhaupt *das* Hauptobjekt. Was ist denn das wichtigste Ding in Deinem Entwurf? Ist das ein Schüler? Oder eine Sammlung von Schülern? Alle Schüler?

Ich denke der Ansatz einfach mal mit irgendeiner Klasse anzufangen und da dann nach und nach Funktionen/Methoden einzufügen führt ohne Erfahrung zu keinem guten Entwurf weil genau das fehlt: ein Entwurf. Man sollte schon mindestens ungefähr wissen was für Objekte mit welchen Methoden man benötigt bevor man die erste Klasse schreibt.

Dazu macht man sich in der Regel zuerst Gedanken darüber was das System leisten muss, welche Anwenderrollen es gibt, falls es verschiedene gibt, also zum Beispiel Admin, Kassenwart, normaler Schüler, ”Bots” die regelmässige Tätigkeiten ausüben, und was die jeweiligen Rollen mit dem System anstellen. Also die Arbeitsschritte die sie für verschiedene Arbeiten durchführen. Diese Überlegungen muss man ja für den Datenbankentwurf eigentlich auch schon angestellt haben. Und so wie sich daraus Tabellendefinitionen ergeben, müsste man auch wichtige Klassen und die Operationen darauf schon aus diesen Rollen und Anwendungsfällen erkennen.

Weder die Datenbank noch die GUI sollte eine Art ”Hauptklasse” sein, denn beide enthalten nicht die Geschäftslogik, also die Operationen die für dieses Programm wichtig sind und die eigentliche Funktionalität ausmachen. Die Datenbank speichert bloss die Daten und die GUI ist bloss *eine* Möglichkeit die Funktionalität zu nutzen. Beides ist austauschbar. Man kann auf der einen Seite verschiedene RDMS verwenden oder sogar die Datenhaltung in einer anderen Form erledigen (Dateien, NoSQL-Datenbank, Webdienst, …). Und auf der anderen Seite kann man das GUI-Toolkit durch ein anderes austauschen, oder eine Webanwendung daraus machen, oder die Programmlogik automatisiert benutzen (regelmässige Erinnerungen verschicken, regelmässig Beträge sollstellen, Bewegungen auf einem echten Bankkonto überwachen und Beträge automatisch als bezahlt markieren, …)

Ad 4. Das Video ist IMHO symptomatisch für den (schlechten) Versuch OOP einzuführen. Da werden sinnlose ”Spielzeug”-Klassenhierarchien aufgebaut ohne tatsächlich eine komplexe Anwendung dafür zu zeigen. Was bleibt ist letztendlich nur das man hinterher die Syntax für Klassen und Vererbung kennt, aber keine Ahnung hat wie man denn nun tatsächiche Probleme sinnvoll auf Klassen aufteilt. Das Problem dabei ist dass man für Vererbung in den meisten Fällen schon ein komplexeres Problem zum Lösen benötigt. Insbesondere bei Sprachen mit ”duck typing” kommt noch dazu das man in vielen Fällen ohne Vererbung auskommt weil es reicht das ein Objekt das erwartete Verhalten an den Tag legt ohne formal von einem bestimmten Typ sein zu müssen oder ”Interfaces” per Vererbung deklarieren zu müssen. Am ehesten machen noch echte GUI-Beispiele Sinn, weil das eines *der* Einsatzgebiete für OOP ist und man kaum ohne auskommt weil die üblichen GUI-Toolkits objektorientiert organisiert sind.

Ad 5. Wenn Du mit MySQL arbeiten möchtest ist das okay, aber die Begründung ist komisch. Man kann mit PHP nicht nur auf MySQL-Datenbanken zugreifen. PostgreSQL- und SQLite-Datenbanken kann man mit PHP genau so verwenden. Mit der `PDO`-API auch über die gleiche API. Es gibt *andere* Module die die DB-API V2 für MySQL implementieren, ob die *besser* sind hängt davon ab was für Anforderungen gestellt werden und ob das jeweilige Modul die erfüllt. Ich denke für eine Klassenkasse dürfte es ziemlich egal sein solange das verwendete Modul grundsätzlich funktioniert.

Ich glaube ich habe SQLAlchemy schon mal erwähnt. ;-) Das kann auch pymysql verwenden. Vollkommen ungetestet:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
from sqlalchemy import (
    Column, create_engine, DECIMAL, ForeignKey, func, INTEGER, TEXT, VARCHAR
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, Session


Base = declarative_base()


class Pupil(Base):
    __tablename__ = 'pupil'

    id = Column(INTEGER, primary_key=True)
    email = Column(VARCHAR(254), nullable=False, unique=True)
    given_name = Column(VARCHAR, nullable=False)
    surname = Column(VARCHAR, nullable=False)

    # `debit_positions` as back reference from `DebitPosition`.

    def add_position(self, amount, remark):
        self.debit_positions.append(
            DebitPosition(pupil=self, amount=amount, remark=remark)
        )

    @classmethod
    def search(cls, session, given_name=None, surname=None):
        query = session.query(cls)
        if given_name:
            query = query.filter(
                cls.given_name.like('%{0}%'.format(given_name))
            )
        if surname:
            query = query.filter(cls.surname.like('%{0}%'.format(surname)))
        return query.all()


class DebitPosition(Base):
    __tablename__ = 'debit_position'

    id = Column(INTEGER, primary_key=True)
    pupil_id = Column(ForeignKey(Pupil.id), nullable=False)
    amount = Column(DECIMAL(6, 2), nullable=False)
    remark = Column(TEXT, nullable=False, default='')

    pupil = relationship(Pupil, backref='debit_positions')


def main():
    engine = create_engine(
        'mysql+pymysql://bxt:***@192.168.2.102/bxt?charset=utf-8',
        echo=True,  # Logs all the generated SQL queries.
        pool_recycle=3600
    )
    Base.metadata.create_all(engine)  # Creates the tables if they don't exist.
    
    session = Session(engine)

    # Neuen Schüler erstellen.
    session.add(
        Pupil(given_name='Peter', surname='Pan', email='pp@neverland.com')
    )
    session.commit()

    # Alle Schüler mit Vornamen 'Peter' abfragen und zusammen mit den
    # einzelnen Schuldenpositionen auflisten.
    pupils = Pupil.search(session, 'Peter')
    for pupil in pupils:
        print(u'{0.surname}, {0.given_name} ({0.email})'.format(pupil))
        for position in pupil.debit_positions:
            print('    {0.amount} - {0.remark}'.format(position))
    session.commit()

    # Für den ersten 'Peter' eine neue Sollstellung anlegen.
    pupil = pupils[0]
    pupil.add_position(42.23, 'Blah...')
    session.commit()

    # Gesamtzahl der Schüler und die Gesamtsumme ermitteln.
    print(
        '{0} Schueler haben insgesamt {1} EUR zu zahlen.'.format(
            session.query(Pupil).count(),
            session.query(func.sum(DebitPosition.amount)).scalar()
        )
    )
    session.commit()


if __name__ == '__main__':
    main()
Bei der Definition der ORM-Klassen kann man bei den Tabellenspalten auch Namen angeben falls die von den Attributnamen auf der Klasse abweichen (sollen).
TrayserCassa
User
Beiträge: 97
Registriert: Donnerstag 2. Mai 2013, 19:11

Erstmal Entschuldigung das ich nicht so schnell antworten konnte.. Haben 2 Klausuren geschrieben und schule geht vor.

Ich hab nun eine Klasse, in der ich zurzeit nur zwei funktionen nutze. (Da fehlen noch einige)

Die Datanbank habe ich geändert, sodass jeder Schüler eine ID besitzt. Leider hab ich immer noch keine Ahnung wie ich sublime einstelle, dass er wirklich nur mit 4 Leerzeilen schreibt. (Hab die Einstellung gefunden steht aber 4 drin. Ich denke das Problem ist: Fals man Eingerückt ist und dann Enter drückt, so übernimmt der Cursor die Einrückung in der neuen Zeile... fals jemand helfen könnte währe das ganz nett)

@Sirus

Ich denke ich habe alles umgesetzt in meinem Code und hoffe, dass der Code nun besser ist als zuvor. :)

@BlackJack

SQLAlchemie:
Ich kann mir gut vorstellen, dass das Modul besser ist als das was ich nutze. Allerdings bin ich damit voll zufrieden. Ich möchte ja SQL Befehle selber schreiben. (Größerer Lerneffekt)

Deinem Code:
Erstmal ein dickes danke für die große mühe :)
Ich bin aber wirklich noch ein anfänger in Python und wenn ich mir den Code anschaue ... Da kommen einiges an Fragen auf ...
Ich weiß deine bemühungen sehr zu schätzen aber es bringt leider wenig, wenn ich nur die hälfte verstehe :oops:

zu Ad 1:
ok ich glaub das habe ich verstanden und auch umgesetzt :)

zu Ad 2:
Weil ich das ganze Programm in eine GUI einpacken möchte. Und dafür benötige ich grundwissen in OOP, welches ich mir durch diesen Code und eurer hilfe aneignen möchte, sowar mein Verstand es zulässt.

zu Ad 3:
Meine Hauptklasse sind die Schüler, bzw der Schüler oder Schülerinn. Später sollen diese mit einer DropDownListe alle Angezeigt werden und per auswahl, die schulden angezeigt werden. Dann kann man Etwas eintragen und auslesen.

Ich weiß sich alles aus dem Gedächniss zu kramen ist nicht gut, vorallem weil man nach einer stunde alles wieder ändern möchte :D
Aber ich mach lieber kleine Stücke, als wenn der Code später nicht funktioniert und ich frustriert aufgebe...

zu Ad 4:
Ich hab mich versucht direkt eine GUI (via OOP) zu schreiben und es zu verstehen... Da musste ich dann frustriert aufgeben. Wie du selber schon sagst, da fehlen einfach Grundkenntnisse. Nur dann will man sie lernen und z.B. das Video ist an sich auch daneben. Woher soll ich wissen was nun gut ist und was nicht :oops:

zu Ad 5:
Ich habe einen kleinen Raspberry Pi und darauf läuft bereits ein MySql server. Das Schulbuch ist auch auf Mysql ausgelegt und ich weiß ehrlich gesagt nicht ob PostgreSQL andere Syntax hat. Somit müsste ich dann immer googlen wenn mir befehle und die Syntax nicht einfallen. da ist mir das Buch schon lieber. Vorallem kann ich mein Lehrer um Rat fragen und er möchte die Datenbank mitverwenden. (Die Datenbank wird in den nächsten Monaten als Projekt für php angekündigt. Da sollen wir dann auch schüler mit schulden eintragen etc.) Demnach muss die Datenbank für Php, Python, C# zugänglich sein.

Ich glaub ich hab grade ein Geistiegen Höhepunkt erreicht (Gleich mal die Uhrzeit aufschreiben). Dein Code ist eigentlich relativ simple. :rooleyes: Hab mir den grade nochmal genau angeschaut. Ich teste morgen deinen Code und geh den nochmal in meinem Kopf durch.

Verständniss:
der Code in Klassenmethoden wird nur innerhalb der Klasse ausgeführt oder? Dann ist das "cls" in Zeile 42 in dem Sinne falsch. Ich glaub das sollte ich auch so nicht machen, aber wie beende ich es sonst?

Hier der bessere und funktionierende Code mit speicher und auslese methoden:

Code: Alles auswählen

import pymysql
import subprocess

class Dbms():
	def __init__(self):

		try:
			self.dbms = pymysql.connect(host="192.168.2.100",
								user="bxt_system",
								passwd="***",
								db="bxt")

		except Exception as e:
			raise (e)

	def get_name(self, eingabe_vorname, eingabe_name):

		name = "%%%s%%" %(eingabe_name)
		vorname = "%%%s%%" %(eingabe_vorname)

		cur = self.dbms.cursor(pymysql.cursors.DictCursor)
		cur.execute("SELECT * FROM schueler WHERE name LIKE %s AND vorname LIKE %s", (name, vorname)) 
		result = cur.fetchall()

		return result

	def get_schulden(self, user):

		cur = self.dbms.cursor(pymysql.cursors.DictCursor)
		cur.execute("SELECT betrag, date, bezahlt, comment FROM schulden WHERE schueler_id = %s", (user[0]["schueler_id"]))
		result = cur.fetchall()

		return result

	def insert(self, user, betrag, comment):

		cur = self.dbms.cursor(pymysql.cursors.DictCursor)
		cur.execute("INSERT schulden VALUES (DEFAULT, %s, %s, %s, DEFAULT, CURDATE())", (user[0]["schueler_id"], betrag, comment))	
		result = cur.fetchall()
		self.dbms.commit()

	def End(cls):
		cls.dbms.rollback()
		cls.dbms.close()

def main():
	while True:
		subprocess.call("cls", shell=True)
		try:
			name = input("Name: ")
			vorname = input("Vorname: ")

			session = Dbms()

			user = session.get_name(vorname, name)

			print("\nErgebniss der Suchanfrage: \n")
			for i in user:
				print("Vorname = %s \t \t Name = %s" % (i["vorname"],i["name"]))

			print("\n")	
			if len(user) == 1:
				while True:
					print("Es wurde ein Schüler ausgewählt. \n")
					print("Was möchten Sie tun:")
					print("\t \t 1 = Schulden Ausgeben lassen.")
					print("\t \t 2 = Schulden Eintragen.")
					print()
					auswahl = input("Geben Sie hier die Nummer ein: ")

					if auswahl == "1":
						schulden = session.get_schulden(user) # Ist es besser das Gesamte Dic der Klasse zu übergeben?
						for i in schulden:
							print(i)

					elif auswahl == "2":
						subprocess.call("cls", shell=True)

						while True:
							betrag = input("Geben sie den Betrag ein (Hinweiß Verwendet Sie ein Punkt anstatt ein Komma): ")
							try:
								float(betrag)
							except:
								print("Das war keine Gültige Eingabe. Versuchen Sie es erneut.")
								continue
							break

						comment = input("Fügen Sie ein Kommentar hinzu: ")
						session.insert(user, betrag, comment)
						break
					else:
						print("Fehler in der Auswahl!")
						continue



			if len(user) == 0:
				print("Kein Schüler gefunden! Versuche es erneut.")
				input("Weiter mit Enter")
				continue

			if len(user) > 1:
				print("Mehrer Schüler wurden gefunden! Versuche es erneut.")
				input("Weiter mit Enter")
				continue



		except KeyboardInterrupt:
			session.End()
			print("\n KeyboardInterrupt")
			break
		print("\n")
		input("Drücken Sie Enter um einen Neuen Schüler Auszuwählen.")

if __name__ == "__main__":
	main()
Vielen Dank für eurer Mühen und ich hoffe man kann schon erfolg sehen. (Versuch ein Lob aus euch zu erbetteln :D)

EDIT: In diesem Editor sind auch die Tabulatoren drinne. Kann das sein, das im Forum ein Tabulator 8 Leerzeichen besitzt? Ich beschäftige mich morgen mit dem Editor und versuch diesen Code noch vernünftig anzupassen.

MfG
Trayser
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

@TrayserCassa: die schlimmsten Fehler sind schonmal draußen. Es gibt aber noch viel zu tun. Ich geh wieder von oben nach unten:
Wenn Du eine Exception fängst, nur um sie gleich wieder zu werfen, kannst Du das try-except auch gleich weglassen. Die Verbindungsparameter will man eigentlich nicht mitten im Code haben, sondern zumindest als Konstanten am Anfang der Datei definiert. get_name ist ein schlechter Nane, weil man den Namen ja als Parameter übergibt. Was wird also da ge«get»tet? Durch Dein zwangs-LIKE wird man nie "Hans Maier" auswählen können, wenn es auch einen "Hans-Werner Maierhof" gibt. Also bei der Auswahl mußt Du noch dringend etwas ändern. get_schulden erwartet eine Liste von Usern von der aber nur das erste Element benutzt wird? Ist das Dein Ernst?! "execute" erwartet als zweiten Parameter eine Liste, würde mich wundern, wenn das so funktioniert. Bei INSERT nehme ich immer die Variante, wo die Feldnamen explizit angegeben werden. Noch fehleranfälliger als der "SELECT *"-Unsinn. Bei INSERT gibt es nichts zu fetchen. End ist keine Klassenmethode, daher ist cls als erser Parameter falsch! main ist viel zu lang. Das mußt Du in etliche Funktionen aufspalten. Wer mir den Bildschirm zwangsweise löscht, dem geschieht von mir dasselbe. user ist eine Liste und sollte deshalb users heißen. i ist ein schlechter Variablenname.
BlackJack

@TrayserCassa: Die Einstellungen zum Einrücken bestehen in der Regel aus zwei Teilen: 1. Wie weit soll eingerückt werden und 2. Womit soll eingerückt werden. Bei Sublime Test ist das Beispielsweise unter dem Menü `View → Indentation` das Kästchen `Indent using Spaces` was angekreuzt sein müsste und dann `Tab Width: 4`.

Standardeinstellung ist das der Editor beim Laden von Dateien versucht herauszufinden wie die Einrückung dort jeweils gelöst ist. Du müsstest bei Python-Quelltext der Tabs zu Einrücken verwendet also diese Tabs mal durch 4 Leerzeichen ersetzen, dann wird das auch in Zukunft in der Datei vom Editor so erkannt. Das Umwandeln geht auch über das Menü `View → Indentation`.

Ad 3: Kleine Stücke zu Implementieren und zu testen ist ja eine gute Idee, aber wie gesagt, ein grobes Bild von den Klassen die man braucht und welche Daten und Eigenschaften die haben sollte man vorher schon mal machen. Sonst ist man als Anfänger hinterher viel mit verschieben von Methoden und Funkionalität zwischen Klassen beschäftigt.

Ad 5: Die SQL-Syntax sollte eigentlich, beziehungsweise ist bei allen SQL-Datenbanken gleich weil die standardisiert ist. Allerdings gibt es manchmal bei der Auslegung des Standards unterschiedliche Meinungen fast jede Datenbank bringt ihre eigenen Erweiterungen in Form von Datentypen, Funktionen, und zusätzlichen Argumenten für die Standardkonstrukte mit. Wenn man die DB austauschbar halten möchte muss man also ein bisschen aufpassen was man von den Möglichkeiten des jeweiligen DBMS verwendet. Und gegebenenfalls ein wenig Code schreiben der Unterschiede zwischen den DBMS ausgleicht. Weshalb ich SQLAlchemy mag, denn das hilft dabei.

Der Code von Klassenmethoden wird wie jede andere Funktion oder Methode ausgeführt. Der bekommt halt die Klasse als erstes Argument übergeben und wird überlicherweise über das Klassen-Objekt aufgerufen und nicht wie normale Methoden über ein Exemplar das mit der Klasse erzeugt wurde. Dein `End()` ist aber auch gar keine Klassenmethode denn dazu müsste man `classmethod()` darauf anwenden, also ist der Argumentname `cls` statt `self` ”falsch”. Klassenmethoden werden sehr häufig als Alternativen zum erstellen von Exemplaren verwendet. In die `__init__()` steckt man normalerweise den allgemeinsten und einfachsten Weg ein Exemplar zu erzeugen, wo man zum Beispiel die Daten die in der Klasse gekapselt werden sollen direkt als Argumente übergibt. Eine Klassenmethode kann man dann beispielsweise schreiben wenn es zu der Klasse ein Dateiformat gibt aus dem man die Daten laden kann. Dann kann man der Klasse beispielsweise eine `load()`-Klassenmethode verpassen die einen Dateinamen als Argument bekommt, die Daten lädt und dann ein Objekt daraus erstellt in dem das erste Argument (`cls`) mit diesen Daten als Argumente aufgerufen wird.

Apropos ``cls``: Wie Sirius3 schon andeutete kann man mit dem Löschen des Terminalinhalts sehr leicht den Hass von Benutzern auf sich ziehen. Da könnte etwas wichtiges gestanden haben von einem vorherigen Programmaufruf. Ausserdem ist ``cls`` nicht plattformunabhängig, auf Deinem Raspi wird das nicht funktionieren.

In Deinem Programm wird jetzt für jeden Durchlauf der Hauptschleife eine neue Verbindung zur Datenbank aufgebaut.

Mit ``continue`` würde ich sparsam umgehen und das Vermeiden wo es nur geht, und das geht fast immer. Das ist eine unbedingte Sprunganweisung die sich nicht in der Einrückung des Quelltextes wiederspiegelt, ist also nicht so leicht zu durchschauen. Wenn man so viele Schleifen verschachtelt muss man auch erst einmal schauen auf welche sich das jeweilge ``continue`` bezieht und welche Auswirkungen das damit auf den Programmfluss hat. Ausserdem lässt sich Code der ``continue`` benutzt schlecht in separate Funktionen heraus ziehen ohne ich umschreiben zu müssen. Ein ``continue`` ist in Deinem Code überflüssig und die anderen lassen sich leicht vermeiden.
TrayserCassa
User
Beiträge: 97
Registriert: Donnerstag 2. Mai 2013, 19:11

So ich habe mich dann mal ein paar tage hingesetzt und möchte euch erstmal zeigen wie weit ich heute gekommen bin. Der Code ist nicht ganz fertig, aber ich denke da sind noch ein paar fehler drin. (Der Code funktioniert fürs erste). Ich denke mal das sternchen import ist nicht pep-8 ^^

Kurze Frage nebenbei: Wie kann ich einen String in meinem Code unterbrechen? Mit """zeichen """ funtioniert es nicht, da er dann die einrückung als Leerzeilen mitschreibt. :?

Fals ihr beiden (Sirius und Black) das Programm testen möchtet, kann ich euch Benutzernamen und Passwort geben. Der Mysql server ist von "Außen" erreichbar.

Edit :
Die Methoden Count_tickets werde ich noch schnell zusammenfügen und dann schlafen gehen.. Ich lass den Code aber hier erstmal unverändert.

Code: Alles auswählen

import pymysql
from tkinter import *
from tkinter import messagebox as msg


class MainUI():
    """doc"""
    def __init__(self, master):
        self.master = master

        master.title("Klassenkasse")
        master.resizable(width=0, height=0)

        ### Frames werden gesetzt ###
        topframeleft = Frame(master, bd=2, relief="groove")
        topframeleft.grid(row=0, column=0)

        topframeright = Frame(master, bd=2, relief="groove")
        topframeright.grid(row=0, column=1)

        bottomframeleft = Frame(master, bd=2, relief="groove")
        bottomframeleft.grid(row=1, column=0, columnspan=2)

        ### TopFrameLeft ###
        user_label = Label(topframeleft, text="Benutzername")
        user_label.grid(row=0, column=0, pady=5, padx=5, sticky=E)

        passwd_label = Label(topframeleft, text="Passwort")
        passwd_label.grid(row=1, column=0, pady=5, padx=5, sticky=E)

        self.entry_login = Entry(topframeleft)
        self.entry_login.grid(row=0, column=1, pady=5, padx=5)

        self.entry_passwd = Entry(topframeleft, show="*")
        self.entry_passwd.grid(row=1, column=1, pady=5, padx=5)

        button_login = Button(topframeleft, text="Login", command=self.login)
        button_login.grid(row=0, column=2, rowspan=2, pady=5, padx=5)

        #self.checkbox_safe = IntVar()
        #checkbox_login = Checkbutton(topframeleft, text="Benutzername und Passwort speichern?",
        #    variable=self.checkbox_safe)
        #checkbox_login.grid(row=2, column=1)

        ### Topframeright ###
        self.label_nochzuzahlen = Label(topframeright)
        self.label_nochzuzahlen.grid(row=1, column=0)

        self.label_bereitsgezahlt = Label(topframeright)
        self.label_bereitsgezahlt.grid(row=2, column=0, columnspan=2)

        self.label_summe = Label(topframeright)
        self.label_summe.grid(row=3, column=0)

        ### BottomFrameLeft ###

        button_insert = Button(bottomframeleft, text="Eintragen", command=self.insert)
        button_insert.grid(row=0, column=0, pady=5, padx=5)

        button_send = Button(bottomframeleft, text="Sende Erinnerung", command=self.send)
        button_send.grid(row=0, column=1, pady=5, padx=5)

        button_bezahlt = Button(bottomframeleft, text="Zahlung eingegangen", command=self.zahlung)
        button_bezahlt.grid(row=0, column=2, pady=5, padx=5)

        button_hilfe = Button(bottomframeleft, text="Hilfe", command=self.show_hilfe)
        button_hilfe.grid(row=0, column=3, pady=5, padx=5)

        ### Text Box ###
        self.text_box = Text(master, bg="#8A8A8A", width=60, height=20, bd=2)
        self.text_box.grid(row=2, column=0, columnspan=2, padx=2, pady=2, sticky=S)

        ### List Box ###
        self.listbox = Listbox(master, bg="#4945F0", height=30, width=30, bd=2, relief="raised")
        self.listbox.grid(row=0, column=2, rowspan=3, padx=2, pady=2, sticky=N)

        ### Status Bar ###
        self.label_status = Label(master, bg="#858585", width=95, bd=2, relief="raised")
        self.label_status.grid(column=0, row=3, columnspan=3)

    def login(self):
        """doc"""
        try:
            user_name = self.entry_login.get()
            passwd = self.entry_passwd.get()

            self.dbms = Dbms(pymysql, user_name, passwd)
            text = "Sie sind Eingeloggt"

            ### Listet die User auf und speichert sie in die Rechte listbox ###
            self.listbox.delete(0, END)
            result = self.dbms.list_name()
            for user in result:
                user = "{0}-{1}-{2}".format(user["schueler_id"], user["vorname"], user["name"])
                self.listbox.insert(END, user)
            self.listbox.bind("<Double-Button-1>", self.schulden)
            self.label_status.config(text="Wähle einen User aus der Liste aus.")

            self.label_bereitsgezahlt.config(text="Geschlossene Tickets : %s" %(self.dbms.count_tickets_payed()))
            self.label_nochzuzahlen.config(text="Offene Tickets : %s" %(self.dbms.count_tickets_notpayed()))
            self.label_summe.config(text="Aktuelle Summe : %s €" %(self.dbms.summe()))

        except Exception as ex:
            msg.showwarning("Fehler beim Einloggen", ex)
            text = "Sie sind nicht Eingeloggt!"
        self.label_status.config(text=text)

    def schulden(self, event):
        """doc"""
        self.text_box.delete("1.0", END)

        widget = event.widget
        selection = widget.curselection()
        user = widget.get(selection[0])

        self.label_status.config(text="Ein User wurde ausgewählt: %s" % (user))

        schueler = user.split("-")

        self.label_status.config(text=schueler)
        self.text_box.insert(END, "Vorname = {0} \nName = {1} \n \n".format(
            schueler[1],
            schueler[2]))

        result = self.dbms.list_schulden(schueler[0])
        
        if len(result) != 0:
            for row in result:
                self.text_box.insert(END, " Betrag = {0} € \n Datum = {1}".format(row["betrag"], row["date"]))
                self.text_box.insert(END, "\n Bezahlt = {0} \n Kommentar = {1} \n \n".format(row["bezahlt"], row["comment"]))

    def insert(self):
        """doc"""
        pass
        #try: Probe versuch listeneintrag zu bekommen.
        #except: Kein Login fehler meldung anzeigen lassen.

    def send(self):
        """doc"""
        pass

    def zahlung(self):
        """doc"""
        pass

    def show_hilfe(self):
        """doc"""
        pass

class Dbms():
    """docstring for Dbms"""
    def __init__(self, root, user, passwd):
        self.dbms = root.connect(
                host="192.168.2.100",
                user=user,
                passwd=passwd,
                db="bxt"
            )

    def list_name(self):
        """doc"""
        cur = self.dbms.cursor(pymysql.cursors.DictCursor)
        cur.execute("SELECT * FROM schueler")
        result = cur.fetchall()

        return result

    def list_schulden(self, user):
        """doc"""
        cur = self.dbms.cursor(pymysql.cursors.DictCursor)
        cur.execute("""SELECT betrag, date, bezahlt, comment FROM
             schulden WHERE schueler_id = %s""", (user))
        result = cur.fetchall()

        return result

    def summe(self):
        """doc"""
        cur = self.dbms.cursor(pymysql.cursors.DictCursor)
        cur.execute("SELECT SUM(betrag) betrag FROM schulden")
        result = cur.fetchall()
        return result[0]["betrag"]

    def count_tickets_payed(self):
        """doc"""
        cur = self.dbms.cursor(pymysql.cursors.DictCursor)
        cur.execute("SELECT COUNT(bezahlt) bezahlt FROM schulden WHERE bezahlt = \"Y\"")
        result = cur.fetchall()
        return result[0]["bezahlt"]
        print(result)


    def count_tickets_notpayed(self):
        """doc"""
        cur = self.dbms.cursor(pymysql.cursors.DictCursor)
        cur.execute("SELECT COUNT(bezahlt) bezahlt FROM schulden WHERE bezahlt = \"N\"")
        result = cur.fetchall()
        return result[0]["bezahlt"]


def main():
    """doc"""
    root = Tk()
    MainUI(root)
    root.mainloop()

if __name__ == '__main__':
    main()
MfG
Trayser
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

@TrayserCassa: für mich ist es unübersichtlich, sowohl Login-Daten als auch den Rest des Programms in einem Fenster zu haben. Login brauche ich nur am Anfang, der Rest ist ohne Login nicht benutzbar aber doch aktiv. Nicht benutzbar, weil self.dbms einen AttributError wirft; dieses Attribut wird erst in login erzeugt. Das zeigt schön, warum alle Attribute schon in __init__ erzeugt werden sollten, und nicht nachträglich irgendwo anders. Die korrekte Lösung wäre es, immer zu prüfen, ob man eingeloggt ist, oder nicht.
Die Exception in login ist zu allgemein und deckt einen viel zu großen Code-Block ab. Wenn Du nur prüfen willst, ob der Login geklappt hat, sollte auch nur dieser ExceptionTyp abgefangen werden und der try-Block nur die "self.dbms = ..."-Zeile enthalten.
In Dbms übergibst Du als root das pymysql-Modul. Das ist ungewöhnlich, aber theoretisch möglich, da alle DB-Api 2.0-kompatible Module austauschbar wären. Dann solltest Du das aber auch konsequent durchziehen, alle anderen Methoden benutzen nämlich pymysql direkt und zudem noch Klassen, die nicht zum Standard gehören. count_tickets_payed und count_tickets_notpayed machen eigentlich das selbe und lassen sich gut zu einer Funktion zusammenfassen. In Python sollte man die verschiedene Arten von Anführungszeichen verwenden um Escapes zu vermeiden. Zeile 204 funktionier auch nur, weil Du Methoden der Instanz an Ereignisse von tk-Objekten heftest, die außerhalb der Klasse verwaltet werden.
BlackJack

@TrayserCassa: Der Sternchen-Import ist in der Tat unschön.

Es gibt 15 Docstrings die allesamt komplett für die Tonne sind weil sie nichts dokumentieren.

In der `__init__()` einer Klasse ein übergebenes Containerwidget mit Widgets zu bestücken ist mindestens komisch. Das ist unflexibel. Normalerweise erzeugt man dieses Containerwidget selbst bzw. normalerweise ist die Klasse dieses Widget und der Aufrufer/Erzeuger kann dann entscheiden wie und wo er das dann in eine GUI einbaut.

Ein paar von den Methodennamen sind für meinen Geschmack zu kurz. `send()` beispielsweise könnte durchaus auch verraten *was* da gesendet wird. Denglisch wie bei `show_hilfe()` ist auch nicht schön. Insgesamt sollte man bei einer Sprache für die Bezeichner bleiben. Das gilt auch für Texte die dem Benutzer angezeigt werden. Das denglische „eingeloggt“ heisst auf Deutsch „angemeldet“.

Wie Sirius3 ja schon angemerkt hat macht es keinen Sinn das Datenbankmodul in der `Dbms.__init__()` als Argument zu übergeben, dann aber sonst `pymysql` direkt anzusprechen und Sachen zu verwenden die nicht Standard sind. Und wieso heisst das Argument `root`? `Dbms` ist für dieses spezielle Objekt auch ein zu generischer Name. Der Host und der Datenbankname wären bessere Kandidaten für Argumente bei dieser Klasse als das Datenbankmodul.

Die `Dbms.list_*()`-Methoden sind nicht gut benannt. Da wird nicht wirklich etwas gelistet (was bedeutet das überhaupt) und `list_name()` liefert nicht *einen* *Namen* sondern *alle* Schülerdatensätze.

Insbesondere in der `Dbms`-Klasse werden zu viele triviale Zwischenergebnisse ohne Grund an einen Namen gebunden. Die ganzen `result`\s kann man sich locker sparen ohne dass das einen Einfluss auf die Lesbarkeit oder Verständlichkeit hätte.

In `count_tickets_payed()` steht Code *nach* dem ``return`` was unsinnig ist weil der *niemals* ausgeführt wird.

Der Name `user` wird in der `login()`-Methode als Schleifenvariable verwendet und *in* der Schleife dann an einen anderen Wert gebunden.

Der Statustext wird in der `login()` nach dem befüllen der Benutzerlistbox auf 'Wähle einen User aus der Liste aus.' gesetzt, nur um gleich danach auf 'Sie sind angemeldet.' geändert zu werden. Texte die der Benutzer nie zu sehen bekommt braucht man auch nicht “anzeigen“. Ein ähnliches Vorgehen ist in `schulden()` zu finden.

`notpayed` ist kein englisches Wort. Die Methode hiesse besser `count_unpaid_tickets()`. Allerdings wäre eine `get_payment_statistics()` die Anzahl und Beträge von bezahlten und unbezahlten Tickets abfragt vielleicht besser als zwei Anfragen zu stellen die auf die gleichen Spaltenwerte filtert. An der Stelle sollte man vielleicht auch den Spaltentyp von `bezahlt` auf BOOLEAN ändern statt Buchstaben zu verwenden wo dazu auch noch in einer Deutsch benannten Spalte die Buchstaben für englische Ja/Nein-Antworten stehen.

Das mit den Daten in der Listbox ist äusserst unschön gelöst. Man steckt keine Daten in eine Zeichenkette um die dann da umständlich hinterher wieder heraus zu prokeln. Dabei gehen Typinformationen verloren. Falls das tatsächlich klappt die einen Schüler auszuwählen und dann dessen Daten von Datenbank abzufragen dann hast Du an zwei Stellen unwahrscheinliches Glück. 1. das die DB dann als ID wo (hoffentlich) eine Zahl erwartet wird auch eine Zeichenkette akzeptiert, und 2. das die ID in Deinen Tests einstellig war, denn wenn sie mehr als ein Zeichen hat wird sie so wie Du `execute()` damit aufrufst das als mehr als einen Wert ansehen.

Das mit den Listboxen löst man in der Regel so dass man die Daten zu der Zeichenkette in einem Wörterbuch hinterlegt.

Der Test ob das `result` eine andere Länge als 0 hat vor der Schleife ist überflüssig. Das Programm verhält sich ohne den Test identisch und man spart sich eine Einrückebene. Und den Namen `result` kann man sich dann auch sparen.

Die Methoden von `Dbms` folgen alle einem Muster: Cursor erstellen, abfrage absetzen, alle Ergebnisse holen. So etwas kann man prima in eine eigene Methode auslagern.

Mir fehlt insgesamt so ein bisschen eine getrennte Geschäftslogik. Es sieht so aus als würde das alles in der GUI abgewickelt werden.

Ein paar von den besprochenen Änderungen (ungetestet):

Code: Alles auswählen

# coding: utf8
from __future__ import absolute_import, division, print_function
from tkinter import *
from tkinter.messagebox import showwarning
 
import pymysql
 

class Dbms(object):

    def __init__(self, host, user, password, database):
        self.dbms = pymysql.connect(
            host=host, user=user, passwd=password, db=database
        )
 
    def _execute(self, sql, values=None):
        cursor = self.dbms.cursor(pymysql.cursors.DictCursor)
        cursor.execute(sql, values)
        return cursor.fetchall()

    def get_users(self):
        return self._execute('SELECT * FROM schueler')
 
    def get_open_items(self, user):
        return self._execute(
            'SELECT betrag, date, bezahlt, comment'
            ' FROM schulden'
            ' WHERE schueler_id = %s',
            (user,)
        )

    def get_payment_statistics(self):
        result = self._execute(
            'SELECT bezahlt, COUNT(bezahlt) bezahlt_count, SUM(betrag) total'
            ' FROM schulden'
            ' GROUP BY bezahlt'
            ' ORDER BY bezahlt'
        )
        return [(row['bezahlt_count'], row['total']) for row in result]
 
 
class MainUI(object):

    def __init__(self, master):
        self.master = master
        self.dbms = None
 
        master.title('Klassenkasse')
        master.resizable(width=0, height=0)
 
        ### Frames werden gesetzt ###
        topframeleft = Frame(master, bd=2, relief=GROOVE)
        topframeleft.grid(row=0, column=0)
 
        topframeright = Frame(master, bd=2, relief=GROOVE)
        topframeright.grid(row=0, column=1)
 
        bottomframeleft = Frame(master, bd=2, relief=GROOVE)
        bottomframeleft.grid(row=1, column=0, columnspan=2)
 
        ### TopFrameLeft ###
        user_label = Label(topframeleft, text='Benutzername')
        user_label.grid(row=0, column=0, pady=5, padx=5, sticky=E)
 
        passwd_label = Label(topframeleft, text='Passwort')
        passwd_label.grid(row=1, column=0, pady=5, padx=5, sticky=E)
 
        self.entry_login = Entry(topframeleft)
        self.entry_login.grid(row=0, column=1, pady=5, padx=5)
 
        self.entry_passwd = Entry(topframeleft, show='*')
        self.entry_passwd.grid(row=1, column=1, pady=5, padx=5)
 
        button_login = Button(topframeleft, text='Login', command=self.login)
        button_login.grid(row=0, column=2, rowspan=2, pady=5, padx=5)
 
        ### Topframeright ###
        self.label_nochzuzahlen = Label(topframeright)
        self.label_nochzuzahlen.grid(row=1, column=0)
 
        self.label_bereitsgezahlt = Label(topframeright)
        self.label_bereitsgezahlt.grid(row=2, column=0, columnspan=2)
 
        self.label_summe = Label(topframeright)
        self.label_summe.grid(row=3, column=0)
 
        ### BottomFrameLeft ###
        button_insert = Button(
            bottomframeleft, text='Eintragen', command=self.add_open_item
        )
        button_insert.grid(row=0, column=0, pady=5, padx=5)
 
        button_send = Button(
            bottomframeleft, text='Sende Erinnerung', command=self.send_reminder
        )
        button_send.grid(row=0, column=1, pady=5, padx=5)
 
        button_bezahlt = Button(
            bottomframeleft, text='Zahlung eingegangen', command=self.close_item
        )
        button_bezahlt.grid(row=0, column=2, pady=5, padx=5)
 
        button_hilfe = Button(
            bottomframeleft, text='Hilfe', command=self.show_help
        )
        button_hilfe.grid(row=0, column=3, pady=5, padx=5)
 
        self.text_box = Text(master, bg='#8A8A8A', width=60, height=20, bd=2)
        self.text_box.grid(
            row=2, column=0, columnspan=2, padx=2, pady=2, sticky=S
        )
 
        self.listbox = Listbox(
            master, bg='#4945F0', height=30, width=30, bd=2, relief=RAISED
        )
        self.listbox.grid(row=0, column=2, rowspan=3, padx=2, pady=2, sticky=N)
        self.users = dict()

        self.label_status = Label(
            master, bg='#858585', width=95, bd=2, relief=RAISED
        )
        self.label_status.grid(column=0, row=3, columnspan=3)
 
    def login(self):
        try:
            self.dbms = Dbms(
                '192.168.2.100',
                self.entry_login.get(),
                self.entry_passwd.get(),
                'bxt'
            )
 
            ### Listet die User auf und speichert sie in die Rechte listbox ###
            self.listbox.delete(0, END)
            self.users = dict()
            for user in self.dbms.get_users():
                user_string = (
                    '{0["schueler_id"]}-{0["vorname"]}-{0["name"]}'.format(user)
                )
                self.listbox.insert(END, user_string)
                self.users[user_string] = user
            self.listbox.bind('<Double-Button-1>', self.on_user_selected)
 
            (
                (unpaid_count, unpaid_total), (paid_count, paid_total)
            ) = self.dbms.get_payment_statistics()
            self.label_bereitsgezahlt['text'] = (
                'Geschlossene Tickets : {0}'.format(paid_count)
            )
            self.label_nochzuzahlen['text'] = (
                'Offene Tickets : {0}'.format(unpaid_count)
            )
            self.label_summe['text'] = (
                'Aktuelle Summe : {0:.2f} €'.format(unpaid_total + paid_total)
            )
        except Exception as error:
            showwarning('Fehler beim Einloggen', str(error))
            status_text = 'Sie sind nicht angemeldet!'
        else:
            status_text = 'Sie sind angemeldet.'

        self.label_status['text'] = status_text
 
    def on_user_selected(self, _event=None):
        user_key = self.listbox.get(self.listbox.curselection()[0])
        self.label_status['text'] = 'Ein User wurde ausgewählt: {0}'.format(
            user_key
        )
        user = self.users[user_key]
        self.text_box.delete('1.0', END)
        self.text_box.insert(
            END,
            'Vorname = {0["vorname"]}\nName = {0["name"]}\n\n'.format(user)
        )
        for row in self.dbms.get_open_items(user['schueler_id']):
            self.text_box.insert(
                END,
                ' Betrag = {0["betrag"]} €\n'
                ' Datum = {0["date"]}\n'
                ' Bezahlt = {1}\n'
                ' Kommentar = {0["comment"]}\n\n'.format(
                    row, 'J' if row['bezahlt'] else 'N'
                )
            )
 
    def add_open_item(self):
        pass
 
    def send_reminder(self):
        pass
 
    def close_item(self):
        pass
 
    def show_help(self):
        pass
 

def main():
    root = Tk()
    MainUI(root)
    root.mainloop()

 
if __name__ == '__main__':
    main()
TrayserCassa
User
Beiträge: 97
Registriert: Donnerstag 2. Mai 2013, 19:11

Erstmal wieder danke für die schnellen Antworten :)

@Sirius
Ich geb dir in allen punkten recht..

Der erste: Ich wollte erst ein Pop Up Fenster mit anmeldung machen. Das kam mir aber doch recht kompliziert vor.. Bzw weiß ich nicht ganz wie ich das gestalten könnte. Ich teste grade und gestalte eine neue Klasse als PoupWindow. Nur komm ich nicht ganz drauf wie ich der MainUI das Object vererbe.. Ich denke hier wird es schlauer draus:

Ich Rufe die Klasse MainUI auf und in der __init__ darin eine andere Klasse, die ein zweites Fenster öffnet, wo ein anmelde Formular erscheint. Dann versuche ich mich einzuloggen und übergebe die Variablen des Formulars. Fals ich mich erfolgreich einlogge, habe ich ja ein Objekt der Klasse Dbms. Wie übergebe ich das Objekt denn wieder zurück zur MainUI ?

@BlackJack

In der Datenbank (MySQL) gibt es kein Wert Boolean. Man könnte ein enum erstellen, allerdings habe ich drauf verzichtet und ein Char(1) wert benutzt.

Aber natürlich habe ich die Primärschlüssel geändert ;) Ist ja nicht so als würd ich alles Ignorieren von dir :P
Liste:
Die Einträge haben follgendes muster: schueler_id-schueler_vorname-schueler_name

Der User des Programms braucht die Infos Name und Vorname, während das Programm nur die schueler_id benötigt, deswegen split und die Informationen werden ja im Textwidget genutzt.

Und das Programm funktionierte einwandfrei auch mit "zweistelliger" schueler_id. Allerdings habe ich dein Programm versucht zu starten, da kommt ein Error, wo versucht wird in der Liste die Werte einzutragen. Bitte mach dir nicht immer die mühe ein ganzen Code zu schreiben ._. Von deinen Texten komm ich mir so erschlagen vor und deinen Code versteh ich auch nur 70%. Ich find es Super das du mir helfen willst und bin dir auch dankbar dafür, aber vielleicht sollten wir uns einfach auf die Aktuellen Probleme fokusieren. Und später, wenn ich sage "Fertig" und die Docstrings immer noch leer sind, dann darfst du mich schlagen :D

Meine Aktuellen Probleme:
1. Ich habe 3 Klassen (MainUI, Dbms, PopupWindow). Ich denke die Funktion kann man aus den Namen auslesen. In meiner Main-methode rufe ich die MainUI auf. In der MainUI.__init__ rufe ich die Klasse PopupWindow auf. Diese zeigt ein kleines Fenster mit einem Login Formular. Daraufhin wird versucht, durch die Dbms-klasse, sich auf den Server anzumelden. Fals das klappt soll sich das kleine Loginfenster schließen und der MainUI das Objekt von der Dbms-klasse übergeben. Geht das oder habe ich eine Falsche denkweise? Ich geh mal von meiner denkweise aus ^^

MfG
Trayser
BlackJack

@TrayserCassa: MySQL hat zwar keinen BOOLEAN Typ aber man kann das trotzdem als Spaltentyp setzen, ist ein Synonym für TINYINT(1), und es gibt die Konstanten TRUE und FALSE in MySQL die für 1 und 0 stehen und sämtliche Operationen die eigentlich ein boole'sches Ergebnis hätten oder einen Wahrheitswert als Operanden nehmen behandeln 0 als „falsch“ und jede andere Zahl als „wahr“. Und zufällig tut Python das auch. Also sollte man auch bei MySQL den Typ BOOLEAN verwenden wenn man eine Spalte mit Wahrheitswerten hat. Dafür ist der Typ/Alias schliesslich da. Dann kann man in Python auch die Python-Werte `True` und `False` als Werte verwenden, denn Python's `bool` ist eine Subklasse von `int` und die Werte `True` und `False` haben die Zahlenwerte 1 und 0. BOOLEAN ist auch ein SQL93-Standardtyp.

Und nochmal: Daten zu einem String zusammensetzen, den in der GUI anzeigen, und dann den GUI-Wert im Programm wieder zu Daten zu parsen ist ein No-Go. So etwas macht man nicht. Das ist nichts was man später mal anders macht sondern etwas das man gar nicht erst anfangen sollte.

Das kann mit Deinem gezeigten Quelltext nicht mit IDs funktionieren die mehr als ein Zeichen haben. Dann musst Du das mit einem anderem Quelltext gemacht haben. Denn `schueler` ist eine Liste mit Zeichenketten (durch `split()` entstanden), damit ist auch `schueler[0]` eine Zeichenkette und wenn die mehr als ein Zeichen enthält wird ein `cursor.execute()` bei dem das SQL-Argument nur einen Platzhalter enthält bei einer Zeichenkette mit mehr als einem Zeichen als zweites Argument eine entsprechende Ausnahme auslösen.

Mein Programm funktioniert tatsächlich nicht ganz, jetzt hätte ich ja nach der Ausnahme und dem Traceback fragen können, aber Deine Ausnahmebehandlung verhindert das ja. In den Platzhaltern für `format()` sind die " zu viel.

Welche 70% meinst Du? So viel habe ich ja nicht geändert, das ist ja überwiegend immer noch Dein Programm.
Antworten