Richtige/sinnvolle Verwendung von Property

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
Dennis89
User
Beiträge: 1578
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

ich habe mal wieder eine Frage zu "wie macht man es richtig". Aktuell habe ich folgende Struktur, hier als Minimalbeispiel:

Code: Alles auswählen

#!/usr/bin/env python
from attrs import define, field


@define
class Cylinder:
    double = field()
    area = field()
    hole_area = field()
    _volume = field(default=None)

    @property
    def volume(self):
        """
        `calculate_volume`have to be called once before you can access this property.
        :raise ValueError: If `calculate_volume`is not be called before volume-access.
        :return float: volume in cubic-meter
        """
        if self._volume is None:
            raise ValueError(
                "Stroke-volume have to be calculated first. Call `calculate_stroke_volume`."
            )
        return self._volume

    def calculate_volume(self, length):
        """
        Depending on double acting the volume will be calculated.
        :param int|float length: In meter
        """
        area = self.area - self.hole_area if self.double else self.area
        self._volume = area * length


@define
class Machine:
    cylinders: list[Cylinder] = field()
    length = field()

    def __attrs_post_init__(self):
        for cylinder in self.cylinders:
            cylinder.calculate_volume(self.length)


def main():
    machine = Machine([Cylinder(100, 20, False)], 150)
    for cylinder in machine.cylinders:
        print(cylinder.volume)


if __name__ == "__main__":
    main()

Macht das mit `property` hier Sinn? Ich finde es ganz geschickt, weil ich sicherstellen kann, dass der Wert berechnet wird. An sich könnte ich das Ergebnis von `calculate_volume` auch direkt an das Attribut `volume` der `Cylinder` - Klasse binden, weil ich durch `__attrs_post_init` sicherstelle, dass die Methode aufgerufen wird.
Wie wäre hier der richtige Weg?

Die Attribute sind den Klassen so zugeordnet, wie sie auch in "echt" sind. Es wäre zum Beispiel technisch nicht richtig wenn ich `length` dem Zylinder zuweise und würde dann das physische Objekt nicht richtig wieder spiegeln.


Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18291
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: das macht doch keinen Sinn. Entweder hat ein Cylinder eine Länge und damit ein Volumen, oder aber das Volumen kann nur berechnet werden, wenn die Länge auch übergeben wird.
Das Konstrukt, nachträglich ein Attribut zu setzen, um dann wieder einen Wert abzufragen, ist sehr fehleranfällig. Was soll passieren, wenn sich die Länge ändert?

Code: Alles auswählen

from attrs import define, field


@define
class Cylinder:
    area = field()
    hole_area = field()
    double = field()

    def calculate_volume(self, length):
        """
        Depending on double acting the volume will be calculated.
        :param int|float length: In meter
        """
        area = self.area - self.hole_area if self.double else self.area
        return area * length


@define
class Machine:
    cylinders = field()
    length = field()


def main():
    machine = Machine([Cylinder(100, 20, False)], 150)
    for cylinder in machine.cylinders:
        print(cylinder.calculate_volume(machine.length))


if __name__ == "__main__":
    main()
Benutzeravatar
Dennis89
User
Beiträge: 1578
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die Antwort.

Die Länge die der Zylinder hat, bzw. die Länge die für das zu berechneten Volumen benutzt wird hängt in diesem Fall von der Maschine ab. Wenn man das mal auf ein Verbrennungsmotor bezieht, dann wäre die Länge gleich des Hubs der Kurbelwelle. Da kann der Zylinder auch viel längerer sein, ich würde trotzdem das Volumen berechnen wollen, dass durch den Hub entsteht. (Rein theoretische Betrachtung)

Die Länge wird in meinem Fall übergeben und dann das Volumen berechnet. Die Länge ist fix, die wird sich während des Programmablaufs nicht ändern. Ursprünglich hatte ich das ähnlich wie du, da ich das berechnete Volumen öfters benötige und da das konstant ist, wollte ich mir wiederholte Funktionsaufrufe oder das herumreichen von Namen sparen und bin dann erst dazu übergegangen, dass ich nachträglich ein Attribut setze. Dann dachte ich, wenn das aber nicht gesetzt wird, dann bricht mein Programm ab und bin deswegen zu `property` übergegangen um eine Ausnahme mit gewünschter Info auslösen zu können.


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18291
Registriert: Sonntag 21. Oktober 2012, 17:20

Nach dem Initialisieren muss ein Objekt vollständig sein. Du hast also entweder einen Zylinder ohne Länge und Volumen und berechnen das Volumen dynamisch, oder du hast einen Zylinder mit Länge oder Volumen. Umsetzen läßt sich das über zwei Klassen, einen Zylinder ohne Maschinenbezug, der dann innerhalb der Maschinenklasse in ein Zylinderobjekt mit Maschinenbezug umgewandelt wird.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1254
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Ich würde festlegen, welche Größen von Bedeutung sind, z.B. der Durchmesser und die Länge. Das Volumen wird berechnet.
Wenn man nun eine Instanz aus Volumen und Länge erzeugen möchte, eignet sich dafür eine Klassenmethode als alternativer Konstruktor, wo dann eine Instanz mit Durchmesser und Länge erstellt wird.
Das Gleiche kann man nochmal für Volumen und Durchmesser machen. Da dann die Länge berechnen und eine Instanz mit Durchmesser und Länge zurückgeben.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
Dennis89
User
Beiträge: 1578
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für eure weiteren Antworten.
Sirius3 hat geschrieben: Donnerstag 28. August 2025, 10:35 Umsetzen läßt sich das über zwei Klassen, einen Zylinder ohne Maschinenbezug, der dann innerhalb der Maschinenklasse in ein Zylinderobjekt mit Maschinenbezug umgewandelt wird.
Ich erstelle also eine Cylinder-Klasse, bei der nach der Instanzierug alle Attributte gesetzt sind. Dann übergebe ich diese Klasse an die Maschinen-Klasse, wenn die insatanziert wird und dann? Wie wandle ich die Cylinder-Klasse in eine Cylinder-Klasse mit Maschinenbezug um? Ich stolpere vermutlich über die Beschreibung "mit Maschinenbezug". Ich muss nachträglich eine Eigenschaft oder ein Attribut hinzufügen, dass das berechnete Volumen beinhaltet? Da habe ich gerade einen Knoten im Kopf.

DeaD_EyE hat geschrieben: Donnerstag 28. August 2025, 11:34 eignet sich dafür eine Klassenmethode als alternativer Konstruktor,
Dann hätte ich allerdings wieder eine Cylinder-Klasse die erst vollständig ist, nach dem die Maschinenklasse instanziert ist? Das würde im Prinzip meine `__attrs_post_init`ersetzen, wenn ich dich richtig verstehe?


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18291
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: effektiv hast Du das doch: ohne Maschine ist ein Zylinder unvollständig. Das solltest Du nicht verschleiern. Es fehlt noch, wie Du das ganze wirklich benutzen möchtest, vielleicht wird dann die Lösung klarer.
Benutzeravatar
Dennis89
User
Beiträge: 1578
Registriert: Freitag 11. Dezember 2020, 15:13

Ja, ich kann die Zylinder-Klasse nicht für sich selbst nutzen, das sehe ich ein.

Grund für das Konstrukt ist, das ich eine Gesamtmaschine nachbilden will. Die besteht hier vereinfacht gesagt aus einer Maschine und mehreren Zylindern. Dabei ist die Maschine und die Zylinder so konstruiert, dass unterschiedliche Zylindergrößen auf die Maschine montiert werden können. Dadurch kann man unterschiedliche Kundenanforderungen gerecht werden. Ich weiß jetzt zum Beispiel was der Kunde der Maschine zuführt und was er für ein Ergebnis erwartet. Um das rechnerisch nachzuweisen benötigt die Rechnung Parameter die von der Maschine gegeben sind und welche die vom Zylinder gegeben sind.
Die ausschlagebende Rechnung ist von der Art her wie die, die wir hier durchgesprochen hatten:
viewtopic.php?p=434622#p434622
(Ich habe noch keine Rückmeldung geben können, da ich jetzt erst wieder daran arbeite und erst ein ordentliches Code-Design haben möchte.)

Die ganzen Parameter, die die zwei Klassen benötigen, kommen im JSON-Format und da sich damit und mit `cattrs.structure` die Klassen schön instanzieren lassen und da das Design so der Realität am nächsten kommt, habe ich es so gewählt.
Die Berechnung soll, wenn sie fertig ist, eine API für eine Web-Anwendung sein. Die Web-Anwendung kommt nicht von mir, ich soll die Logik/Berechnung liefern und dafür sorgen, dass das Design so ist, das zeitgemäß Daten übertragen werden und zurückgegeben werden.

Dann haben die Zylindern auch noch Komponenten, die ausgetauscht werden und auch wieder Einfluss auf das Ergebnis haben. Ich muss das so modular wie möglich aufbauen, weil die Berechnung Schritt für Schritt erweitert wird und daher ist es mir jetzt zu Beginn sehr wichtig, dass die Struktur stimmt. Ich hoffe die Beschreibung hilft weiter. Ich weiß Code wäre verständlicher, den darf ich nur nicht veröffentlichen. Ich brauche während manchen Berechnungen Werte aus Maschine und den einzelnen Zylinder. Also beispielsweise immer die Drehzahl der Maschine und die verrechne ich dann mit ein paar Werten aus den Zylindern, suche das Maximum aus und schreibe das in eine Ergebnis-Klasse.

Hilf das stark vereinfachte Beispiel?

Code: Alles auswählen

#!/usr/bin/env python
from attrs import define, field, make_class


Results = make_class(
    "Results",
    ["max_volume"],
)


@define
class Cylinder:
    double = field()
    area = field()
    hole_area = field()
    _volume = field(default=None)

    @property
    def volume(self):
        """
        `calculate_volume`have to be called once before you can access this property.
        :raise ValueError: If `calculate_volume`is not be called before volume-access.
        :return float: volume in cubic-meter
        """
        if self._volume is None:
            raise ValueError(
                "Stroke-volume have to be calculated first. Call `calculate_stroke_volume`."
            )
        return self._volume

    def calculate_volume(self, length):
        """
        Depending on double acting the volume will be calculated.
        :param int|float length: In meter
        """
        area = self.area - self.hole_area if self.double else self.area
        self._volume = area * length


@define
class Machine:
    cylinders: list[Cylinder] = field()
    length = field()
    speed = field()

    def __attrs_post_init__(self):
        for cylinder in self.cylinders:
            cylinder.calculate_volume(self.length)


def main():
    machine = Machine([Cylinder(100, 20, False), Cylinder(100, 20, True)], 150, 600)
    volume_flows = [machine.speed * cylinder.volume for cylinder in machine.cylinders]
    print(Results(max(volume_flows)))


if __name__ == "__main__":
    main()
Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
DeaD_EyE
User
Beiträge: 1254
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Beispiel mit alternativen Konstruktoren:

Code: Alles auswählen

import math


class Cylinder:
    def __init__(self, diameter, height):
        self.diameter = diameter
        self.height = height

    @property
    def volume(self):
        return (self.diameter / 2)**2 * math.pi * self.height

    @classmethod
    def from_volume_diameter(cls, volume, diameter):
        g = (diameter / 2) ** 2 * math.pi
        h = volume / g
        return cls(diameter, h)
    
    @classmethod
    def from_volume_height(cls, volume, height):
        g = volume / height
        diameter = math.sqrt(g / math.pi) * 2
        return cls(diameter, height)        


z1 = Cylinder(5, 10)
print(f"{z1.diameter=} {z1.height=} {z1.volume=}")

z2 = Cylinder.from_volume_diameter(196.3495, 5)
print(f"{z2.diameter=} {z2.height=} {z2.volume=}")

z3 = Cylinder.from_volume_height(196.3495, 10)
print(f"{z3.diameter=} {z3.height=} {z3.volume=}")

sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Sirius3
User
Beiträge: 18291
Registriert: Sonntag 21. Oktober 2012, 17:20

@Dennis89: alles was Du bisher gezeigt hast, greift immer über machine auf cylinders zu, so dass es kein Problem ist calculate_volumes aufzurufen.
Idealerweise ist volume_flows eine Methode von Machine.
mechanicalStore
User
Beiträge: 180
Registriert: Dienstag 29. Dezember 2009, 00:09

@Dennis89
Wenn Du die Werte der Maschine berechnen willst, brauchst Du doch ohnehin noch andere Bauteile. Spendiere der Maschine eine Kurbelwelle, in der Z.b. auch der Achsenversatz definiert ist. Den gibst du den Zylindern beim Erzeugen als Referenz mit und kannst ab dann immer das passende Volumen abrufen. Die Zylinderlänge ist in dem Moment dann nicht mehr relevant. Alternativ, wie Sirius schon sagt, kannst du alle Werte dann auch in der Maschinenklasse abgreifen und berechnen.
Benutzeravatar
Dennis89
User
Beiträge: 1578
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für eure Antworten und die Beispiele.

@Sirius3 Ein Problem stellt der Aufruf nicht dar. Ich wollte nur wiederholte Funktionsaufrufe vermeiden oder unnötig viele Namen herumreichen und fand es daher gut, wenn ich einmal rechne und das Ergebnis in der Klasse ablege und nur noch abfragen muss und das wollte ich möglichst richtig machen. Soll ich ein weiteres Objekt, so was wie "Zwischenergebnisse" erstellen? Das klingt allerdings auch leicht schräg.

@mechanicalStore die Zylinderlänge war an sich noch nie relevant, ich habe es weiter oben beschrieben, dass das in einem vergleichbaren Beispiel eher mit dem Hub einer Kurbelwelle zu vergleichen ist. Es gehören nicht alle Werte in die Maschinenklasse. Wenn ich bspw. die Temperatur in einem Zylinder berechne, dann ist das kein Wert der Maschine, sondern einen für jeden Zylinder. Wenn ich die benötigte Leistung berechnen will, dann wäre das in der Maschine richtig. Das Problem ist nicht, das ich nicht weiß wie und von wo ich auf die Werte zugreifen/berechnen kann, sondern welches der richtige Weg ist um den Code modular zu halten, den Code so sauber wie möglich, ohne unnöig viele "Zwischennamen", die nur verwirren, zu halten.
Ich weiß das so Fragen ohne realen Code schwer zu beantworten sind. In dem Fall tue ich mir echt schwer irgendetwas vergleichbares zu finden.

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

@Dennis89: Falls Du Dir Gedanken um die Rechenzeit beim neuberechnen machst, wo sich die Länge selten bis nie ändert, kannst Du den Wert ja cachen. Siehe `functools.cache()` oder `functools.lru_cache()`.
“It is easier to change the specification to fit the program than vice versa.” — Alan J. Perlis
Benutzeravatar
Dennis89
User
Beiträge: 1578
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für den Tipp.
Performance ist tatsächlich auch ein Punkt den ich noch betrachten muss. Deswegen wurde damals auch das Thread, das ich oben verlinkt hatte, erstellt. Dann mache ich am Montag mit dem Vorschlag von Sirius3 in Verbindung mit `functools.chache()` weiter.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
pillmuncher
User
Beiträge: 1531
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@Dennis89: Mir ist nicht recht klar was du tun möchtest, was die Ausgangsdaten sind, und was das Ergebnis sein soll.

Du schreibst, dass du die Ausgangsdaten als JSON bekommst. Ist das eine Beschreibung der Maschine als ganzer, inklusive aller Komponenten? Oder eine Beschreibung der Komponenten als solcher, die du dann zur gewünschten Maschine zusammenbauen must? Du schreibst auch, dass in eine Maschine verschiedene Zylinder eingebaut werden können. Sind das verschiedene Zylinder, die als Alternativen zueinander eingebaut werden können, oder kann eine Maschine mehrere Zylinder gleichzeitig haben? Du schreibst auch, dass die Zylinder ebenfalls aus Komponenten bestehen, die dann einen Einfluss auf das Gesamtergebnis haben. Ist das orthogonal zur Maschine? Damit meine ich: ist es für die Maschine geometrisch egal, aus welchen Komponenten die Zylinder bestehen, oder führt das zusätzliche Beschränkungen mit sich, also dass ein Zylinder mit dan Maßen uvw nur eingebaut werden kann, wenn er die komponenten ABC hat, aber nicht die Komponenten DEF? Das würde bedeuten, dass du Validierungen durchführen musst. Die einfachste Validierung ist: Habe ich alle benötigten Daten zur Durchführung meiner Bereschnungen? Oder komplexere Validierungen wie: Sind die Eingangsdaten miteinander kompatibel, oder bekomme ich Werte die nicht passen, etwa Zylindermaße, die gar nicht auf die Maschine passen? Oder kannst du das einfach ignorieren, weil der Erzeuger der Daten dafür zuständig ist?

Die Idee, zuerst einmal die Komponentenklassen mittels cattrs zu erzeugen, halte ich für richtig. Danach stellt sich aber die Frage: Sind das die Klassen, die auch das Endergebnis bescheiben, oder sind die mit cattrs erzeugten Klassen eine intermediäre Representation der Rohdaten, die man dann zur Konstruktion der eigentlichen Komponenten verwendet? Muss die Gesamtmaschine alle geometrischnen Eigenschaften beinhalten, also nicht nur die Maße der Komponenten, sondern auch deren geometrische Position zueinander? Anhand dessen was du bisher geschrieben hast, brauchst du das anscheinend nicht, sondern die Gesamtmaschine ist nur ein Berechnungsobjekt, ähnlich einem Spreadsheet. Damit stellt sich die Frage: Muss diese Gesamtmaschine wirklich die Maschinen- und Komponentenstruktur 1:1 widerspiegeln, oder kann man sie tatsächlich als Spreadsheet betrachten, das aus Repräsentation von Berechnungen besteht?

Generell kann man das auch als Algebra betrachten. Eine Gesamtmaschine besteht aus verschiedenen Komponenten, wir zB. Zylindern. Wenn jede Maschine genau einen Zylinder hat, der verschiedene Maße haben kann, dann hat die diese Struktur: Maschine ⨯ Zylinder. Hat sie zwei Zylinder gleichzeitig, hat sie diese Struktur: Maschine ⨯ Zylinder ⨯ Zylinder. Müssen die Zylinder dieselben Maße und Subkomponenten haben, dann hat man: Maschine ⨯ (2 * Zylinder).

Das lässt verschiedene Implementationen zu. Etwa:

Code: Alles auswählen

from typing import Self

class MachineCalculator:
    ...
    def add_cylinder(self, cylinder:Cylinder) -> Self:
        ...
    def add_XYZ(self, xyz:XYZ) -> Self:
        ...
...        
calculator = MaschineCalculator().add_cylinder(cylinder).add_XYZ(xyz)
result = calculator.calculate_something()
Oder auch:

Code: Alles auswählen

class MachineCalculator:
    ...
    def __add__(self, component:Component) -> Self:
        match component:
            case Cylinder(height=height, diameter=diameter):
                ...
            case XYZ(x=x, y=y, z=z):
                ...
...
calculator = MaschineCalculator() + cylinder + xyz
result = calculator.calculate_something()
Oder, falls die Maschine mehrere Zylinder haben kann, die dieselbem Maße haben müssen:

Code: Alles auswählen

from toolz.functoolz import flip

class Cylinder:
    ...
    def __mult__(self, n:int): tuple[Self]:
        return (self,) * n
        
    __rmult__ = flip(__mult__)
    ...
    
class MachineCalculator:
    ...
    def __add__(self, component:Component|tuple[Component]) -> Self:
        match component:
            case Component() as component, *components
                if not components:
                    return self + component
                return self + component + components
            ...
...
calculator = MaschineCalculator() + 2 * cylinder + xyz
result = calculator.calculate_something()
Die type annotations hier dienen zur Illustration, ich sage nicht, dass du sie verwenden musst.

Die Ergebnisse der Methoden in MachineCalculator oben sind alle vom Typ Self, also MachineCalculator. Das lässt zwei Implementationen zu: Enweder self wird mutiert, oder es wird eine mutierte Kopie zurückgegeben. Letzteres ist idR. die übersichtlichere Variante und angesichts der geringen Datenmengen hier vorzuziehen.

Mein Punkt hier ist dieser: Zuerst muss man sich die Struktur des Problems bewusst machen und eine angemessene Form finden, diese im Code auszudrücken. Die Struktur ist in deinem Fall eher algebraisch als geometrisch. Du willst ja kein CAD Programm bauen, sondern möchtest Berechnungen durchführen. Um die Performance brauchst du dich erst zu kümmern, wenn das ausprogrammiert ist und korrekt arbeitet. Wie Kent Beck immer sagt: First make it work. Then make it right. Then make it fast.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
Dennis89
User
Beiträge: 1578
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen und danke für die Antwort.

ich versuche deine Fragen in der gleichen Reihenfolge zu beantworten.

Die Daten die das Programm bekommt beschreiben die Gesamtemaschine.Allerdings wird das JSON-Format gleich wie das Klassen-Design aufgebaut, so dass auch hier zusammengehörige Komponenten übersichtlich angeordnet sind. Die Gesamtmaschine kann mehrere Zylinder haben und das Codedesign sollte die mögliche Anzahl an Zylinder nicht einschränken. Einer muss es zumindest immer sein. Die Daten die übergeben werden passen geometrisch zusammen, darum muss ich mich nicht kümmern. Abgesehen von den Komponenten, benötigt man noch ein paar Betriebsbedingungen, diese Daten muss ich prüfen. Da bietet `attrs` ja `validator` an, aber das ist für die Programmstruktur erst mal zweitrangig, würde ich behaupten.

Ja die Maschine ist ein reines Berechnungsobjekt. Mein Gedanke zur 1:1 Darstellung war der, damit ich nach der Berechnung eine theoretische vollständige Maschine habe. Hier kann dann jede Abteilung die benötigten Infos abgreifen. Die einen können die verbauten Komponenten bestellen, die anderen sehen was sie um die Maschine darum herum bauen müssen, bei Grenzfällen kann man sich mögliche Zwischenergebnisse anschauen und ich kann das Programm so Stück für Stück verfeinern. Allerdings habe ich bei dem Wort "Spreadsheet" so etwas wie eine Tabellenkalkulation im Kopf, typischerweise Excel. Hast du das gemeint? Ich hatte in einem Beitrag mal erwähnt, dass das ein API für eine Webanwendung werden soll.

Die Beispiele spiele ich am Montag mal durch, danke dafür.

Zu deinem letzten Absatz, da stimme ich dir ganz zu, so würde ich es auch gerne machen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten