__init__ Durchlauf mit for Schleife

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.
Antworten
Hartmannsgruber
User
Beiträge: 89
Registriert: Mittwoch 15. Januar 2014, 22:30
Wohnort: Bad Kötzting
Kontaktdaten:

Servus liebes Forum,

ich habe mich neu in die objektorientierte Programmierung eingearbeitet.
Nun komme ich aber an das erste Problem.
Ich habe eine Klasse (Artikel) und möchte über alle Variablen die in __init__ vergeben wurden mit Hilfe einer for - Schleife durchlaufen.
Mein Gedanke dahingehend wäre gewesen, sollten neue Eigenschaften eines Artikels hinzukommen, müsste ich nicht __repr__ abändern.

Leider erhalte ich immer die Fehlermeldung "TypeError: 'function' object is not iterable".
Auch nach vermehrter suche im Forum und in der Suchmaschine habe ich leider keine Lösung für mein Problem finden können.
Ich würde mich daher sehr um Hilfe freuen.

Code: Alles auswählen

#!usr/bin/python3

import csv
import re
import shutil

class Lager:
    def __init__(self):
        self.Lager = []
        self.Feldnamen = []
        
    def artikel_hinzufuegen(self, Artikel):
        self.Lager.append(Artikel)

        
    def artikel_anzeigen(self):
        for a in self.Feldnamen:
            if a != "St. 7%" and a != "St. 19%"and a != "Lieferant" and a != "Lieferantenartikelnr.": 
                print(a + " | ", end="")
        print(end="\n")
        konsolen_breite_fuellen("-")
        
        for Artikel in self.Lager:
            print(Artikel)
            konsolen_breite_fuellen("-")

            
    def artikel_einlesen(self, Datei, Ziel):
        with open(Datei, "r") as CSVdatei:
            reader = csv.reader(CSVdatei, delimiter=";")
            data = list(reader)

            for Feldname in data[0]:
                self.Feldnamen.append(Feldname)

            for Einzelliste in data[1:]:
                Ziel.artikel_hinzufuegen(Artikel(Einzelliste[0], Einzelliste[1], Einzelliste[2], \
                                                 Einzelliste[3], Einzelliste[4], Einzelliste[5], \
                                                 Einzelliste[6], Einzelliste[7], Einzelliste[8], \
                                                 Einzelliste[9], Einzelliste[10], Einzelliste[11], \
                                                 Einzelliste[12]))

            
class Artikel():
    def __init__(self, nr, bez, menge, einh, vpe, e_pr_ne, st_7, st_19, e_pr_br, geb_pr_ne, geb_pr_br, lief, liefartnr):
        self.Artikelnummer = int(nr)
        self.Bezeichnung = bez
        self.Menge = float(menge.replace(",", "."))
        self.Einheit = einh
        self.VPE = vpe#int(vpe)
        self.E_Pr_Ne = e_pr_ne#float(e_pr_ne.replace(",", "."))
        self.Steuer7 = bool(st_7)
        self.Steuer19 = bool(st_19)
        self.E_Pr_Br = e_pr_br#float(e_pr_br.replace(",", "."))
        self.Geb_Pr_Ne = geb_pr_ne#float(geb_pr_ne.replace(",", "."))
        self.Geb_Pr_Br = geb_pr_br#float(geb_pr_br.replace(",", "."))
        self.Lieferant = lief
        self.Lieferantenartikelnummmer = int(liefartnr)
                
    def __repr__(self):
        for a in Artikel.__init__:
            return(a)
        #return(str(self.Artikelnummer) + " | " + self.Bezeichnung)

    
def konsolen_breite_fuellen(Trennzeichen):
    breite = shutil.get_terminal_size()
    zeilen_breite = breite.columns
    print(zeilen_breite * Trennzeichen, end="")

    
LA = Lager()
LA.artikel_einlesen("Artikelliste.csv", LA)
LA.artikel_anzeigen()
Über allgemeine Verbesserungsvorschläge bin ich ebenfalls immer sehr dankbar! :D
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Variablennamen und Attribute schreibt man nach Konvention klein_mit_unterstrich.
Im csv-Modul gibt es den DictReader.
Keine Abkürzungen verwenden.
›__repr__‹ ist falsch. ›return‹ ist keine Funktion, sollte also auch nicht wie eine geschrieben werden, Damit wird die Funktion nach dem ersten Schleifendurchgang auch gleich wieder verlassen. ›__repr__‹ sollte etwas liefern, was das Objekt wieder erzeugen kann, und ist nur für Debug-Zwecke gedacht. Was Du überschreiben willst ist ›__str__‹.
Was Du suchst ist ›self.__dict__‹, was aber erst seit Python 3.7 die Reihenfolge garantiert.
Explizit ist besser als implizit.
Benutzeravatar
__blackjack__
User
Beiträge: 14044
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ein Blick auf das `attr`-Modul wäre vielleicht eine gute Idee, dann muss man eine schöne `__repr__()` nicht selbst schreiben.

Das `Ziel`-Argument bei `Lager.artikel_einlesen()` ist überflüssig/falsch. Das `Lager`-Objekt auf dem die Methode aufgerufen wird, ist ja bereits als erstes Argument vorhanden, und es würde kaum Sinn machen die Methode auf einem `Lager`-Objekt aufzurufen aber ein anderes als `Ziel` zu übergeben, denn wenn man ein anderes hat wo die Daten eingelesen werden sollen, dann würde man die Methode ja auf *dem* Objekt aufrufen.

Das sollte auch keine normale Methode sein, denn so wie der Code jetzt geschrieben ist, macht ein erneuter Aufruf die Daten kaputt, weil `Feldnamen` einfach erweitert werden. `Lager.__init__()` sollte die Werte für die Attribute am schon als Argumente übergeben bekommen und Einlesen eines `Lager`-Objekts aus einer Datei wäre dann eine `classmethod()` welche die Daten einliest, und dann ein Objekt erstellt und die Daten dabei übergibt.

Die Daten erst komplett in eine Liste einzulesen nur um das erste Element anders behandeln zu können als alle anderen ist nicht speichereffizient. Den ersten Datensatz, also die Kopfzeile, kann man mit `next()` vom CSV-Reader holen und danach dann in einer Schleife über die restlichen Datensätze iterieren.

Das mit der Kopfzeile ist aber sowieso komisch, denn letztlich braucht man diese Werte nicht wenn die CSV-Datei einen festen Aufbau hat. Und den muss sie haben, weil die `Artikel`-Attribute fest sind.

``\`` zum fortsetzen von (logischen) Zeilen ist fehleranfällig. Die sind auch gar nicht nötig, denn der Compiler ist schon so schlau zu wissen, das die (logische) Zeile noch nicht zuende sein kann, solange noch schliessende Klammern ausstehen. Zudem ist das ausschreiben von `Einzelliste` mit all den aufsteigenden Indexwerten auch eher Schreibarbeit für Leute die zu viel Langeweile haben. Man kann mit der entsprechenden Syntax den Inhalt eines iterierbaren Objekts als einzelne Argumente übergeben lassen. Arbeite mal die beiden Abschnitte zu Funktionen im Tutorial in der Python-Dokumentation durch.

Die Argumentnamen von `Artikel.__init__()` sollten ordentlich sein. Nicht `bez` wenn `bezeichnung` gemeint ist. Ich würde da auch keine Umwandlungen der Argumente vornehmen sondern die Methode ”dumm” halten – wäre sie bei Verwendung von `attr` sowieso – und eher eine CSV-Datensatz-spezifische Klassenmethode schreiben die das macht.

Was ist `Steuer7` und `Steuer19`? Kann es sein, dass man im Programm Namen ändern muss, wenn sich die Steuersätze ändern? Ich finde sowas toll, ehrlich, weil ich damit schon mal Geld verdient habe bei der Erhöhung auf 19% so etwas zu fixen. 😎 Aber selbst sollte man so etwas nicht machen. Zudem finde ich die beiden Attribute auch komisch, also das es beide gibt. Kann denn ein Datensatz bei beiden Werten `True` haben? Und wie sieht es bei keinem von beiden aus? Ist das nicht doch eher *ein* Attribut `mehrwertsteuersatz` das entweder den Wert 7 oder den Wert 19 hat? Eventuell noch `None` wenn er nicht bekannt ist.

Auch bei den Attributen sollte man vernünftige Namen haben, also nicht so etwas wie `E_Pr_Ne`. Muss man das im Periodensystem suchen?
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Benutzeravatar
/me
User
Beiträge: 3561
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

__blackjack__ hat geschrieben: Montag 1. Juli 2019, 22:18 Ein Blick auf das `attr`-Modul wäre vielleicht eine gute Idee, dann muss man eine schöne `__repr__()` nicht selbst schreiben.
Du meintest `attrs`, nicht `attr, oder hab ich da was übersehen?
Benutzeravatar
__blackjack__
User
Beiträge: 14044
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@/me: Also es heisst ``import attr`` aber ``pip install attrs``. Dafür sollte man den Autor teeren und federn…
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Hartmannsgruber
User
Beiträge: 89
Registriert: Mittwoch 15. Januar 2014, 22:30
Wohnort: Bad Kötzting
Kontaktdaten:

So ich habe nun ein paar der Verbesserungen am Quelltext vorgenommen.

Verbesserungen:
  • Variablennamen und Attribute schreibt man nach Konvention klein_mit_unterstrich.
  • Im csv-Modul gibt es den DictReader.
  • Das `Ziel`-Argument bei `Lager.artikel_einlesen()` ist überflüssig/falsch.
  • ``\`` zum fortsetzen von (logischen) Zeilen ist fehleranfällig.
  • Die Argumentnamen von `Artikel.__init__()` sollten ordentlich sein.
  • Die Daten erst komplett in eine Liste einzulesen nur um das erste Element anders behandeln zu können als alle anderen ist nicht speichereffizient.

Code: Alles auswählen

#!usr/bin/python3

import csv
import re
import shutil

class Lager:
    def __init__(self):
        self.lager = []
        
    def artikel_hinzufuegen(self, artikelliste):
        print("artikel hinzufügen gestartet")
        for artikel in artikelliste:
            self.lager.append(Artikel(artikel["Art.-Nr."],
                                      artikel["Artikelbezeichnung"],
                                      artikel["Menge"],
                                      artikel["Einh."],
                                      artikel["VPE"],
                                      artikel["E.-Pr. Ne."],
                                      artikel["Steuersatz"],
                                      artikel["E.-Pr. Br."],
                                      artikel["Geb.-Pr. Ne."],
                                      artikel["Geb.-Pr. Br."],
                                      artikel["Lieferant"],
                                      artikel["Lieferantenartikelnr."]))
                              
        print(self.lager)
        
 #   def artikel_anzeigen(self):
 #       for a in self.feldnamen:
 #           if a != "St. 7%" and a != "St. 19%" and a != "Lieferant" and a != "Lieferantenartikelnr.": 
 #               print(a + " | ", end="")
 #       print(end="\n")
 #       konsolen_breite_fuellen("-")
 #       
 #       for artikel in self.lager:
 #           print(artikel)
 #           konsolen_breite_fuellen("-")

            
    def artikel_einlesen(self, datei):
        with open(datei, "r") as csv_datei:
            reader = csv.DictReader(csv_datei, delimiter=";")
            csv_liste = list(reader)
            
            for row in csv_liste:
                for einzelwert in row:
                    if row[einzelwert] == '':
                        row[einzelwert] = "0"
                        
                row["Art.-Nr."] = int(row["Art.-Nr."])
                row["Artikelbezeichnung"] = row["Artikelbezeichnung"]
                row["Menge"] = float(row["Menge"].replace(",","."))
                row["Einh."] = row["Einh."]
                row["VPE"] = int(row["VPE"])
                row["E.-Pr. Ne."] = float(row["E.-Pr. Ne."].replace(",","."))
                row["Steuersatz"] = int(row["Steuersatz"])
                row["E.-Pr. Br."] = float(row["E.-Pr. Br."].replace(",","."))
                row["Geb.-Pr. Ne."] = float(row["Geb.-Pr. Br."].replace(",","."))
                row["Geb.-Pr. Br."] = float(row["Geb.-Pr. Br."].replace(",","."))
                row["Lieferant"] = row["Lieferant"]
                row["Lieferantenartikelnr."] = int(row["Lieferantenartikelnr."])

            return csv_liste
            
class Artikel():
    def __init__(self, artikelnummer, bezeichnung, menge, einheit, vpe, e_pr_ne, steuersatz,
                 e_pr_br, geb_pr_ne, geb_pr_br, lieferant, lieferantenartikelnummer):
        self.artikelnummer = artikelnummer # -> int
        self.bezeichnung = bezeichnung # -> str
        self.menge = menge # -> float  
        self.einheit = einheit # -> str
        self.vpe = vpe # -> int
        self.einzelpreis_netto = e_pr_ne # -> float 
        self.steuersatz = steuersatz # -> int
        self.einzelpreis_brutto = e_pr_br # -> float
        self.gebbindepreis_netto = geb_pr_ne # -> float 
        self.gebindepreis_brutto = geb_pr_br # -> float 
        self.lieferant = lieferant # -> str
        self.lieferantenartikelnummmer = lieferantenartikelnummer # -> int
                
    def __str__(self):
        print(str(self.artikelnummer) + " | " + self.bezeichnung)

    
def konsolen_breite_fuellen(trennzeichen):
    breite = shutil.get_terminal_size()
    zeilen_breite = breite.columns
    print(zeilen_breite * trennzeichen, end="")

    
LA = Lager()
LA.artikel_einlesen("Artikelliste.csv")
LA.artikel_hinzufuegen(LA.artikel_einlesen("Artikelliste.csv"))
#LA.artikel_anzeigen()
Jetzt habe ich aber immer noch das blöde Problem mit der Ausgabe.
Wie kriege ich die einzelnen Instanzvariablen der Klasse bei der Ausgabe formatiert?

Wenn ich wie beschrieben die __str__ anstelle der __repr__ verwende erhalte ich nur "main.obejct at ......"
Auch wenn ich __dict__ verwende erhalte ich zwar eine schöne Liste, aber auch nur mit "main.object at ..."
@__blackninja__
Was ist `Steuer7` und `Steuer19`? Kann es sein, dass man im Programm Namen ändern muss, wenn sich die Steuersätze ändern? Ich finde sowas toll, ehrlich, weil ich damit schon mal Geld verdient habe bei der Erhöhung auf 19% so etwas zu fixen. 😎 Aber selbst sollte man so etwas nicht machen. Zudem finde ich die beiden Attribute auch komisch, also das es beide gibt. Kann denn ein Datensatz bei beiden Werten `True` haben? Und wie sieht es bei keinem von beiden aus? Ist das nicht doch eher *ein* Attribut `mehrwertsteuersatz` das entweder den Wert 7 oder den Wert 19 hat? Eventuell noch `None` wenn er nicht bekannt ist.
Bei den Artikeln handelt es sich um Lebensmittel, die ja nicht einheitlich mit 7% besteuert werden, sondern auch mit 0%, oder 19%.
0% wären in meinen Fall nicht vorgekommen, daher hatte ich die beiden Spalten erdacht und hätte diesen entweder den Wert True oder False gegeben.
Ich habe es aber nun auf eine Spalte umgestellt in der die Prozentwerte eingetragen sind um es zu vereinfachen.
Benutzeravatar
sparrow
User
Beiträge: 4538
Registriert: Freitag 17. April 2009, 10:28

Hartmannsgruber hat geschrieben: Donnerstag 4. Juli 2019, 06:36Bei den Artikeln handelt es sich um Lebensmittel, die ja nicht einheitlich mit 7% besteuert werden, sondern auch mit 0%, oder 19%.
0% wären in meinen Fall nicht vorgekommen, daher hatte ich die beiden Spalten erdacht und hätte diesen entweder den Wert True oder False gegeben.
Ich habe es aber nun auf eine Spalte umgestellt in der die Prozentwerte eingetragen sind um es zu vereinfachen.
Oder halt 10,7% MwSt. Kommt immer auf den Anwendungsfall an. Und deshalb sollte man das auf keinen Fall hart im Code hinterlegen.
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Hartmannsgruber hat geschrieben: Donnerstag 4. Juli 2019, 06:36 Wenn ich wie beschrieben die __str__ anstelle der __repr__ verwende erhalte ich nur "main.obejct at ......"
Auch wenn ich __dict__ verwende erhalte ich zwar eine schöne Liste, aber auch nur mit "main.object at ..."
Die Repräsentation einer Liste oder eines Wörterbuchs ist ja auch nicht für die "schöne" Ausgabe gedacht.
Wenn Du eine Liste ausgeben willst, gibst Du die einzelnen Einträge nacheinander aus, eben so formatiert, wie Du es als "schön" empfindest.
Bei einem Wörterbuch möchte man vielleicht eine zweispaltige Tabelle haben, oder bei Dir noch viel mehr Spalten.
So etwas muß man dann explizit programmieren.
Benutzeravatar
__blackjack__
User
Beiträge: 14044
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Hartmannsgruber: Die `__str__()`-Methode darf nichts *aus*geben, die muss eine Zeichenkette als Ergebnis *zurück*geben!

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

Der erste Aufruf von `artikel_einlesen()` hat keinen Effekt weil mit dem Ergebnis nichts gemacht wird.

Der Attributname `lager` ist nicht so toll, denn ein Lager besteht ja nicht aus einem `lager` sondern aus Artikeln.

`artikel_einlesen()` ist gar keine Methode mehr, das ist eine Funktion die in der Klasse Lager steckt. Funktionen sollte man nicht in Klassen definieren, ausser es gibt einen guten Grund dafür. Und dann sollte man das auch mit `staticmethod()` klar machen, dass das Absicht ist, dass da eine Funktion in der Klasse steckt.

Alternativ könnte man daraus eine Klassenmethode machen, die ein `Lager`-Objekt aus einer CSV-Datei erstellt.

Grunddatentypen wie `liste` gehören nicht in Namen. Es kommt häufiger mal vor, dass man den Typ ändert, oder das an der Stelle gar keine Liste erforderlich ist, sondern beliebige iterierbare Objekte möglich sind. Wenn man den Datentyp ändert, oder wenn der gar nicht so genau erforderlich ist, hat man irreführende Namen im Programm. Das führt dann dazu das Deutsch keine gute Sprache für Bezeichner ist, weil man dann Probleme bekommt wenn Einzahl und Mehrzahl eines Begriffs gleich geschrieben werden. Im Englischen kann man ``for article in articles:`` schreiben. Mit Deutsch hat man an der Stelle ein Problem.

Die Spaltennamen aus der CSV-Datei sollten nur *einmal* im Programm stehen. Bei den vielen Wiederholungen steigt die Gefahr, dass man sich irgendwo mal verschreibt, und wenn man da mal etwas anpassen will/muss, muss man das an jeder Stelle tun, wo der Spaltenname im Quelltext steht. Es ist sogar schon ein Fehler im Programm: beim Umwandeln wird der Wert von 'Geb.-Pr. Ne.' auf den Wert von 'Geb.-Pr. Br.' gesetzt.

Am besten definiert man eine Konstante die Paare aus Spaltenname und Umwandlungsfunktion enthält, dann kann man sich eine Menge Tipparbeit sparen.

Die Umwandlung einer CSV-Zeile in ein `Artikel`-Objekt kann man zusammen mit der Konstante als Klassenmethode auf `Artikel` definieren.

Das Erstellen von `Artikel`-Objekten sollte nicht in `Lager.artikel_hinzufügen()` passieren. Da sollte man bereits fertige `Artikel`-Objekte übergeben. Wenn die nämlich mal aus einer anderen Quelle kommen sollten als aus CSV-Dateien müsste man diese Werte erst einmal in eine Liste aus Wörterbüchern mit den Spaltennamen der CSV-Dateien als Schlüssel umwandeln, damit sie dann in der `artikel_hinzufügen()`-Methode in `Artikel`-Objekte umgewandelt werden.

Beim `Lager` würde es Sinn machen, dass man die Anzahl der Artikel mit der `len()`-Funktion abfragen könnte, und das man über das Lager iterieren könnte.

Beim öffnen von Textdateien sollte man immer die Kodierung explizit angeben.

Ungetestet:

Code: Alles auswählen

#!usr/bin/python3
import csv

from attr import attrib, attrs


def parse_float(string):
    if not string:
        string = '0'
    return float(string.replace(',', '.'))


@attrs
class Artikel:
    
    COLUMNS = [
        ('Art.-Nr.', int),
        ('Artikelbezeichnung', str),
        ('Menge', parse_float),
        ('Einh.', str),
        ('VPE', int),
        ('E.-Pr. Ne.', parse_float),
        ('Steuersatz', parse_float),
        ('E.-Pr. Br.', parse_float),
        ('Geb.-Pr. Ne.', parse_float),
        ('Geb.-Pr. Br.', parse_float),
        ('Lieferant', str),
        ('Lieferantenartikelnr', int),
    ]

    artikelnummer = attrib()
    bezeichnung = attrib()
    menge = attrib()
    einheit = attrib()
    vpe = attrib()
    einzelpreis_netto = attrib()
    steuersatz = attrib()
    einzelpreis_brutto = attrib()
    gebbindepreis_netto = attrib()
    gebindepreis_brutto = attrib()
    lieferant = attrib()
    lieferantenartikelnummmer = attrib()
                
    def __str__(self):
        return f'{self.artikelnummer} | {self.bezeichnung}'
    
    @classmethod
    def from_row(cls, row):
        return cls(*(converter(row[name]) for name, converter in cls.COLUMNS))


@attrs
class Lager:

    artikel = attrib(factory=list)

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

    def __iter__(self):
        return iter(self.artikel)

    @classmethod
    def from_csv(cls, filename):
        with open(filename, 'r', encoding='utf-8') as csv_file:
            reader = csv.DictReader(csv_file, delimiter=';')
            return cls(list(map(Artikel.from_row, reader)))


def main():
    lager = Lager.from_csv('Artikelliste.csv')
    print(lager)
    print()
    print('Es sind', len(lager), 'Artikel im Lager:')
    for artikel in lager:
        print(artikel)


if __name__ == '__main__':
    main()
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Antworten