OOP Anfängerfrage

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.
BlackJack

@tom1968: Nach 'n' oder 'x' zu suchen ist unwahrscheinlich? Du kannst Dir also nicht vorstellen, das jemand eine Spalte hat die 'j' oder 'n' für Ja und Nein enthält? Oder '.' und 'x' für etwas Ähnliches? Und um Rechenzeit musst Du Dir echt keine Sorgen machen. Davon verbrätst Du eh schon genug. Ob eine Datensatz markiert ist oder nicht ist tabelleninterner Verwaltungskram. Der sollte in den Daten eigentlich sowieso nichts zu suchen haben. Das wäre etwas für eine Attribut zusätzlich zu den Daten in einem eigenen Datentyp für einzelne Datensätze. Die Wahl von 'x' und 'n' finde ich auch ein wenig komisch. 'j'/'n', 't'/'f', 'y'/'n' okay, aber 'x'/'n'?

Und beim `self.maxzeiger` verstehe ich Deine Argumentation nicht. Wozu brauchst Du den? Warum nicht einfach ``len(self.tabelle)``? Das sollte immer den gleichen Wert liefern wie `self.maxzeiger`. Ohne dass Du das noch einmal parallel verwalten musst. Listen kennen ihre eigene Länge. Wobei auch Ausnahmen hier nicht unbedingt schlecht sind, weil das ja tatsächlich nur in einer Ausnahmesituation zutrifft. Letztendlich ist aber diese ganze „Zeiger”-Idee unnötig umständlich.

Deine gesamte Methode könnte ungefähr so aussehen (ungetestet):

Code: Alles auswählen

    def suchen(self, wort, spalten_anfang=0, spalten_ende=-2, marker_spalte=-1):
        self.zeiger = None
        for i, datensatz in enumerate(self.tabelle):
            gefunden = any(
                wort in zelle for zelle in datensatz[spalten_anfang:spalten_ende]
            )
            datensatz[marker_spalte] = 'j' if gefunden else 'n'
            if gefunden and self.zeiger is None:
                self.zeiger = i
Dann hätte man natürlich immer noch dieses „Zeiger”-Konzept, was komisch ist.

Ist aber auch immer noch etwas unflexibel. Ich würde eine Methode zur Verfügung stellen, die ein aufrufbares Objekt entgegen nimmt, was mit jedem Datensatz aufgerufen wird und entscheidet ob der zum Ergebnis gehört oder nicht. Quasi eine Methode wie die `filter()`-Funktion.
tom1968
User
Beiträge: 38
Registriert: Sonntag 30. Juni 2013, 09:54

Danke BlackJack, habe Deinen Code nun mal in meine Klasse reinkopiert - und es funktioniert (ohne jede Änderung)
Und die Methode hat nun 6 statt wie vorher 22 Zeilen :o

Ich werde nun die anderen Methoden auch umbauen, denn zugegebenermaßen sieht das so wie von Dir vorgeschlagen sauberer aus und wird wohl auch leichter wartbar sein.

Den self.maxzeiger dachte ich mir deshalb aus weil ich den bei meinem Code relativ häufig brauche (wegen meiner häufigen Verwendung von "for ...range" - und ich vermutete dass das performancetechnisch schneller geht wenn hier nicht jedesmal mit len gerarbeitet wird ?!
Ist aber egal, dank Deines Beispiels mit enumerate versuche ich range soweit möglich zu ersetzen und brauche dann vermutlich weder den maxzeiger noch len, der maxzeiger fliegt also raus :D

.... Ich würde eine Methode zur Verfügung stellen, die ein aufrufbares Objekt entgegen nimmt, was mit jedem Datensatz aufgerufen wird und entscheidet ob der zum Ergebnis gehört oder nicht. Quasi eine Methode wie die `filter()`-Funktion.
Könntest Du mir das bitte mit einfachen Worten oder anhand eines kurzen Codebeispiels nochmal erklärten, ich verstehe nicht was Du meinst.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

tom1968 hat geschrieben:
.... Ich würde eine Methode zur Verfügung stellen, die ein aufrufbares Objekt entgegen nimmt, was mit jedem Datensatz aufgerufen wird und entscheidet ob der zum Ergebnis gehört oder nicht. Quasi eine Methode wie die `filter()`-Funktion.
Könntest Du mir das bitte mit einfachen Worten oder anhand eines kurzen Codebeispiels nochmal erklärten, ich verstehe nicht was Du meinst.
In ganz grundlegendem Python und einem Prädikat (Funktion die Wahrheitswerte ausgibt) das prüft ob das übergebene Element dem String ``"green"`` entspricht:

Code: Alles auswählen

def is_green(element):
    return element == "green"

def my_filter(fn, lat):
    matched = []
    for e in lat:
        if fn(e):
            matched.append(e)

ele = ["green", "red", "blue", "green"]
``my_filter`` kann man etwas optimieren:

Code: Alles auswählen

def my_filter(fn, lat):
    return [e for e in lat if fn(e)]
Und genau das macht die eingebaute Funktion ``filter``, also braucht man ``my_filter`` gar nicht. Hoffe das hilft etwas.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
BlackJack

(Achtung: Python 2.x)

Code: Alles auswählen

import csv
import sys
from functools import partial
from itertools import ifilter


def any_column_contains(value, row):
    return any(value in cell for cell in row)


class Table(object):
    def __init__(self, rows=()):
        self.rows = list(rows)

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

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

    def filtered(self, predicate):
        return Table(ifilter(predicate, self))

    def write_csv(self, out_file, delimiter=';'):
        csv.writer(out_file, delimiter=delimiter).writerows(self)

    @classmethod
    def load_csv(cls, filename, delimiter=';'):
        with open(filename) as csv_file:
            return cls(csv.reader(csv_file, delimiter=delimiter))


def main():
    table = Table.load_csv('test.csv')
    table.write_csv(sys.stdout)
    print
    view = table.filtered(partial(any_column_contains, 'Muster'))
    view.write_csv(sys.stdout)


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

Code: Alles auswählen

Max;Mustermann;München
Melanie;Musterfrau;Köln
Angela;Merkel;Berlin

Max;Mustermann;München
Melanie;Musterfrau;Köln
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

Jetzt erben wir noch direkt von »list« und nehmen die dann überflüssigen Methoden raus.
Die Asymmetrie zwischen »load_csv« und »write_csv« ist auch unschön.
Ich hätte ja persönlich eine Filterklasse benutzt:

Code: Alles auswählen

import csv
import sys


class AnyColumnContainsFilter(object):
    def __init__(self, value, first_column=0, last_column=None):
        self.value = value
        self.first_column = first_column
        self.last_column = last_column

    def __call__(self, row):
        return any(self.value in cell for cell in row[self.first_column:self.last_column])


class Table(list):
    def filtered(self, predicate):
        return Table(row for row in self if predicate(row))

    def write_csv(self, out_file, delimiter=';'):
        csv.writer(out_file, delimiter=delimiter).writerows(self)

    @classmethod
    def load_csv(cls, csv_file, delimiter=';'):
        return cls(csv.reader(csv_file, delimiter=delimiter))


def main():
    with open('test.csv') as csv_file:
        table = Table.load_csv(csv_file)
    table.write_csv(sys.stdout)
    print
    view = table.filtered(AnyColumnContainsFilter('Muster'))
    view.write_csv(sys.stdout)


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

@Sirius3: Erben von eingebauten Datentypen finde ich in der Regel nicht schön weil man da, nun ja, alles von dem Typ erbt. Oft auch vieles was man gar nicht haben möchte, oder die Benutzer sind dann enttäuscht, dass bei einigen Operationen wo sie den neuen Typ erwarten, dann doch wieder der Basistyp zurückgegeben wird.

Die Assymmetrie beim lesen und schreiben von CSV ist da weil es ein minimales Beispiel bleiben sollte. Ausgebaut hätte es natürlich `read()`/`write()` gegeben und darauf aufbauende `load()`/`save()` Methoden. Nur das ich `read()` und `save()` für das Beispiel nicht gebraucht habe, und das ja auch ums Filtern ging.

Ich hätte wohl auch eine Klasse für den Filter geschrieben, aber der OP scheint ja damit eher sparsam umgehen zu wollen. :-)
tom1968
User
Beiträge: 38
Registriert: Sonntag 30. Juni 2013, 09:54

Ich hätte wohl auch eine Klasse für den Filter geschrieben, aber der OP scheint ja damit eher sparsam umgehen zu wollen.
Ich bin froh überhaupt eine Klasse zustande gebracht zu haben, meine erste Klasse überhaupt - und werde mich da jetzt nicht in weitere Klassen verzetteln :D :D :D :D

Die Klasse steht und funktioniert, ist aber nicht besonders pythonisch :P
Gut möglich dass ich das später noch ändere, aber Priorität hat natürlich die Funktion an sich.

Ich möchte mich bei allen die sich hier in diesem Thread beteiligt haben sehr herzlich für die aufgewendete Zeit bedanken, auch wenn nicht alles umsetzbar war bzw. mir für meine Zwecke sinnvoll erschien - z.B. eine Schleife die alle markierten Datensätze zurückgibt - diese Schleife ist bei mir im Hauptprogramm - was ich für flexibler halte da die Klasse dann unabhängiger ist (Frontend - grafisch oder Konsole).

Auf jeden Fall bin ich einen Schritt weiter was das Verständnis für Klassen anbetrifft, auch wenn gerade dieses Beispiel hier wohl als Modul mindestens genauso sinnvoll gewesen wäre. Aber vielleicht will ich ja einmal mit einem Frontend an zwei Listen gleichzeitig arbeiten und dann, denke ich, macht die Klasse natürlich mehr Sinn als ein Modul.
Und ich habe die tollen Möglichkeiten der for Schleife ohne range begriffen, was ich bisher eher für eine Spielerei :D hielt.

Also, nochmal Danke für eure Hilfe !!!
Thomas
BlackJack

@tom1968: Die Schleife im Hauptprogramm ist nicht flexibler, sondern schränkt eher ein. Dazu muss Dein Hauptprogramm nämlich wissen wie die Tabelle intern aufgebaut ist, also kann man das nicht mehr so einfach ändern ohne dass man den Code der die Tabelle benutzt auch ändern muss. Genau so etwas versucht man zu verhindern und eine Schnittstelle nach aussen anzubieten, welche die Interna einer Tabelle kapselt.
tom1968
User
Beiträge: 38
Registriert: Sonntag 30. Juni 2013, 09:54

O.k., "Schleife" war wohl unpräzise, ich mache es anders, aber....
Mein Ziel ist genau das von Dir beschriebene, ich will in der Klasse, für die jeweiligen 'Datenbanken' nichts verändern oder anpassen - und genau das müsste ich machen wenn die Ausgabe durch die Klasse erfolgte und wenn ich die Daten einmal in der Konsole übereinander (z.B. Bücherliste), einmal nebeneinander (z.B. Joggingtagebuch wo fast nur Zahlen enthalten sind) und einmal per tkinter auf einer grafischen Oberfläche darstellen will.
Auch wird die tabellen-variable oder der zeiger von außen nicht angerührt.

Das Ganze mal an einem Beispiel: Es wird irgendein Datensatz ausgegeben und ich möchten mir den nächsten Datensatz ansehen.

Das kann dann für eine Konsolenanwendung so aussehen (der Code wurde nur zum Testen der Klasse verwendet und wird durch die Auswahl „Ausgabe" aus einem Hauptmenue aufgerufen:

Code: Alles auswählen

def ausgabe():
    c = tabelle.gebe_aktuellen_Datensatz()
    print (c[0]+c[13])    #testweise hier nur mal zwei Spalten
    while True:
        b= input ("q = vor  w = ende  a = zurück s = anfang  ä = ändern lö = löschen ende = Hauptmenue ")
....
        if b == "q":
            c = tabelle.gebe_naechsten_Datensatz()
....
        print (c[0]+c[13])   #testweise hier nur mal zwei Spalten

Für eine grafische Oberfläche könnte dass dann so aussehen:

Code: Alles auswählen

vorwaertsButton = Button(root,width =7, text = " > ", command = ausgabe(tabelle.gebe_naechsten_Datensatz())
.....

def ausgabe(datensatz):
    textfeld1.delete(0, END)    
    textfeld.insert .....
......
Das zweite Beispiel ist natürlich nur schematisch und ich empfinde es als unschön nach der button-command Anweisung zu einer Funktion zu springen die im Grunde nur eine Klassenmethode aufruft und dann eine weitere Funktion - aber was besseres fällt mir momentan nicht ein - zwei Aufrufe hintereinander wird mir die command Anweisung vermutlich nicht akzeptieren ?!

Der Code an sich ist zweitrangig, was ich damit darstellen will ist lediglich dass an der Klasse, egal wie das Frontend aussieht, rein gar nichts verändert werden muss, der Aufruf ist in beiden Fällen der gleiche - und dass auch nicht auf Internes der Klasse zurückgegriffen wird.
tom1968
User
Beiträge: 38
Registriert: Sonntag 30. Juni 2013, 09:54

Eine andere Möglichkeit wäre es vielleicht noch Subklassen zu erstellen, in denen dann die Ausgabe für die jeweiligen „Datenbanken“ bewerkstelligt wird.

Genau betrachtet bringt das aber nichts weil diese Ausgabe bei jedem Programm anders aussehen wird, die Wiederverwendbarkeit dieser Subklassen als gleich null wäre und ich nur Code vom Hauptprogramm in die Subklassen schieben würde - was sich bei der Programmgröße nicht lohnt.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Du kannst es drehen und wenden wie Du willst: ``gebe_naechsten_Datensatz`` ist und bleibt keine schöne API! Jeder Container in Python ist nach dem Iterator-Protokoll implementiert (bzw. dem älteren, aber kompatiblen __getitem__) und der Benutzer erwartet das einfach. Wieso Du auf Teufel komm raus einen Sonderweg gehen willst, erschließt sich mir nicht...
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@tom1968: Ich sprach nicht davon die *Ausgabe* in der Tabellen-Klasse zu machen, sondern die Schleife. Wobei noch nicht einmal das, denn das Iterieren gehört da auch nicht hinein, sondern in einen eigenen Datentyp. Als API für einen nur vorwärts laufenden Iterator bietet Python ja bereits ein Protokoll welches von vielen Objekten implementiert wird.

Du möchtest aber anscheinend einen Zugriff der vor und zurück gehen kann. Da braucht man dann einen eigenen Typ. Denn die Information gehört IMHO nicht in die Tabelle. „Der aktuelle Datensatz” ist ja eher ein Konzept der Benutzeroberfläche und nicht der Datenhaltung in der Tabelle. Da würde es bei der Tabelle grundsätzlich erst einmal ausreichen den Indexzugriff mit `__getitem__()` möglich zu machen. Damit funktioniert dann zum einen die ``for``-Schleife über die Zeilen der Tabelle automatisch — also das was Python-Programmierer von so einem Datentyp erwarten. Und man kann sich einfach einen Iteratortyp schreiben, der auf der Tabelle operiert. Beziehungweise ganz generell auf Sequenztypen. Ungetestet:

Code: Alles auswählen

class Iterator(object):
    def __init__(self, sequence):
        self.sequence = sequence
        self.index = 0

    @property
    def current_item(self):
        return self.sequence[self.index]

    def move_forward(self, amount=1):
        self.index += amount
        if not (0 <= self.index < len(self.sequence)):
            self.index = 0 if amount < 0 else len(self.table) - 1
            raise IndexError()

    def move_backward(self, amount=1):
        self.move_forward(-amount)
Das könnte man aber auch in den Bereich der Benutzeroberfläche integrieren. Und ich würde dem Ding wohl noch die Methoden für einen normalen Iterator in Python spendieren, also eine entsprechende `__iter__()` und `next()`-Methode.

Wie geht Deine API eigentlich mit den Grenzen um?

Das vor der ``while``-Schleife etwas steht was in der Schleife noch mal vorkommen muss, nämlich den aktuellen Datensatz ausgeben, ist unschön. Das würde man nur einmal schreiben und zwar gleich am Anfang der Schleife.

Beim GUI-Beispiel fällt auf, dass es nicht objektorientiert ist. Das ist bei GUI-Anwendungen IMHO zwingend. Wenn man das nicht in Klassen strukturiert endet man mit einem verworrenen ineinander durch Abhängigkeiten verstrickten Klumpen, den man ab einer gewissen Grösse schlicht nicht mehr versteht.

Beim `command`-Argument fehlt noch ein ``lambda`` um den *Aufruf* der dort steht in eine *Funktion* zu verpacken, die dort erwartet wird.

Man würde die '>'-Schaltfläche auch eher mit einer `naechster_datensatz()`-Methode verbinden, die ihrerseits die Methode zur aktualisierung der Anzeige aufruft, falls das nötig ist.
tom1968
User
Beiträge: 38
Registriert: Sonntag 30. Juni 2013, 09:54

Wie geht Deine API eigentlich mit den Grenzen um?
Der momentane Stand ist der dass wenn Grenzen erreicht sind einfach der letzte Datensatz immer wieder zurückgegeben wird.

z.B.
Angezeigt wird Datensatz 2 (index)
>>> tabelle.gebe_vorherigen_Datensatz()
zurückgegeben wird Datensatz 1
>>> tabelle.gebe_vorherigen_Datensatz()
zurückgegeben wird Datensatz 0
>>> tabelle.gebe_vorherigen_Datensatz()
zurückgegeben wird Datensatz 0

Man könnte das natürlich auch ändern so dass nach dem letzten Datensatz wieder der erste zurückgegeben wird, mal sehen.
Momentan habe ich für "Sprünge" von einem Ende zum anderen die Methoden "gebe_ersten_datensatz" und "gebe_letzten_Datensatz"


Was die while Schleife angeht hast du natürlich recht, aber wie gesagt das wurde nur schnell hingeschrieben um die Klasse zu testen.

MfG
Thomas
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich würde die Daten wahrscheinlich in eine Deque stecken und dann .rotate() benutzen, um die Datensätze zu durchlaufen. Aber das ist wahrscheinlich zu einfach. ^^

EDIT: Wobei ich das bei näherem Überlegen wohl doch nicht tun würde. Ich denke, ein Datentyp, der intern eine Liste benutzt und sich die aktuelle Position merkt, ist letztlich doch die richtige Wahl. Eine Deque würde ja über die ursprünglichen Grenzen hinaus gehen, sobald sie rotiert wurde.
Antworten