Dataclass: Attribute automatisch berechnen

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
Benutzeravatar
noisefloor
User
Beiträge: 4185
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

folgende Problemstellung: es soll eine Durchflussrate umgerechnet werden, es gibt fünf mögliche Einheiten. Wenn man den Wert für eine Einheit vorgibt, sollen die anderen vier automatisch berechnet werden.

Mein Ansatz bisher ist über eine DataClass:

Code: Alles auswählen

from dataclasses import dataclass

@dataclass
class Flowrate():
    _kg_to_lbs: float = 2.20462262
    kg_per_min: int = None
    kg_per_h: int = None
    t_per_h: int = None
    lbs_per_min: int = None
    lbs_per_h: int = None

    def __post_init__(self):
        if self.kg_per_min:
            self.kg_per_h = self.kg_per_min * 60
            self.t_per_h = self.kg_per_h / 1000
            self.lbs_per_min = round(self.kg_per_min * self._kg_to_lbs, 0)
            self.lbs_per_h = self.lbs_per_min * 60
        if self.kg_per_h:
            self.kg_per_min = int(self.kg_per_h / 60)
            self.t_per_h = self.kg_per_h / 1000
            self.lbs_per_min = round(self.kg_per_min * self._kg_to_lbs, 0)
            self.lbs_per_h = self.lbs_per_min * 60
        #und so weiter
Das funktioniert auch:

Code: Alles auswählen

>>> >>> f = Flowrate(kg_per_min = 100)
>>> f
Flowrate(_kg_to_lbs=2.20462262, kg_per_min=100, kg_per_h=6000, t_per_h=6.0, lbs_per_min=220.0, lbs_per_h=13200.0)
aber logischerweise nur bei der Instanzierung. Wenn ich dann z.B. `f.kg_per_min = 200` aufführen, ändern sich die anderen vier Attribute nicht mehr.

Theoretisch sollte man doch setter für jedes Attribut schreiben können, die dann die andere vier Attribute ändern. Wenn das gehen sollte checke ich nicht, wie das geht.

Es muss auch keine DataClass sein, es ginge auch jedes andere Objekt, was die fünf Attribute hat und die Werte neu berechnet, wenn man ein Attribut ändert.

Gruß, noisefloor
Benutzeravatar
Dennis89
User
Beiträge: 1526
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

spontan würde mir das einfallen, ist aber bei mehreren Werten etwas "schreib"-aufwändig:

Code: Alles auswählen

from dataclasses import dataclass


@dataclass
class Flowrate:
    _kg_per_min = 2.20462262
    kg_per_h = None

    def __post_init__(self):
        self.calculate_units()

    def clear_values(self):
        self._kg_per_min = None
        self.kg_per_h = None

    def calculate_units(self):
        if self._kg_per_min:
            self.kg_per_h = self._kg_per_min * 60
        elif self.kg_per_h:
            self.kg_per_min = int(self.kg_per_h / 60)

    @property
    def kg_per_min(self):
        return self._kg_per_min

    @kg_per_min.setter
    def kg_per_min(self, value):
        self.clear_values()
        self._kg_per_min = value
        self.calculate_units()


def main():
    flowrate = Flowrate()
    print(flowrate.kg_per_h)
    flowrate.kg_per_min = 100
    print(flowrate.kg_per_h)


if __name__ == "__main__":
    main()
`attrs`hat einen `converter`, aber ich glaube nicht dass der hilft. Macht es Sinn eine neue Instanz zu erstellen, wenn sich ein Wert ändert?


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14019
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@noisefloor: Das klingt eher nach einem Fall für Properties.

Code: Alles auswählen

class Flowrate:
    _KG_TO_LBS = 2.20462262

    def __init__(self, kg_per_h=0):
        self.kg_per_h = kg_per_h

    @property
    def kg_per_min(self):
        return self.kg_per_h // 60

    @kg_per_min.setter
    def kg_per_min(self, value):
        self.kg_per_h = value * 60

    @property
    def t_per_h(self):
        return self.kg_per_h // 1000

    @t_per_h.setter
    def t_per_h(self, value):
        self.kg_per_h = value * 1000

    @property
    def lbs_per_min(self):
        return int(round(self.kg_per_min * self._KG_TO_LBS))

    @lbs_per_min.setter
    def lbs_per_min(self, value):
        self.kg_per_min = int(round(value / self._KG_TO_LBS))

    from_kg_per_h = __init__

    @classmethod
    def from_t_per_h(cls, value):
        result = cls()
        result.t_per_h = value
        return result

    @classmethod
    def from_lbs_per_min(cls, value):
        result = cls()
        result.lbs_per_min = value
        return result
Wobei ich das generell in Frage stellen würde diese ganzen Attribute zu haben. Ich würde da eher schauen ob eine Bibliothek wie `pint` mich da weiter bringt.

Code: Alles auswählen

In [102]: ur = pint.UnitRegistry()

In [103]: ur.kilogram / ur.hour
Out[103]: <Unit('kilogram / hour')>

In [104]: kg_per_hour = ur.kilogram / ur.hour

In [105]: lbs_per_minute = ur.pound / ur.minute

In [106]: x = 10 * kg_per_hour
Out[106]: <Quantity(10, 'kilogram / hour')>

In [107]: x.to(lbs_per_minute)
Out[107]: <Quantity(0.367437104, 'pound / minute')>
“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
noisefloor
User
Beiträge: 4185
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

Danke für das Feedback. Was glaube ich in meinen Ausgangpost nicht ganz klar rüber kam: das soll in alle fünf Richtungen funktionieren. Also wenn man meine DataClass nehmen würde und mit `f = Flowrate(lbs_per_h=40000)` instanziert, sollen kg_per_min usw. berechnet werden. Es muss nicht immer `kg_per_min` als Argument übergeben werden.

Genutzt werden soll das später im Backend einer Webanwendung: es gibt eine Formular mit einem Eingabewert für einen positiven Interger Wert und ein Auswahlmenü für die fünf Einheiten. Nach dem Übertragen sollen im Backend die andere vier Werte berechnet werden und dann als HTML-Seite ausgegeben.

Gruß, noisefloor
Benutzeravatar
Dennis89
User
Beiträge: 1526
Registriert: Freitag 11. Dezember 2020, 15:13

das soll in alle fünf Richtungen funktionieren
Das wäre so möglich. Ist nur ein Beispiel die Umrechnungen und andere Werte müssen noch ergänzt werden.

Code: Alles auswählen

from attrs import define, field

@define
class Flowrate:
    _kg_per_min = field(default=None)
    _kg_per_h = field(default=None)

    def __attrs_post_init__(self):
        self.calculate_units()

    def clear_values(self):
        self._kg_per_min = None
        self._kg_per_h = None

    def calculate_units(self):
        if self._kg_per_min:
            self._kg_per_h = self._kg_per_min * 60
        elif self.kg_per_h:
            self.kg_per_min = int(self._kg_per_h / 60)

    @property
    def kg_per_h(self):
        return self._kg_per_h

    @property
    def kg_per_min(self):
        return self._kg_per_min

    @kg_per_min.setter
    def kg_per_min(self, value):
        self.clear_values()
        self._kg_per_min = value
        self.calculate_units()


def main():
    flowrate = Flowrate(kg_per_min=200)
    print(flowrate.kg_per_h)
    flowrate.kg_per_min = 100
    print(flowrate.kg_per_h)


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14019
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@noisefloor: Ich sehe da nicht zwingend eine Klasse und schon mal gar nicht, dass das Objekt dafür veränderbar sein muss, also dass man nicht einfach bei einem neuen Wert ein neues Objekt erstellen kann, statt diesen einen Wert neu zu setzen und die anderen neu zu berechnen.

Wenn man `pint` nicht verwenden möchte, dann könnte man aber zumindest den Ansatz übernehmen so etwas wie zusammengesetzte Werte + Einheiten zu haben und die ”umrechenbar” zu machen.

Code: Alles auswählen

#!/usr/bin/env python3
from collections import namedtuple

from pint import UnitRegistry

UNITS = ["kg per min", "kg per h", "t per h", "lbs per min", "lbs per h"]

Flowrate = namedtuple("Flowrate", [unit.replace(" ", "_") for unit in UNITS])


def calculate_flowrate(units, flow_rate_text):
    flow_rate = units[flow_rate_text]
    return Flowrate(*(round(flow_rate.to(unit).magnitude) for unit in UNITS))


def main():
    units = UnitRegistry()
    print(calculate_flowrate(units, "100 kg per min"))


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

Code: Alles auswählen

Flowrate(kg_per_min=100, kg_per_h=6000, t_per_h=6, lbs_per_min=220, lbs_per_h=13228)
“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
kbr
User
Beiträge: 1506
Registriert: Mittwoch 15. Oktober 2008, 09:27

Ich würde auch den Ansatz mit Properties wählen und den Wert selbst nur über ein Attribut (z. B. kg pro min) speichern. Die Getter und Setter rechnen dann gegen dieses Attribut. Eine dataclass brauchst du nicht.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1231
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Ich würde immer auf die SI-Einheit zurückfallen, properties für die anderen Einheiten verwenden und mit Klassenmethoden von anderen Einheiten instanziieren.

Code: Alles auswählen

from dataclasses import dataclass


@dataclass
class FlowRate:
    m3_p_s: float

    @property
    def l_p_s(self) -> float:
        """Liters per second."""
        return self.m3_p_s * 1000
    
    @property
    def m3_p_h(self) -> float:
        """Cubic meters per hour."""
        return self.m3_p_s * 3600
    
    @classmethod
    def from_l_p_s(cls, l_p_s: float) -> "FlowRate":
        """Create FlowRate from liters per second."""
        return cls(m3_p_s=l_p_s / 1000)
    
    @classmethod
    def from_m3_p_h(cls, m3_p_h: float) -> "FlowRate":
        """Create FlowRate from cubic meters per hour."""
        return cls(m3_p_s=m3_p_h / 3600)



fr = FlowRate.from_l_p_s(10.0)

print(fr)
print(fr.m3_p_s)
print(fr.l_p_s)
print(fr.m3_p_h)
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
Kebap
User
Beiträge: 776
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

Was ist denn effektiver? Die 5 Werte zu speichern, oder nur einen speichern, aber bei jeder Ausgabe die 4 fehlenden neu zu berechnen?
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
__blackjack__
User
Beiträge: 14019
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Kebap: Da gibt es keinen Unterschied. Vielleicht wolltest Du fragen was effizienter ist? 😝
“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
kbr
User
Beiträge: 1506
Registriert: Mittwoch 15. Oktober 2008, 09:27

@Kebap: Wenn über einen zentralen Wert gegangen wird, dann hast du pro Abfrage maximal eine Rechenoperation. Beim setzen ebenso.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1231
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Über die Ausführungsgeschwindigkeit sollte man sich jedenfalls keine Gedanken machen :-D
Die macht man sich z.B. erst, wenn man ein paar Millionen Objekte hat. Dann möchte man die Werte speichern, die zur Verfügung stehen bzw. in die entsprechende Einheit umrechnen. Aber bei so ein paar Objekten würde ich mir eher über die Wartung des Codes Gedanken machen und mögliche Seiteneffekte der Dataclass berücksichtigen. Je mehr Code, desto mehr wahrscheinliche Bugs.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
Kebap
User
Beiträge: 776
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

__blackjack__ hat geschrieben: Dienstag 24. Juni 2025, 15:08 @Kebap: Da gibt es keinen Unterschied. Vielleicht wolltest Du fragen was effizienter ist? 😝
Ja.
kbr hat geschrieben: Dienstag 24. Juni 2025, 15:57 @Kebap: Wenn über einen zentralen Wert gegangen wird, dann hast du pro Abfrage maximal eine Rechenoperation. Beim setzen ebenso.
Wenn ich @noisefloor richtig verstand, dann soll im Web ein Wert eingegeben, und die resultierenden 4 anderen Werte ausgegeben werden. Also muss man die hier alle einmal umrechnen. OK, das muss man sowieso.

Die Frage ist ja eher, ob es eine andere Webseite gibt, auf der man regelmäßig alle 5 Werte gleichzeitig sichtbar machen will, ohne dass gerade einer geändert wurde und so alle neu berechnet werden mussten. Dann müsste man immer sehr viel rechnen, um diese Seite bloß anzuzeigen.

Vielleicht hat @Dead_Eye aber auch recht, und die Berechnungen werden die Geschwindigkeit der Anzeige kaum beeinflussen. Jedenfalls reduziert das den Speicherplatz und die Komplexität des Codes.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
noisefloor
User
Beiträge: 4185
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

Danke für die Antworten und die Diskussion. Ich bin am Ende doch beim 1. Ansatz mit der dataclass und eine __post_init__ geblieben. Das sich alle Werte ändern sollen, wenn man eine aktualisiert, wäre interessant und nice-to-have, aber aktuell muss die erzeugte Instanz in der Tat nicht änderbar sein. Die "lebt" auch nur eine Request lang in einer Django View Funktion und ist dann Geschichte.

@__blackjack__: pint sieht interessant aus, schaue ich mir bei Gelegenheit mal an.

Gruß, noisefloor
Antworten