Seite 1 von 3

Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 15:45
von hcshm
Hallo, ich grübele seit geraumer Zeit über folgendem Problem: Ich möchte Instanzen einer Klasse aus einer Datei bilden - siehe nachfolgendes listing. Die Instanzen werden zwar gebildet, ich kann aber nicht auf sie zugreifen (NameError). Demgegenüber funktioniert der Zugriff, wenn ich die Instanzen manuell anlege oder über ein dictionary gehe. Ich würde aber gern direkt auf die Instanzen zugreifen. Kann mir bitte jemand erklären, was ich tun muss? Vielen Dank!

import csv

class Familienmitglieder(object):
def __init__(self, groesse, gewicht):
""" Klasse, deren Instanzen durch das Einlesen einer Datei kreiert werden sollen,
Diese Datei - familiendaten.txt - hat folgende:
Zeilenstruktur: katrin,180,85 (name, groesse, gewicht)"""
self.groesse = groesse
self.gewicht = gewicht

def einlesen(filename):
familiendaten: dict[str, Familienmitglieder] = {}
with open(filename, "r") as csvfile:
for row in csv.reader(csvfile):
familiendaten[row[0]] = Familienmitglieder(*row[1:])
return familiendaten

def main():
hugo = Familienmitglieder(185, 85.0)
print(hugo.groesse) # 185 - FUNKTIONIERT
familiendaten = einlesen("familiendaten.txt")
print(familiendaten["katrin"].groesse) # 180 - FUNKTIONIERT
print(katrin.groesse) # FUNKTIONIERT NICHT

if __name__ == '__main__':
main()

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 16:07
von sparrow
Wie du ja selbst siehst, wird "katrin" an keiner Stelle definiert. Also kann man darauf auch nicht zugreifen.
Tatsächlich hast du bereits den korrekten Weg gefunden: Die Daten gehören in eine geeignete Datenstruktur. Das Dictionary ist hier richtig.

Variablen dynamisch zu erzeugen ist zwar technisch möglich, aber unnötig schwer zu debuggen und im Code nicht nachvollziehbar.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 16:25
von hcshm
Vielen Dank für die prompte Antwort. "katrin" ist doch aber in der Datei definiert, die ich einlese. Was ist daran anders als bei der manuellen Anlage?

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 16:32
von sparrow

Code: Alles auswählen

hugo = Familienmitglieder(185, 85.0)
Dem Namen "hugo" wird als Wert eine Instanz von Familienmitglieder zugewiesen. Für das Zuweisen eines Wertes ist das "=" da. Es wird also etwas an den Namen "hugo" gebunden.

In "einlesen" erstellst du ein Dictionary und gibst das mit return zurück.
Wo genau denkst du denn, dass etwas an den Namen "katrin" gebunden wird?

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 16:34
von hcshm
Vielen Dank, das verstehe ich, kann man diese Zuweisung nicht aus einer einzulesenden Datei heraus machen?

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 16:38
von sparrow
Welches Problem möchtest du damit denn lösen?
Angenommen in der Datei stehen 200 Personendaten. Woher weißt du, wieviele es waren? Woher weißt du, welche Namen darin standen, damit du sie verwenden kannst?
Deshalb verwendet man die passende Datenstruktur. Zum Beispiel eine Liste. Oder ein Dicitonary, wenn es einen eindeutigen Schlüssel gibt, über den man auf den Wert zugreifen möchte.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 16:48
von hcshm
Vielen Dank, das verstehe ich alles, auch Deine Argumente zum Nachvollzug von Datenstrukturen leuchten mir absolut ein. Das, was ich nicht verstehe ist: warum geht das technisch nicht? Ich würde gern verstehen, warum die Anlage von Instanzen aus einer Datei heraus nicht funktioniert. Noch besser wäre natürlich, wenn mir jemand sagen würde, wie es funktioniert.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 16:54
von /me
hcshm hat geschrieben: Montag 24. Mai 2021, 16:48 Das, was ich nicht verstehe ist: warum geht das technisch nicht? Ich würde gern verstehen, warum die Anlage von Instanzen aus einer Datei heraus nicht funktioniert.
Du erstellst doch beim Einlesen der Datei eine Instanz deiner Klasse. Mit `familiendaten["katrin"]` kannst du darauf zugreifen.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 17:00
von sparrow
@hcshm: Doch, das geht technisch durchaus. Aber es zu tun macht keinen Sinn. Denn eben dafür sind Datenstrukturen da.
Deshalb habe ich dich gefragt, welches Problem du damit lösen möchtest.

Zum Code:
Der Name von der Klasse sollte im Singular stehen. Denn eine Instanz davon bildet ja nur ein Familienmitglied ab, nicht Familienmitglieder.
Klassen müssen spätestens seit Python 3 nicht mehr von "object" erben.
Wenn man eine Klasse nur als Datencontainer verwendet - in der Regel daran zu erkennen, dass sie nur aus einer __init__ bestehen - bietet es sich an, stattdessen einen Namedtuple zu verwenden. Allerdings nur, wenn man die Werte nicht verändern möchte.

Code: Alles auswählen

>>> from collections import namedtuple
>>> Familienmitglied = namedtuple('Familienmitglied', ("groesse", "gewicht"))
>>> norma = Familienmitglied(164, 56)
>>> norma.groesse
164
>>> norma.gewicht
56
>>> # aber:
>>> norma.groesse = 155
Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    norma.groesse = 155
AttributeError: can't set attribute

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 17:00
von kbr
@hcshm: wie sparrow schon schrieb, lassen sich Namen zwar dynamisch anlegen, was aber zu erheblichen Wartungs- und Verständnisproblemen später führen kann und wird. Der saubere Weg führt in solchen Fällen über eine separate Datenstruktur. Statt

Code: Alles auswählen

hugo = Familienmitglied(185, 85.0)
wäre das z.B.:

Code: Alles auswählen

familienmitglieder["Hugo"] = Familienmitglied("Hugo", 185, 85.0)
Auch so sind die Instanzen über die Namen direkt zugänglich und der globale Namensraum bleibt aufgeräumt.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 17:02
von hcshm
Vielen Dank, ja, aber ich greife doch über ein dictionary darauf zu. Warum kann ich nicht direkt zugreifen (bzw. wie kann ich direkt zugreifen)? Sorry vielmals für die Mühe und Gedanken, die ich den Experten mache. Irgendwie empfinde ich das dictionary als Umweg bzw. workaround. Und ich verstehe nicht, warum ich nicht direkt auf eine Instanz zugreifen kann, die ich durch das Einlesen einer Datei kreire.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 17:05
von sparrow
@hcshm:
@kbr erklärt das richtig.
Das ist kein Umweg. Du greifst direkt auf die Instanz zu. Sie ist nur nicht an den Namen "hugo" sondern an 'familienmitglieder["Hugo"]' gebunden.
Das ist kein Workaround sondern dafür sind Datenstrukturen da.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 17:11
von /me
hcshm hat geschrieben: Montag 24. Mai 2021, 17:02 Und ich verstehe nicht, warum ich nicht direkt auf eine Instanz zugreifen kann, die ich durch das Einlesen einer Datei kreire.
Du greifst doch direkt darauf zu. Das Dictionary ist kein Umweg, auch wenn du das glaubst.

Und wie würdest du dann dein Programm schreiben wollen?

Code: Alles auswählen

if katrin.groesse > 150 then
    pass
was soll denn dann im Code passieren wenn katrin gar nicht in der Datei ist? Und was soll mit deinem im Code angelegten hugo passieren wenn dazu ein Eintrag in der Datei existiert. Soll hugo an die neue Instanz gebunden werden. Warum? Oder warum nicht?

Warum glaubst du, dass ein Dictionary oder eine Liste nicht ausreicht und du individuelle Namen benötigst? Zeig mal einen Anwendungsfall.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 17:15
von hcshm
Vielen Dank, an /me and sparrow: Erstens bin ich jetzt von dem fixen Gedanken runter, dass irgendwas nicht gehen könnte. Zweitens kann ich alle Eure Erläuterungen in den letzten Beiträgen nachvollziehen. Erneut vielen Dank! Schöne Pfingsten noch!

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 19:25
von LukeNukem
hcshm hat geschrieben: Montag 24. Mai 2021, 17:15 Vielen Dank, an /me and sparrow: Erstens bin ich jetzt von dem fixen Gedanken runter, dass irgendwas nicht gehen könnte. Zweitens kann ich alle Eure Erläuterungen in den letzten Beiträgen nachvollziehen. Erneut vielen Dank! Schöne Pfingsten noch!
Also... räusper... das, was Du ursprünglich vorhattest, das geht schon, aber wie meine verehrten Vorposter bereits geschrieben haben: es ergibt keinen Sinn, und der Trick, um das zu machen, ist nicht nur häßlich, sondern -- aus guten Gründen -- selten dokumentiert und eher unbekannt. Ich zeige die besagten Tricks daher nur zu rein informativen Zwecken und hoffe inständig, daß niemand ohne gute Gründe auf die Idee kommt, das jemals in realem Code zu nutzen...

Code: Alles auswählen

>>> a = 5
>>> globals()['a']
5
>>> globals()['a'] = 7
>>> a
7
>>> exec('a = 9')
>>> a
9
>>> 
In Deinem Fall würde ich es allerdings ohnehin etwas anders machen und den Vornamen des Familienmitglieds in Deine "Mitglied"-Klasse aufnehmen und zudem eine Klasse "Familie" schreiben, in die Deine Mitglieder aufgenommen werden, die selbständig Deine CSV-Datei einlesen und darauf Suchfunktion(en) und einen Iterator bereitstellen kann.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import csv
from argparse import ArgumentParser

class NoMitgliedError(Exception): pass

class Mitglied:
    '''vielleicht besser das dataclasses-Modul oder ein
    collections.namedtuple benutzen...'''
    def __init__(self, vorname, groesse, gewicht):
        self.vorname = vorname
        self.groesse = groesse
        self.gewicht = gewicht

    def __str__(self):
        '''für die Ausgabe...'''
        return f'{self.vorname} ist {self.groesse} cm groß und wiegt {self.gewicht} kg.'


class Familie:
    mitglieder = list()

    def read_csv(self, filename):
        '''CSV-Datei mit Familienmitgliedern lesen'''
        with open(filename, 'r') as ifh:
            reader = csv.reader(ifh)
            for line in reader:
                mitglied = Mitglied(*line)
                self.mitglieder.append(mitglied)
        return self

    def add(self, mitglied):
        '''Jippie, ein neues Familienmitglied!'''
        # hier besser isinstance(mitglied, Mitglied) prüfen...
        # ... nicht daß sich ein Erbschleicher einschmuggelt!
        self.mitglieder.append(mitglied)

    def get(self, vorname):
        '''Hole ein Familienmitglied per Vornamen, sauber!'''
        for mitglied in self.mitglieder:
            if mitglied.vorname == vorname:
                return mitglied
        return None

    def __iter__(self):
        '''über unsere Familienmitglieder iterieren'''
        for mitglied in self.mitglieder:
            yield mitglied


if __name__ == '__main__':
    parser = ArgumentParser(description='Lies die Familienmitglieder')
    parser.add_argument('filename', type=str, help='Datei mit den...')
    args = parser.parse_args()

    familie = Familie().read_csv(args.filename)

    ## geht
    print('_________eins__________')
    for mitglied in familie:
        print(mitglied)

    ## geht
    print('_________zwei__________')
    karin = familie.get('karin')
    print(karin)
Bitte beachte übrigens den... kleinen Trick mit dem "return self" in der Funktion read_csv(); dadurch kann ich auf meiner frischen Familie()-Instanz gleich die Methode read_csv() aufrufen und bekomme trotzdem sofort die Instanz zurück, siehe "familie = Familie().read_csv()". Klar: das Einlesen der CSV-Datei könnte ich auch in einem Konstruktor machen, aber vielleicht möchte ich meine Daten ja morgen in einer SQL-Datenbank, einem Redis, einem Elasticsearch oder sogar in mehreren CSV-Dateien ablegen, und auf diese Weise bleibe ich dabei vollkommen flexibel.

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 19:48
von Sirius3
@LukeNukem: dein Familien-Klasse kann nicht mehr, als ein simples Wörterbuch, kann also weg.
`read_csv` liefert dann auch besser ein Wörterbuch zurück. Benutze keine Klassenvariablen. So wie Dein Klassendesign jetzt ist, kann es nur eine Familie, also ein Exemplar dieser Klasse geben. Also ein weiterer Grund, das nicht so als Klasse zu schreiben.
Benutze keine kryptischen Abkürzungen. Was soll `ifh` bedeuten?

Re: Instanzenbildung aus Datei

Verfasst: Montag 24. Mai 2021, 23:46
von __blackjack__
Ergänzend: Das ``return self`` ist unpythonisch. Die Methode verändert das Objekt und gibt keinen erstellten sinnvollen Rückgabewert zurück. Die idiomatische Lösung wäre eine `classmethod()` die ein `Familie`-Objekt aus einer CSV-Datei erstellt.

Der Kommentar beim hinzufügen: Besser nicht den Typ prüfen, oder eine andere Programmiersprache verwenden, die nicht auf Duck Typing setzt.

Bei Textdateien sollte man immer die Kodierung angeben und bei CSV-Dateien noch das `newline`-Argument.

Ungetestet:

Code: Alles auswählen

class Familie:
    def __init__(self):
        self.mitglieder = list()

    def __iter__(self):
        """über unsere Familienmitglieder iterieren"""
        return iter(self.mitglieder)

    def add(self, mitglied):
        """Jippie, ein neues Familienmitglied!"""
        self.mitglieder.append(mitglied)

    def get(self, vorname):
        """Hole ein Familienmitglied per Vornamen, sauber!"""
        return next(
            (mitglied for mitglied in self if mitglied.vorname == vorname),
            None,
        )

    @classmethod
    def read_csv(cls, filename):
        """CSV-Datei mit Familienmitgliedern lesen."""
        result = cls()
        with open(filename, "r", encoding="utf-8", newline="") as file:
            for row in csv.reader(file):
                result.add(Mitglied(*row))
        return result
Problematisch finde ich das nirgends geprüft wird ob der Vorname mehr als einmal vorkommt, was dann dazu führen würde das Mitglieder nicht mit `get()` erreicht werden können.

Re: Instanzenbildung aus Datei

Verfasst: Dienstag 25. Mai 2021, 02:44
von LukeNukem
Sirius3 hat geschrieben: Montag 24. Mai 2021, 19:48 @LukeNukem: dein Familien-Klasse kann nicht mehr, als ein simples Wörterbuch, kann also weg.
Du hast in Deinem ganzen Leben noch keine zwanzig Zeilen lesbaren Python-Code implementiert, oder?
Sirius3 hat geschrieben: Montag 24. Mai 2021, 19:48 `read_csv` liefert dann auch besser ein Wörterbuch zurück. Benutze keine Klassenvariablen.
Kommentare lesen kannst Du also auch nicht... ;-)
Sirius3 hat geschrieben: Montag 24. Mai 2021, 19:48 So wie Dein Klassendesign jetzt ist, kann es nur eine Familie, also ein Exemplar dieser Klasse geben. Also ein weiterer Grund, das nicht so als Klasse zu schreiben.
Das stimmt. Das ist auch genau so gewollt. Wenn ich das richtig sehe, wollte der TO auch nur eine Familie.
Sirius3 hat geschrieben: Montag 24. Mai 2021, 19:48 Benutze keine kryptischen Abkürzungen. Was soll `ifh` bedeuten?
Input File Handle. Meine Güte. Variablennamen.

Re: Instanzenbildung aus Datei

Verfasst: Dienstag 25. Mai 2021, 03:13
von LukeNukem
__blackjack__ hat geschrieben: Montag 24. Mai 2021, 23:46 Ergänzend: Das ``return self`` ist unpythonisch.
Nein. Sonst würden numpy, scipy und opencv nicht sauber funktionieren, aber klar, die sind halt "unpythonisch".
__blackjack__ hat geschrieben: Montag 24. Mai 2021, 23:46 Die Methode verändert das Objekt und gibt keinen erstellten sinnvollen Rückgabewert zurück.
Aber natürlich. Das macht Python in seiner eigenen API ganz genauso... think .sort() vs. .sorted().
__blackjack__ hat geschrieben: Montag 24. Mai 2021, 23:46 Die idiomatische Lösung wäre eine `classmethod()` die ein `Familie`-Objekt aus einer CSV-Datei erstellt.
Ja, und? Dann mach' das doch mal sauber, wie Du Dir das vorstellst.
__blackjack__ hat geschrieben: Montag 24. Mai 2021, 23:46 Der Kommentar beim hinzufügen: Besser nicht den Typ prüfen, oder eine andere Programmiersprache verwenden, die nicht auf Duck Typing setzt.
Zeig' was Besseres. Kannst Du? Ach, nein. "return next()" ist voll anfängertauglich, weißt Du.
__blackjack__ hat geschrieben: Montag 24. Mai 2021, 23:46 Bei Textdateien sollte man immer die Kodierung angeben und bei CSV-Dateien noch das `newline`-Argument.
Ja, und beim Gesäßabwischen sollte man unbedingt spezifizieren, welches Material verwendet wird. Insbesondere bei Newbies!

Schon lustig, daß Ihr zum Thema nichts beitragen vermögt und erst erwacht, wenn einer etwas erklärt. Richtige kleine Helden.

Re: Instanzenbildung aus Datei

Verfasst: Dienstag 25. Mai 2021, 07:28
von Sirius3
@LukeNukem: obwohl Du persönlich ausfällig wirst, möchte ich Dir nicht auf der selben Ebene antworten sondern sachlich bleiben.
Jede Sprache hat ihre Eigenheiten. Dass in Python einiges anders gemacht wird, als in Deiner gewohnten Programmiersprache, solltest Du annehmen, und nicht verärgert reagieren, wenn andere Leute Anmerkungen haben, die nicht gegen Dich als Person gerichtet sind, sondern allgemein allen Lesern hier im Forum helfen sollen, besseren Code zu schreiben.

Ja, in vielen Bibliotheken gibt es Abweichungen von idiomatischem Python. Meist gibt es dafür nachvollziehbare Gründe.
Das von Dir erwähnte list.sort gegenüber sorted ist ein gutes Beispiel. list.sort sortiert die Liste in-place, braucht also keinen Rückgabewert und liefert daher None zurück. sorted erzeugt eine neue sortierte Liste und muß folglich diese sortierte Liste zurückgeben.

Keine eigene Klasse zu verwenden, solange ein eingebauter Typ alles liefert, was man bisher braucht, hilft der Lesbarkeit. Dem familie.get seht man nicht an, ob jetzt hinter familie eine eigene Klasse oder ein Wörterbuch steckt, erspart aber 30 Zeilen zusätzlichen Code, den ich lesen muß.

Bei `familie = Familie().read_csv()` weiß der Leser nicht, ob read_csv nun eine neue Familie oder irgendeinen anderen Typ zurückliefert.
Bei

Code: Alles auswählen

familie = Familie()
familie.read_csv("familie.csv")
ist es dagegen klar, dass erst eine Familie erzeugt wird, und dann etwas in die Familie gelesen wird.
Will man es kürzer haben, benutzt man die von __blackjack__ erwähnte classmethod:

Code: Alles auswählen

familie = Familie.read_csv("familie.csv")
Ein wichtiges Grundprinzip beim Programmieren ist die Allgemeingültigkeit. Auch wenn man zu Anfang denkt, dass man nur eine Familie braucht, sollte man sein Programm nicht so schreiben, dass es von einer Klasse nur ein Exemplar geben darf. Denn das hat man längst vergessen, wenn dann doch die zweite Familie dazu kommt.

Hier noch die Beispielimplementierung:

Code: Alles auswählen

import csv

class Mitglied:
    def __init__(self, vorname, groesse, gewicht):
        self.vorname = vorname
        self.groesse = groesse
        self.gewicht = gewicht

    def __str__(self):
        return f'{self.vorname} ist {self.groesse} cm groß und wiegt {self.gewicht} kg.'

def read_family(filename):
    '''CSV-Datei mit Familienmitgliedern lesen'''
    result = {}
    with open(filename, encoding="utf8", newline="") as input:
        reader = csv.reader(input)
        for entry in reader:
            member = Mitglied(*entry)
            result[member.vorname] = member
    return result

def main():
    familie = read_family("familie.csv")

    for mitglied in familie.values():
        print(mitglied)

    karin = familie['karin']
    print(karin)

if __name__ == '__main__':
    main()