Objekt teilweise updaten

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.
mechanicalStore
User
Beiträge: 172
Registriert: Dienstag 29. Dezember 2009, 00:09

Hallo Zusammen,

ich suche einen einfacheren Weg, nur bestimmte Attribute in einem Objekt zu aktualisieren, alle anderen Attribute sollen ihre Werte behahalten. Welche ich aendere und welche nicht, entscheidet sich ebenfalls erst zur Laufzeit. Mit default-werten und optionalen Argumenten komme ich nicht weiter, da dann alles andere ueberschrieben wird. Folgender (sehr umstaendlicher) Ansatz geht zwar, aber gibt es einen einfacheren Weg?

from pprint import pprint

Code: Alles auswählen

class Foo:
    def __init__(self, bar, bridge, street):
        self.bar = bar
        self.bridge = bridge
        self.street = street

    def update(self, d: dict):
        for key, value in d.items():
            # print(key)
            setattr(self, key, value)

def main():

    f = Foo('Test-Bar', 'Test-Bridge', 'Test-Street')
    pprint(vars(f))

    d = dict(bar = "new_bar", bridge = "new-bridge")
    f.update(d)
    pprint(vars(f))

if __name__ == '__main__':
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Ich sehe jetzt gerade nicht wo das ”sehr umständlich” sein soll?
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
pillmuncher
User
Beiträge: 1529
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@mechanicalStore:

Code: Alles auswählen

from pprint import pprint

class Foo:
    def __init__(self, bar, bridge, street):
        self.bar = bar
        self.bridge = bridge
        self.street = street

def main():
    f = Foo("Test-Bar", "Test-Bridge", "Test-Street")
    pprint(vars(f))
    d = dict(bar="new_bar", bridge="new-bridge")
    f.__dict__.update(d)  # <-------------------
    pprint(vars(f))

if __name__ == "__main__":
    main()
Siehe auch hier.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Dazu muss es aber auch ein `__dict__` geben. Das haben nicht alle in C implementierten Datentypen und auch nicht wenn man `__slots__` verwendet. Alternative Python-Implementierungen müssen das auch nicht haben. Ich würde da bei Schleife und `setattr()` bleiben.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Sirius3
User
Beiträge: 18250
Registriert: Sonntag 21. Oktober 2012, 17:20

`dataclass` bringt bereits eine `replace`-Methode mit, die zusätzlich den Vorteil hat, dass sie das Objekt nicht ändert, sondern ein geändertes Objekt zurückgibt.
Benutzeravatar
pillmuncher
User
Beiträge: 1529
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

__blackjack__ hat geschrieben: Dienstag 26. November 2024, 15:53 Dazu muss es aber auch ein `__dict__` geben. Das haben nicht alle in C implementierten Datentypen und auch nicht wenn man `__slots__` verwendet.
Ich weiß. Deswegen hatte ich ja die Doku zu object.__dict__ verlinkt.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wenn man `dataclasses` in Erwägung zieht: `attrs` hat eine `evolve()`-Funktion, die das gleiche macht. 😄
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 172
Registriert: Dienstag 29. Dezember 2009, 00:09

Jetzt habe ich einen aehnlichen Fall. Kann man Attribute zur Laufzeit hinzu fuegen? Z.B. durch ein Dictionary?

Code: Alles auswählen

#!/usr/bin/env python

class Foo:
    def __init__(self, test_var):
        self.the_only_defined_var_in_this_class = test_var

def main():

    d = dict(variable_1 = 'test', variable_2 = 'foo', variable_3 = 'bar')
    f = Foo('the one and only')

    # connect d to f ????

if __name__ == '__main__':
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Ich verstehe die Frage nicht ganz. Suchst Du jetzt eine *weitere* Möglichkeit? Denn *zwei* kennst Du ja bereits, wobei die eine davon direkt von Dir selbst im ersten Beitrag steht.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 172
Registriert: Dienstag 29. Dezember 2009, 00:09

@__blackjack__: Bei der ersten Anfrage ging es darum, beliebige Attribute upzudaten. Bei der heutigen Frage ging es darum, einer Instanz waehrend der Laufzeit neue Attribute hinzu zu fuegen. Ich dachte bisher, dass sowas nicht geht. Aber offenbar kann man mit setattr nicht nur vorhandene Attribute updaten, sondern auch beliebig neue Attribute anhaengen. Jedoch ob das ein idiomatischer Weg ist, konnte ich nirgends nachlesen.
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Idiomatisch ist beides nicht zu machen. 🤓
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
snafu
User
Beiträge: 6850
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

In diesem Fall würde ich kein Dict übergeben, sondern mit einer variablen Anzahl an Keyword-Argumenten in der Signatur arbeiten:

Code: Alles auswählen

class Foo:
    def __init__(self, bar, bridge, street):
        self.bar = bar
        self.bridge = bridge
        self.street = street

    def update(self, **changes):
        for key, value in changes.items():
            setattr(self, key, value)


def main():
    foo = Foo('Test-Bar', 'Test-Bridge', 'Test-Street')
    foo.update(bar="new_bar", bridge="new-bridge")
    print(vars(f))

if __name__ == '__main__':
    main()
Aber wie schon geschrieben wurde: Im dataclasses-Modul wird diese Möglichkeit auch angeboten. Sogar mit einer hübschen Repräsentation der jeweiligen Instanz, damit die Ausgabe via pprint() wegfallen kann.
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Vielleicht ist ja auch etwas wie https://github.com/python-attrs/cattrs interessant wenn man aus Wörterbüchern Objekte machen möchte. Oder das Python Wörterbuch das besser als Heroin ist: https://github.com/mewwts/addict 😇

Hier noch mal konkret in welche Richtung man beim ersten Beispiel gehen könnte:

Code: Alles auswählen

from attrs import evolve, field, frozen


@frozen
class Foo:
    bar = field()
    bridge = field()
    street = field()


def main():
    foo = Foo("Test-Bar", "Test-Bridge", "Test-Street")
    print(foo)

    changes = dict(bar="new_bar", bridge="new-bridge")
    foo = evolve(foo, **changes)
    print(foo)


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

Code: Alles auswählen

Foo(bar='Test-Bar', bridge='Test-Bridge', street='Test-Street')
Foo(bar='new_bar', bridge='new-bridge', street='Test-Street')
Dein letztes Beispiel sollte es nicht geben. Nach der `__init__()` sollten alle Attribute existieren. Wenn man noch keine Werte hat, dann halt mit einem Default-Wert, beispielsweise `None`, aber sie sollte da sein, damit man weiss welche Attribute es geben kann. Das muss ja möglich sein, denn sonst stellt sich die Frage wie man auf die Attribute zugreifen soll im Code, wenn man gar nicht weiss welche es gibt.

Drei Varianten, wobei die mit `cattrs` hier wohl Kanonen auf Spatzen ist, aber sobald sich heraus stellt, dass man das auch verschachtelt haben möchte, lohnt sich ein Blick auf `cattrs`. So eine Bibliothek sollte man im Hinterkopf haben (es gibt da noch andere):

Code: Alles auswählen

import cattrs
from attrs import evolve, field, frozen


@frozen
class Foo:
    the_only_defined_var_in_this_class = field()
    variable_1 = field(default=None)
    variable_2 = field(default=None)
    variable_3 = field(default=None)


def main():
    changes = dict(variable_1="test", variable_2="foo", variable_3="bar")
    foo = Foo("the one and only")
    print(foo)

    foo = evolve(foo, **changes)
    print(foo)

    # Oder:

    mapping = dict(variable_1="test", variable_2="foo", variable_3="bar")
    mapping["the_only_defined_var_in_this_class"] = "the one and only"
    foo = Foo(**mapping)
    print(foo)

    # Oder:

    mapping = dict(variable_1="test", variable_2="foo", variable_3="bar")
    mapping["the_only_defined_var_in_this_class"] = "the one and only"
    foo = cattrs.structure(mapping, Foo)
    print(foo)


if __name__ == "__main__":
    main()
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
mechanicalStore
User
Beiträge: 172
Registriert: Dienstag 29. Dezember 2009, 00:09

Sehr abstract erklärt geht es darum, flexibel auf eine Datenquelle zu reagieren, sobald diese sich ändert, ohne dass man überall im Code eingreifen muss (soweit das möglich ist!). Die Datenquelle kann irgendwas sein, Textdatei, json, Datenbank, csv oder sonstwas. Die Daten werden gesammelt und je nach Art der Daten "geschieht" damit was. Sobald sich nun die Datenquelle ändert, soll der Code ohne viel Aufwand daran angepasst werden.
Ich habe dazu mal ein sehr abstraktes Beispiel erstellt. Hier die die Datenquelle der Einfachheit halber ein csv-file. Nach Einlesen werden daraus Objekte gebaut (mit der umstrittenen Methode, um die es eigentlich in diesem thread geht). In work_with_objects werden die Objekte dann weiter verarbeitet und Bar Objekte daraus erstellt. In dem Fall hier, werden diese nach einfacher Regel kombiniert, jedoch wird es später viel komplexer, auch mit verschiedenen Objektstrukturen. Wenn sich nun irgendwas an der (in dem Falle csv, Header umbenennen, Spalten anfügen, etc.) ändert, braucht man nur die anfängliche dict anzupassen.

Code: Alles auswählen

Beispiel csv:

description_1;description_2;bar_description_1
desc_1_test1;desc_2_test1;bdesc_1_test1
desc_1_test2;desc_2_test2;bdesc_1_test2
desc_1_test3;desc_2_test3;bdesc_1_test3
desc_1_test4;desc_2_test4;bdesc_1_test4

Code: Alles auswählen

#!/usr/bin/env/python

import csv

d = dict(foo_var_1 = 'description_1', foo_var_2 = 'description_2',
         bar_var_1 = 'bar_description_1')

class Foo():
    def __init__(self):
        pass

def read_csv_file() -> list:
       with open('test.csv', newline = '', encoding = 'utf-8') as csvfile:
        line = csv.DictReader(csvfile, delimiter=';')
        l = []
        for row in line:
            f = Foo()
            for key, value in d.items():
                setattr(f, key, row[value])
            l.append(f)    
        return l

class Bar():
    def __init__(self):
        pass

def work_with_objects(list_of_foo) -> tuple:
    a_list = []
    b_list = []
    for n in list_of_foo:
        a = Bar()
        b = Bar()
        for key, value in d.items():
            if 'foo' in key:
                setattr(a, key, getattr(n, key))
            if 'bar' in key:
                setattr(b, key, getattr(n, key))
        a_list.append(a)
        b_list.append(b)
    return (a_list, b_list)


def main():
    list_of_foo = read_csv_file()
    works = work_with_objects(list_of_foo)

if __name__ == '__main__':
    main()
Benutzeravatar
snafu
User
Beiträge: 6850
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Na dann schau dir doch wirklich cattrs mal näher an. Das scheint ja genau für dein Vorhaben geeignet zu sein. Würde dir wahrscheinlich viel Hirnschmalz & Arbeit (Zeit) ersparen.
Benutzeravatar
__blackjack__
User
Beiträge: 13998
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mechanicalStore: Das ist doch Murks weil unbenutzbar. Jetzt hast Du `Bar`-Objekte bei denen die Objekte unterschiedliche Attribute (nicht) haben. Das geht so nicht. Wenn man den Datentyp kennt, dann weiss man welche Attribute und Methoden Objekte von dem Typ haben. Das ist doch der ganze Sinn warum man Datentypen definiert und nicht einfach Wörterbücher für alles nimmt.

Auch sinnlos ist diese beiden Listen zu erstellen, weil man einfach in beiden Fällen mit der ursprünglichen Liste arbeiten kann, denn die Objekte dort haben ja *alle* Attribute, also auch die nötigen Attribute wenn eine Funktion nur von Teilen davon Gebrauch macht.

Da bleibt letztlich das hier übrig:

Code: Alles auswählen

#!/usr/bin/env python3
import csv

NAME_TO_COLUMN_NAME = {
    "var_1": "description_1",
    "var_2": "description_2",
    "bar_var_1": "bar_description_1",
}


class Foo:
    def __init__(self, var_1, var_2, bar_var_1):
        self.var_1 = var_1
        self.var_2 = var_2
        self.bar_var_1 = bar_var_1


def load_csv_file(filename):
    with open(filename, newline="", encoding="utf-8") as file:
        return [
            Foo(
                **{
                    argument_name: row[column_name]
                    for argument_name, column_name in NAME_TO_COLUMN_NAME.items()
                }
            )
            for row in csv.DictReader(file, delimiter=";")
        ]


def main():
    foos = load_csv_file("test.csv")
    ...


if __name__ == "__main__":
    main()
Man könnte das erstellen eines `Foo`-Objekts aus einem CSV-Datensatz noch auf das `Foo`-Objekt verschieben wenn man mag:

Code: Alles auswählen

class Foo:
    def __init__(self, var_1, var_2, bar_var_1):
        self.var_1 = var_1
        self.var_2 = var_2
        self.bar_var_1 = bar_var_1

    @classmethod
    def from_row(cls, row):
        cls(
            **{
                argument_name: row[column_name]
                for argument_name, column_name in NAME_TO_COLUMN_NAME.items()
            }
        )


def load_csv_file(filename):
    with open(filename, newline="", encoding="utf-8") as file:
        return list(map(Foo.from_row, csv.DictReader(file, delimiter=";")))
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
Dennis89
User
Beiträge: 1517
Registriert: Freitag 11. Dezember 2020, 15:13

Da `cattrs` schon angesprochen wurde:

Code: Alles auswählen

#!/usr/bin/env/python

import csv
from pathlib import Path

from attrs import field, frozen
from cattrs import structure

NAMES = ["var_1", "var_2", "bar_var_1"]
CSV_FILE = Path(__file__).parent / "test.csv"


@frozen
class Foo:
    var_1 = field()
    var_2 = field()
    bar_var_1 = field()


def load_csv_file(file):
    with open(file, newline="", encoding="utf-8") as csvfile:
        return [
            structure(dict(zip(NAMES, row.values())), Foo)
            for row in csv.DictReader(csvfile, delimiter=";")
        ]


def main():
    foos = load_csv_file(CSV_FILE)


if __name__ == "__main__":
    main()
Ich geh davon aus, dass die Spalten des Wörterbuchs sich nicht irgendwie verschieben und habe deswegen die Namen nur in eine Liste gepackt.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
snafu
User
Beiträge: 6850
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Würde das mit cattrs nicht einfach so gehen?

Code: Alles auswählen

def load_csv_file(file):
    with open(file, newline="", encoding="utf-8") as csvfile:
        reader = csv.DictReader(csvfile, delimiter=";")
        return structure(list(reader), list[Foo])
Natürlich vorausgesetzt, dass die Attribute von Foo die gleichen Namen haben wie die Spalten der CSV-Datei.
mechanicalStore
User
Beiträge: 172
Registriert: Dienstag 29. Dezember 2009, 00:09

__blackjack__ hat geschrieben: Sonntag 26. Januar 2025, 16:33 ... Jetzt hast Du `Bar`-Objekte bei denen die Objekte unterschiedliche Attribute (nicht) haben. Das geht so nicht.
Es waeren nicht nur Bar-Objekte, sondern auch andere. Daher (und weil ich weiss, in welche Liste sie gepackt sind) wuerde ich schon wissen, was drin ist. Aber ich habe den Plan verworfen, Du hast mich ueberzeugt, dass das eine schlechte Loesung ist.
Trotzdem konnte ich aus Deinen Antworten (sowie die der Anderen) einiges neues mitnehmen, das mir bei anderen Sachen hilft. Daher an Dich und alle Anderen meinen herzlichen Dank fuer die nuetzlichen Beitraege.

Gruss
Benutzeravatar
snafu
User
Beiträge: 6850
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hier übrigens ein Beispiel mit Personen und Adressen bei Nutzung von cattrs (legt eine JSON-Datei im aktuellen Pfad an):

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import date
from pathlib import Path
from pprint import pprint

from attrs import define
from cattrs.preconf.json import make_converter

_CONVERTER = make_converter()
JSON_FILE = Path("persons.json")

@define
class Address:
    street: str
    house_no: str
    zip_code: str
    city: str


@define
class Person:
    name: str
    birthday: date
    address: Address


def save_json(
    persons: list[Person],
    json_file: Path,
    encoding: str = "utf-8"
) -> None:
    json_file.write_text(_CONVERTER.dumps(persons), encoding)

def load_json(
    json_file: Path,
    encoding: str = "utf-8"
) -> list[Person]:
    return _CONVERTER.loads(json_file.read_text(encoding), list[Person])


def main():
    address = Address("Spämstraße", "42 B", "01234", "Spämdorf")
    man = Person("Jürgen Müller", date(1957, 5, 2), address)
    woman = Person("Gertrud Müller", date(1957, 7, 18), address)
    save_json([man, woman], JSON_FILE)
    pprint(load_json(JSON_FILE))


if __name__ == "__main__":
    main()
Antworten