Adressbuch

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Xfd7887a
User
Beiträge: 135
Registriert: Montag 23. Juni 2014, 17:11

Habe nach langer Zeit beschlossen, wieder mal etwas mit Python zu machen. Und zwar würde ich gerne ein Adressbuch mit GUI programmieren. Dazu ein paar Fragen:

1. Welches GUI-Framework sollte ich nehmen (für Mac OS X und Linux Mint)?
2. Ich dachte an die folgende Struktur:

Code: Alles auswählen

class Adresse(object):
    def __init__(self):
        self.name = ...
        self.vorname = ...

class AdressbuchDB(object):
    def __init__(self):
        self.adressen = adressen

    def eintrag_erstellen(neuer_kontakt)
        self.adressen[neuer_kontakt.nachname] = neuer_kontakt

class Controller(object):
    def __initi__(self):
        self.adressen = self.adressen_laden
        self.adressbuch = AdressbuchDB.new(@adressen)
...
Kann man das so machen? Ich habe leider keine Ahnung von GUIs, dass kann ich hoffentlich mit eurer Hilfe lernen.
3. Nehme ich mir da zu viel vor (keine Ahnung von GUI, wenig von OOP)? Bitte ehrlich sein :D

Danke
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Also Deine Struktur erschließt sich mir nicht! (Ob deine Adressen wirklich als Klasse modelliert sein müssen, weiß man noch nicht, aber was diese DB-Klasse und dieser Controller machen sollen, ist mir doch unklar und einiges erscheint mir da zu künstlich in ein Klassenkorsett geschnürt) Aber das kann man ja auch erst einmal bei Seite lassen ;-)

Also OOP ist schon die Grundlage für GUIs jeder Art; man kann das natürlich durchaus auch mit GUI-Applikationen firmieren, aber ehrlich gesagt ist es schon gut, wenn man die Grundlagen von OOP in Python drauf hat :-)

Unabhängig von Plattform-Belangen mochte ich immer das Qt-Rahmenwerk. Da ich es aus C++ Zeiten kannte, habe ich damit auch in Python früher einiges gemacht. (Zeitweise auch in Java :-) ) Qt hat i.A. einen guten Ruf bezüglich der Plattformunabhängigkeit - speziell zu Apple Krams kann ich nichts konkretes sagen, aber unter Linux kriegst Du so ziemlich jedes Binding für ein GUI-Toolkit zum Laufen.

Im Zweifel schauste Dir eben mal ein paar Tutorials an und guckst eben dadurch, welches Framework Dir besser gefällt. Ob Gtk oder Qt nimmt sich nicht viel :-)

Da man die GUI eh vom Rest des Codes (also bei Dir eben die Domänen-Objekte und Funktionalität, sowie zusätzlich ggf. eine Persistenzschicht) trennen sollte, kannst Du ja auch erst einmal mit dem Adressbuch an sich anfangen. Wenn Du das sauber hinbekommen hast, kannst Du dann ja anfangen, eine GUI zu designen, die darauf aufsetzt :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Xfd7887a
User
Beiträge: 135
Registriert: Montag 23. Juni 2014, 17:11

Ok, danke :D Werde ich bald anfangen. Welche Klassen würdest du vorschlagen? Ich habe sonst keinen Anhaltspunkt.
BlackJack

@Xfd7887a: Du musst Dir halt überlegen welche Einzeldaten Du hast, welche davon zusammen gehören, und welche Operationen darauf möglich sein sollen. Im einfachsten Fall kann man eine Adresse in einem Wörterbuch speichern und viele Adressen in einer Liste oder einem weiteren Wörterbuch zusammenfassen. Dann noch ein paar Funktionen mit denen die Operationen auf den Adressen definiert werden. Auf der anderen Seite kann man sich auch einen kleinen Klassenzoo mit 20+ Klassen für die Aufgabe heranzüchten. Ein vernünftiger Entwurf läge wahrscheinlich irgendwo zwischen diesen beiden Extremen. ;-)
_nohtyp_
User
Beiträge: 127
Registriert: Mittwoch 8. Januar 2014, 15:38

Habe jetzt die folgende Klasse geschrieben:

Code: Alles auswählen

class Adresse(object):

    def __init__(self, name, vorname, telefon_nr):
        self.name = name
        self.vorname = vorname
        self.telefon_nr = float(telefon_nr)

hans = Adresse("Müller", "Hans", "012523746343")
erik = Adresse("Bush", "Erik", "0124334214312")
print(hans.name)
print(erik.name)
Welche Klassen muss ich jetzt noch schreiben, um das ganze zu verwalten? Könntet ihr mir da ein Ansatz geben?
BlackJack

@_nohtyp_: Telefonnummern sind keine Zahlen, und ganz bestimmt keine Gleitkommazahlen. Deine Beispieltelefonnummern funktionieren ja noch nicht einmal, das hätte Dir auffallen können.
_nohtyp_
User
Beiträge: 127
Registriert: Mittwoch 8. Januar 2014, 15:38

Also :

Code: Alles auswählen

self.telefon_nr = telefon_nr
Die Nummern müssen ja nicht funktionieren, sind ja nur ein Beispiel.
BlackJack

@_nohtyp_: Mit funkionieren nicht meinte ich nicht dass das keine tatsächlich vorhandenen Nummern sind, das habe ich mir schon gedacht, sondern dass der Code diese Nummern ”kaputt” macht.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

_nohtyp_ hat geschrieben: Welche Klassen muss ich jetzt noch schreiben, um das ganze zu verwalten? Könntet ihr mir da ein Ansatz geben?
Ok, also: Keine‽ :twisted:

Spaß bei Seite... man kann das sicherlich komplett *ohne* Klassen schreiben. Das bisherige könnte man locker in ein Dictionary (oder vielleicht auch ein ``collections.namedtuple``) packen, ohne irgend etwas an Funktionalität einzubüßen. Klassen sind kein Selbstzweck! - nicht wie z.B. in Java oder C#, wo man nicht ohne Klassen programmieren kann.

Ich könnte das auch so schreiben:

Code: Alles auswählen

def create_address(name, firstname, phone):
    return {"name: name, "firstname": firstname, "phone": phone}

hans = create_address("Müller", "Hans", "0123456789")
print(hans)
Vorteil des klassenlosen Ansatzes ist es, dass Du den viel einfacher speichern kannst! Nimm z.B. das ``json``-Modul und serialisiere den Datensatz:

Code: Alles auswählen

import json

with open("addressbook.json", "w") as f:
    json.dump(hans, f)

# und später kannst Du es wieder laden:
with open("addressbook.json") as f:
    hans_neu = json.load(f)

print(hans_neu)
Hier habe ich *keine* Klasse benötigt! Man müsste nun noch das Laden und Speichern in Funktionen verpacken und schon hat man die Persistenz erledigt.

Ich kann natürlich auch eine Klasse in JSON serialisieren, aber dazu müsste ich erst definieren, *wie* ich deren Attribute in JSON ausdrücken will. An dieser Stelle ist der klassenlose Ansatz also einfacher.

Naja, außerdem ist ein Dictionary nur für *einen* Datensatz gut; da Du ja sicher beliebig viele verwalten willst, wäre eine umschließende Liste eine Idee, oder noch ein Dictionary, welches einen speziellen Key hat, um Daten später besser zu finden.

Die Frage ist ja, welche Operationen willst Du mit dem Adressbuch durchführen! Das muss Dir klar sein und dann kannst Du damit loslegen und prüfen, welche Datenstruktur wirklich sinnvoll ist.

Aber wie Du siehst braucht man für vieles eben gar keine Klassen - und schon gar nicht kann man einfach erfragen, welche nun denn faktisch fehlen ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
_nohtyp_
User
Beiträge: 127
Registriert: Mittwoch 8. Januar 2014, 15:38

Aber muss ich, sobald ich eine GUI benutze, nicht sowieso mit Klassen arbeiten?
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

_nohtyp_ hat geschrieben:Aber muss ich, sobald ich eine GUI benutze, nicht sowieso mit Klassen arbeiten?
Unter Python schon, weil da Klassen natürlich in das Konzept einer GUI hinein passen. Aber was hat denn die GUI mit der Domänenlogik eines Adressbuches zu tun? ;-)

Ich hatte gerade gestern (oder sogar heute?) jemanden auch diesen Thread ans Herz gelegt. Ich erinnerte mich daran, dass wir dort jemandem versucht haben zu vermitteln, was Trennung zwischen GUI- und Domänenebene bedeutet. (Weiter hinten wird 's iirc unschön - aber die ersten drei vier Seiten sind die relevanten)

Der BMI-Rechner dort hat Funktionalität (== Domänenebene) als einfache Funktion und eine GUI, die auf einer Klasse basiert. Beides beißt sich überhaupt nicht :-) Und die Berechnung (also die eigentliche Kern-Funktionalität) ist vollkommen unabhängig von der GUI. Man könnte dazu auch ein Webinterface schreiben, welches diese Funktion für einen Online-BMI-Rechner nutzt!

Nicht falsch verstehen: Evtl. sind Klassen bei Deiner Anwendung ein gutes Mittel - aber das hängt eben davon ab, *was* man modellieren will. Da gibt es leider kein Patentrezept! Das bisher gezeigte kann man in einer Klasse lassen, aber was nun noch kommen soll, darüber kann man so keine Aussage treffen.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Xfd7887a
User
Beiträge: 135
Registriert: Montag 23. Juni 2014, 17:11

Ok, danke für die ausführliche Antwort. Habe den Thread gelesen und glaube, dass ich es verstanden habe :D Hier mein Code:

Code: Alles auswählen

import json


def adressbuch_laden():
    try:
        with open("addressbook.json") as f:
            return json.load(f)
    except FileNotFoundError:
        print("Datei nicht gefunden. Neue Datei wird erstellt.")
        return []


def adressbuch_speichern(adressen):
    with open("addressbook.json", "w") as f:
        json.dump(adressen, f)


def neue_adresse_erstellen(name, vorname, telefon_nr):
    return {"Name": name, "Vorname": vorname, "Telefonnr.": telefon_nr}


def adresse_aendern():
    pass


def adresse_loeschen():
    pass


def adressen_anzeigen(adressen):
    for adresse in adressen:
        print(adresse)


def main():
    adressen = adressbuch_laden()

    adressen.append(neue_adresse_erstellen("Müller", "Hans", "12345678"))
    adressen.append(neue_adresse_erstellen("Kins", "Erik", "87654321"))
    adressen_anzeigen(adressen)

    adressbuch_speichern(adressen)

if __name__ == "__main__":
    main()
Wie ich die beiden leeren Funktionen fülle, weiß ich noch nicht. Aber insgesamt wären das erstmal alle Funktionen, die mein Programm bringen soll (nicht sehr viel, ich weiß :) ).
Benutzeravatar
ngulam
User
Beiträge: 35
Registriert: Freitag 18. Oktober 2013, 11:03

Und wo ist in diesem Entwurf (eines Telefonverzeichnisses) nun eine Adresse? :K
งูหลาม
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Das sieht insgesamt schon viel besser aus!

Einige Anmerkungen hab ich dann doch noch:

- Ich würde mir überlegen, ob ich wirklich überall das Wort "adresse" in allen Funktionen haben wollen würde. Dafür gibt es ja Module; wenn Du Deiner ``address`` (oder ``adressen`` auf deutsch) nennst, ist ja klar, was ``adressen.laden`` wohl macht. Ansonsten wäre es ja ``adressen.adressbuch_laden`` ;-)

- Ich würde niemals eine Ausgabe in eine Funktion packen, die eine andere Aufgabe hat, als das Ausgeben! Also nimm das ``print``in Zeile 9 besser raus. Du kannst an der aufrufenden Stelle leicht ermitteln, ob die Liste leer ist. Wenn ja, kannst Du dann immer noch einen Text ausgeben, dass jetzt ein neues Adressbuch erstellt wird.

- Dir fehlt noch eine Funktion, um Adressen dem "Adressbuch" (also der Liste) hinzuzufügen. Da Du nicht wissen kannst, ob es sich dabei für immer um eine Liste handelt, solltest Du das in einer Funktion kapseln.

- Zudem wäre eine Funktion nett, die das ganze *formatiert* ausgibt. Deine ``adresse_anzeigen``-Funktion ruft in der Schleife ja nur ein schnödes ``print`` auf. An der Stelle solltest Du eine Funktion aufrufen, die das ganze hübsch formatiert ausgibt. (Diese und die aufrufende sind dann übrigen schon schöne Beispiele dafür, wie Du "UI" und Logiken trennst! Denn das Ausgeben in einer Text-Shell ist ja auch eine Art Benutzerinterface. Wenn Du später eine GUI darüber aufbaust, wirst Du sicherlich andere Funktionen bauen, die die Adresse in GUI-Elementen darstellen kann)

- Ich würde Dir das ``argparse``-Modul ans Herz legen, um den Namen der Adressbuchdatei zu Beginn übergeben zu können. Damit wäre man nicht auf *eine* Datei festgelegt.

- Das Suchen von Datensätzen fehlt übrigens auch noch...

Da ist also noch eine Menge Platz für Erweiterungen :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Xfd7887a
User
Beiträge: 135
Registriert: Montag 23. Juni 2014, 17:11

So, habe ein wenig weitergemacht:

Code: Alles auswählen

import json
from sys import argv


script, datei_name = argv

def laden():
    try:
        with open("{}.json".format(datei_name)) as f:
            return json.load(f)
    except FileNotFoundError:
        return []

adressen = laden()


def speichern():
    with open("{}.json".format(datei_name), "w") as f:
        json.dump(adressen, f)


def neue_erstellen(name, vorname, telefon_nr):
    return {"Name": name, "Vorname": vorname, "Telefonnr.": telefon_nr}


def hinzufuegen(name, vorname, telefon_nr):
    adressen.append(neue_erstellen(name, vorname, telefon_nr))


def aendern(gesucht):
    """UNVOLLSTAENDIG"""
    suchen(gesucht)


def loeschen(gesucht):
    """FEHLER"""
    zu_loeschen = suchen(gesucht)
    for adresse in range(len(adressen)-1):
        if zu_loeschen == adressen[adresse]:
            del adressen[adresse]


def suchen(gesucht):
    for adresse in adressen:
        for kontakt_info in adresse:
            if gesucht == adresse[kontakt_info]:
                return adresse


def ausgabe_formatieren():
    for adresse in adressen:
        for kontakt_info in adresse:
            print("{}: {}".format(kontakt_info, adresse[kontakt_info]))
        print()


def anzeigen():
    ausgabe_formatieren()


def main():

    hinzufuegen("Kins", "Erik", "123456789")
    hinzufuegen("Kuns", "Erik", "123456789")
    #hinzufuegen("Müller", "Erik", "123456789")

    anzeigen()

    #print(suchen("Kins"))
    loeschen("Kins")
    loeschen("Kuns")

    anzeigen()
    speichern()


if __name__ == "__main__":
    main()

Leider habe ich noch ein Problem mit der Löschen- und Ändern-Funktion.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Hm... also da ist jetzt leider wieder einiges nicht so gelungen!

- Die Zeilen 2-5 sind unschön! Kommandozeilen Parsing gehört in eine ``main``-Funktion und vor allem *weg* von Modulebene! Und nutze dafür doch ``argparse``.

- In Zeile 14 handelst Du Dir vollkommen unnötiger Weise eine *globale Variable* ein. Wieso das? Wieso holst Du dir die Adressen nicht in Deiner ``main__-Funktion, also ab Zeile 62 und übergibst diese dann an die Funktionen, die eine Sammlung von Adressen benötigen?

- ``anzeigen`` ist komplett unnötig! Vor allem aber hast Du die Intention nicht verstanden, die ich beim Umbau und dem Vorschlag eine formatierte Ausgabe für *eine* Adresse zu schaffen. In ``ausgabe_formatieren`` behandelst Du jetzt *alle* Adressen, ok. Aber wie bekomme ich denn jetzt *eine* Adresse schön formatiert ausgegeben? Das wäre nur über umständliche Wege zu erreichen! Denk mal drüber nach, wie man das auflösen kann. (Ist wirklich einfach!)

- Die Schlüssel des Dictionary nun ``kontakt_info`` zu nennen, ist wenig hilfreich beim Verständnis des Codes. Ich würde bei ``schluessel`` bleiben (wobei ich englische Namen nutzen würde).

- Du solltest Dir das API von Dictionaries noch einmal angucken: dict Darin findest Du Methoden, die Dich z.B. durch jeweils ein Schlüssel-Wert-Paar laufen lassen, was Dir bei der Ausgabe den Code einfacher werden lässt.

- Wieso interessieren Dich beim Suchen die Schlüssel? Es reicht dabei doch, sich die *Werte* aus einem Adresseintrag heraus zu suchen und bei denen zu testen, ob ein Eintrag dabei ist oder nicht! Im übrigen würde ich statt des ``==``-Operators den ``in``-Operator verwenden; dieser prüft unscharf, d.h. bei der Suche nach ``Chris`` würden sowohl Einträge mit ``Christian`` als auch mit ``Christoph`` gefunden werden. Du könntest auch noch ``str.lower`` nutzen, um Groß- und Kleinschreibung zu ignorieren.

- Das Suchen gibt nur *eine*, nämlich die zuerst gefundene, Adresse zurück - wieso das? Will man da nicht eine *Sammlung* von Adressdaten haben, bei den das Suchmuster gepasst hat?

- Zeile 38 ist ein *Anti-Pattern* in Python! Abgesehen davon, dass die Laufvariable ``adresse`` etwas vollkommen falsches vermittelt (man denkt es handele sich um eine Adresse, in Wahrheit ist es ein *Index*!), wieso zum Geier iterierst Du nicht einfach *direkt* über die Liste von Adressen? Ist doch wesentlich einfacher!

- Beim Löschen würde ich mit einem Schlüssel arbeiten, d.h. der Benutzer muss einen *eindeutigen* Identifier angeben, der einen Datensatz wirklich *eindeutig* bestimmt. Deine Suche - und auch mein Vorschlag zur Änderung der Suche - kann ja alles mögliche zurück liefern und schwupps hast Du Daten gelöscht, die nicht gewollt waren. Der Index eines Eintrags ist leider keine gute Option, da sich dieser nach jeder Lösch-Operation für alle nachfolgenden Elemente ändert. (Außer man würde von hinten nach vorne löschen... aber das erscheint mir ein wenig zu "magic"). An dieser Stelle solltest Du über einen eindeutigen Schlüssel für die Einträge nachdenken. Wie man diesen erzeugt oder definiert ist nun eine Frage der "Kreativität". Man könnte auf jeden Fall anstatt der Liste für die Sammlung von Adressen ein Dictionary nutzen. Damit hast Du schon mal die Eindeutigkeit einer Schlüssel-Wert-Beziehung abgedeckt. Wie Du den Schlüssel wählst, ist dann wieder eine andere Frage. Man könnte einen md5 Hash aus Name und Vorname berechnen o.ä. Man könnte auch aus dem Anfangsbuchstaben eines Nachnamen und eines Vornamens Schlüssel bauen. Allerdings braucht man dann noch Zusatz-Werte bei gleichen Namen. Als einfachste Lösung kannst Du zunächst auf einfach mit einem numerischen Schlüssel arbeiten und bei der Neuanlage einfach den bisherigen Maximalwert + 1 als neuen Schlüssel nehmen.

- Evtl. magst Du Dir mal das Unittest-Modul von Python angucken; damit könntest Du Deine Basisfunktionen ganz einfach testen und wärst auch bei Änderungen in Funktionen (== Refactoring) sicher, dass Du nichts kaputt gemacht hast.

Edit: Ach ja: Nutze für die Entwicklung doch ein Versionskontrollsystem. Zum einen fängt der Code an, zu groß zu werden, um hier in jeden Beitrag eingefügt zu werden, zum anderen verlierst Du dann die "Angst" vor massiven Änderungen, weil Du im Zweifel einfach einen vorherigen Stand wieder abrufen kannst. Viele hier mögen Github, aber auch Bitbucket ist recht beliebt. Evtl. reicht Dir auch erst einmal ein Pastebin aus, um den Code hier zu präsentieren. Neben dem im Forum eingebauten (s. ganz oben in der Menüleiste des Forums!), böte sich sonst auch noch gist.github.com an - als (registrierter?) Benutzer kann man die Gists sogar verwalten, sprich neuere Versionen hochladen.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Xfd7887a
User
Beiträge: 135
Registriert: Montag 23. Juni 2014, 17:11

Ok, danke für deine Antwort. Werde morgen versuchen, es umzusetzen :)
Xfd7887a
User
Beiträge: 135
Registriert: Montag 23. Juni 2014, 17:11

Habe heute weiter gearbeitet, habe aber noch nicht alle Tipps umgesetzt:
https://github.com/toxinman/stuff/blob/ ... essbuch.py
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Nur mal auf die Schnelle:

Finde hier einmal doppelte Code-Passagen und eliminiere diese ;-)
(Tipp: Du hast jetzt eine Funktion für das Ausgeben *eines* Datensatzes!)

Code: Alles auswählen

def ausgabe(adressen):
    """Alle Adressen werden formatiert ausgegeben"""
    for adresse in adressen:
        for key in adresse:
            print("{}: {}".format(key, adresse[key]))
        print()


def kontakt_anzeigen(adressen, kontakt):
    """Eine Adresse wird formatiert ausgegeben"""
    kontakt = suchen(adressen, kontakt)
    for key in kontakt:
        print("{}: {}".format(key, kontakt[key]))
Wieso ist in der letzten Funktion der Aufruf für das Suchen enthalten? Das ist imho unschön, denn das Ausgeben setzt intuitiv voraus, dass man Daten zum Ausgeben schon *hat*. Das Suchen muss man ggf. zuvor *extern* durchführen.

Wieso heißen die nicht ähnlich? Also ``kontakte_anzeigen`` statt nur ``ausgeben``? Und wieso überhaupt "kontakt" anstelle von "adresse"? ;-) Am einfachsten wäre doch ``adressen_anzeigen`` und ``adresse_anzeigen``.

Edit: Noch kurz etwas gefunden in Zeile 56:

Code: Alles auswählen

 if gesucht.lower() in adresse.values():
Das hat sicherlich nicht den gewünschten Effekt! Denn hier bleibt die Groß-/Kleinschreibung der Werte unangetastet. Du musst diese ebenfalls wandeln, z.B. mittels einer List-Comprehension.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Xfd7887a
User
Beiträge: 135
Registriert: Montag 23. Juni 2014, 17:11

Danke :D Bist echt eine große Hilfe
Antworten