Ist die Programmierung dieser Klasse in Ordnung?

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.
Master_Shredder
User
Beiträge: 18
Registriert: Freitag 2. Februar 2024, 18:25

Hi zusammen,

ich würde gerne mal wissen ob ich diese Klasse so korrekt geschrieben habe?
Speziell auch mit dem Datenbank Aufruf im Konstruktor. Dies ist ja eigentlich nur ein Attribute anlegen.

Hier die Klasse:

Code: Alles auswählen

import math
import sqlite3
from datetime import datetime


class Abrechnung:
    """ Klasse Abrechnung. Hier befinden sich die Methoden zur Berechnung der Verbrauchsdaten"""

    def __init__(self):
        # Verbindung zur Datenbank herstellen
        verbindung = sqlite3.connect("Datenbank/verbrauch.db")
        zeiger = verbindung.cursor()

        # Datenbank Abfrage
        zeiger.execute("SELECT zaehlerstand_letzte_abrechnung FROM tarif")
        self.zaehlerstand_letzte_abrechnung = zeiger.fetchone()[0]

        zeiger.execute("SELECT arbeitspreis_netto FROM tarif")
        self.arbeitspreis = zeiger.fetchone()[0]

        zeiger.execute("SELECT grundpreis_netto FROM tarif")
        self.grundpreis_netto = zeiger.fetchone()[0]

        zeiger.execute("SELECT mehrwertsteuer FROM tarif")
        self.mehrwertsteuer = zeiger.fetchone()[0]

        zeiger.execute("SELECT Vertragsbeginn FROM Vertragsdaten")
        self.vertragsbeginn = zeiger.fetchone()[0]

        zeiger.execute("SELECT zu_zahlender_abschlag FROM tarif")
        self.zu_zahlender_abschlag = zeiger.fetchone()[0]

        verbindung.close()

        self.akt_zaehlerstand = 0
        self.arbeitspreis_netto = 0
        self.arbeitspreis_brutto = 0
        self.grundpreis_tag_brutto = 0
        self.grundpreis_tag_netto = 0
        self.gesamtbetrag_netto = 0
        self.gesamtbetrag_brutto = 0
        self.gezahlter_abschlag = 0
        self.gez_abschlag_ej = 0
        self.gp_abz_ablschlag_bj = 0
        self.gp_abz_abschlag_ej = 0

    # Abrechnungen
    # Ermittlung aktueller Zaehlerstand
    def ber_akt_zaehlerstand(self, eingabe):
        self.akt_zaehlerstand = int(eingabe) - self.zaehlerstand_letzte_abrechnung

    # Berechnung Arbeitspreis
    def ber_arbeitspreis_netto(self):
        self.arbeitspreis_netto = round(self.akt_zaehlerstand * self.arbeitspreis / 100, 2)

    def ber_arbeitspreis_brutto(self):
        self.arbeitspreis_brutto = round(
            self.akt_zaehlerstand * self.arbeitspreis / 100 * self.mehrwertsteuer, 2)

    # Berechnung Grundpreis
    def ber_grundpreis_brutto(self):
        self.grundpreis_tag_brutto = round(self.grundpreis_tag_netto * self.mehrwertsteuer, 2)
        
    ...........
Hier geht es jetzt mit so ähnlichen Methoden weiter.

Die Methoden werden dann in dieser Klasse aufgerufen und in einer GUI ausgegeben:

Code: Alles auswählen

..............

class Verbrauchsrechner(QtWidgets.QMainWindow):
    """Main Klasse hier startet das Programm"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Hauptdialog()
        self.ui.setupUi(self)
        # Slot eingerichtet
        self.ui.button_OK.clicked.connect(self.berechne)

    # Slot
    def berechne(self):
        berechnung = Abrechnung()
        berechnung.ber_akt_zaehlerstand(self.ui.spin_box_eingabe.value())
        berechnung.ber_arbeitspreis_netto()
        self.ui.arbeitspreis_netto.setText(str(berechnung.arbeitspreis_netto) + " €")
        berechnung.ber_arbeitspreis_brutto()
        self.ui.arbeitspreis_brutto.setText(str(berechnung.arbeitspreis_brutto) + " €")
        berechnung.ver_grundgebuer()
        self.ui.grundpreis_netto.setText(str(berechnung.grundpreis_tag_netto) + " €")
        berechnung.ber_grundpreis_brutto()
        self.ui.grundpreis_brutto.setText(str(berechnung.grundpreis_tag_brutto) + " €")
        berechnung.ber_gesamtbetrag_netto()
        self.ui.gesamtpreis_netto.setText(str(berechnung.gesamtbetrag_netto) + " €")
        berechnung.ber_gesamtbetrag_brutto()
        self.ui.gesamtpreis_brutto.setText(str(berechnung.gesamtbetrag_brutto) + " €")
        berechnung.er_abschlag()
        self.ui.abschlag_bis_jetzt.setText(str(berechnung.gezahlter_abschlag) + " €")
        self.ui.abschlag_ende_des_jahres.setText(str(berechnung.gez_abschlag_ej) + " €")
        berechnung.ber_gp_abz_abschlag_bj()
        self.ui.gp_abz_abschlag_bj.setText(str(berechnung.gp_abz_ablschlag_bj) + " €")
        berechnung.ber_gp_abz_abschlag_ej()
        self.ui.gp_abz_abschlag_ej.setText(str(berechnung.gp_abz_abschlag_ej) + " €")

...................
Danke schon mal für die Hilfe

MFG Master_Shredder
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Master_Shredder: Das ist falsch, das ist so keine sinnvolle Klasse.

Und die Datenbank an sich sieht schon fragwürdig aus. So eine Abfrage hat keine Ordnung. Da kann bei dem `fetchone()` jeweils ein beliebiger Datensatz ausgewählt werden, das heisst keiner der Werte aus der `tarif`-Tabelle muss zu den anderen passen. Das würde so nur korrekt sein, wenn die beiden Tabellen immer nur genau einen Datensatz enthalten würden, wo dann die Frage ist warum das in einer Datenbank steht.

Wenn das eine sinnvolle Datenbank wäre, müsste man auch über einen Fremdschlüssel sicherstellen, dass die Werte aus der `tarif`-Tabelle zu dem Wert aus der `Vertragsdaten`-Tabelle passt.

Und man würde nicht jeden Wert aus `tarif` einzeln abfragen, sondern alle zusammengehörenden Werte in einer Abfrage zusammenfassen.

So etwas wie eine Datenbankabfrage würde ich auch nicht in der `__init__()` machen. Das macht es sehr schwer für diese Klasse Unit-Tests mit Beispieldaten zu schreiben, oder die Werte auch mal von woanders zu holen als aus einer SQLite-Datenbank. Die `__init__()` ist besser nur zum Initialisieren und ggf. Validieren von Attributen da. Wenn man mehr machen will/muss, bietet es sich an dafür eine `classmethod()` zu schreiben.

Den Datenbankdateinamen sollte man als Argument übergeben. Auch wieder damit das testbar wird.

Die ganzen Attribute die mit 0 initialisiert werden und dann später durch einzelne Methodenaufrufe dann erst den tatsächlichen Wert bekommen, sollte es nicht geben. Das ist fehleranfällig, weil man immer erst die dazugehörige Methode aufrufen muss, damit es einen sinnvollen Wert gibt, und Änderungen an den Ausgangsdaten nach der Berechnung lassen die Werte inkonsistent werden. Das wären vielleicht alles besser Properties (`property()`).

Es würde sich hier auch anbieten auf ein ORM wie SQLAlchemy aufzusetzen, bevor man anfängt sich so etwas selber zu schreiben, oder das alles ”zu Fuss” auszuformulieren.

Namen sollten keine kryptische Abkürzungen enthalten. So etwas wie `ber_gp_abz_abschlag_ej()` versteht doch keiner.

Das Zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

nochmal zur DB: der Code mag zufällig funktionieren, aber in der Form, wie du abfragst, hast du _keine_ garantierte Reihenfolge der Rückgabewerte aus der DB. D.h. `fetchone()[0]` liefert dir _nicht_ zwingend den neuesten Wert - aber wenn das vielleicht zufällig bei dir bis jetzt der Fall ist. Da muss dann noch eine `ORDER BY` Klausel hin, die die Rückgabewerte z.B. nach Datum sortieren würde (was im gegebenen Fall sinnvoll erscheint, dass jeder Eintrag in `tarif` ein Datum hat.

Abgesehen davon hast du unnötig viele Roundtrips durch die Datenbank - alle Abfragen gegen `tarif` kannst du ohne Probleme in eine Abfrage zusammenziehen.

Die GUI-Klasse ist IMHO komisch benannt - da wird ja offensichtlich nicht (nur) der Verbrauch berechnet, sondern auch irgendwas mit Kosten.

Bei der Abrechnung Klasse würde ich das Abholen der Daten auch in eine eigene Methode auslagern (wie z.B. `get_data_from_database` oder so ähnlich), die man explizit aufruft, um an die Daten zu kommen

Gruß, noisefloor
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Das ganze könnte so aussehen:

Code: Alles auswählen

DATABASE_NAME = "Datenbank/verbrauch.db"

class Abrechnung:
    """ Klasse Abrechnung. Hier befinden sich die Methoden zur Berechnung der Verbrauchsdaten"""

    def __init__(self, zaehlerstand_letzte_abrechnung, arbeitspreis_netto, grundpreis_netto, mehrwertsteuer, zu_zahlender_abschlag, Vertragsbeginn, aktueller_zaehlerstand):
        self.zaehlerstand_letzte_abrechnung = zaehlerstand_letzte_abrechnung
        self.arbeitspreis_netto = arbeitspreis_netto
        self.grundpreis_netto = grundpreis_netto
        self.mehrwertsteuer = mehrwertsteuer
        self.zu_zahlender_abschlag = zu_zahlender_abschlag
        self.vertragsbeginn = vertragsbeginn
        self.aktueller_zaehlerstand = aktueller_zaehlerstand

    @classmethod
    def from_database(cls, database, aktueller_zaehlerstand):
        with closing(database.cursor()) as cursor:
            cursor.execute("SELECT zaehlerstand_letzte_abrechnung, arbeitspreis_netto, grundpreis_netto, mehrwertsteuer, zu_zahlender_abschlag, Vertragsbeginn FROM tarif, Vertragsdaten WHERE Vertragsdaten.tarif_id = tarif.id AND bedingung_fuer_aktuellen_vertrag")
            zaehlerstand_letzte_abrechnung, arbeitspreis_netto, grundpreis_netto, mehrwertsteuer, zu_zahlender_abschlag, Vertragsbeginn = cursor.fetchone()
        return cls(zaehlerstand_letzte_abrechnung, arbeitspreis_netto, grundpreis_netto, mehrwertsteuer, zu_zahlender_abschlag, Vertragsbeginn, aktueller_zaehlerstand)

    @property
    def arbeitspreis_netto(self):
        return self.akt_zaehlerstand * self.arbeitspreis / 100

    @property
    def arbeitspreis_brutto(self):
        return self.arbeitspreis_netto * self.mehrwertsteuer

...

    def berechne(self):
        zaehlerstand = self.ui.spin_box_eingabe.value()
        with closing(sqlite3.connect(DATABASE_NAME)) as database:
            abrechnung = Abrechnung.from_database(database, zaehlerstand)
        self.ui.arbeitspreis_netto.setText(f"{abrechnung.arbeitspreis_netto:.2f} €")
        self.ui.arbeitspreis_brutto.setText(f"{abrechnung.arbeitspreis_brutto:.2f} €")
        ...
Master_Shredder
User
Beiträge: 18
Registriert: Freitag 2. Februar 2024, 18:25

Hi @all,

WOW, OK, interessant.

Also mit der Klassen Methode from_database() frage ich die Datenbank ab und gebe die Werte zurück und initialisiere damit die Attribute.

Die anderen Methoden sind eigentlich Property Getter die in der Rückgabe die Werte erst ermitteln. Interessant.

Und mit der "with" Anweisung rufe ich die Datenbank auf und stelle sicher, dass sie wieder geschlossen wird. Also eigentlich das ganze "connect" und "close" in einer Anweisung, cool.
Das Zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.
Ah ja. Das ist ja auch cool. Da habe ich gerade auch noch gerundet :D .
Es würde sich hier auch anbieten auf ein ORM wie SQLAlchemy aufzusetzen, bevor man anfängt sich so etwas selber zu schreiben, oder das alles ”zu Fuss” auszuformulieren.
OK, dies muss ich mir mal genau ansehen.
müsste man auch über einen Fremdschlüssel sicherstellen, dass die Werte aus der `tarif`-Tabelle zu dem Wert aus der `Vertragsdaten`-Tabelle passt.
K. sehe ich mir an.

Also die Datenbank ist da um die Vertragsdaten, Tarifdaten zu speichern auch bei Änderung. Und des weiteren soll sie später noch Daten bzw. Zählerstande, nach Wunsch, speichern um einen Überblick des Verbrauchs fürs ganze Jahr zu bekommen z.B. Strom.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Man spart sich viel Tipparbeit bei der __init__ Methode, wenn man eine dataclass verwendet.
Falls man etwas innerhalb der __init__ Methode machen muss, kann man die Methode __post_init__ verwenden.

Anmerkung: Man muss im eingerückten Block der Klassendefinition die Typen der Namen mit angeben.
Getrennt wird mit einem Doppelpunkt. Das heißt aber nicht, dass die Dataclass eine
automatische Typenkonvertirung vornimmt und es wird auch nicht überprüft, ob die Typen überhaupt stimmen.

Um sich noch mehr Schreibarbeit zu ersparen, kann könnte auch auf einen ORM zurückgreifen. SQLAlchemy ist ja bereits erwähnt worden. Die ORM checken auch die Datentypen und es ist garantiert, dass ein Fehler ausgelöst wird, wenn die Daten den falschen Datentyp haben. Abstraktion kostet immer Speicherplatz und etwas mehr Laufzeit. Wenn man z.B. vor hat das mit einem Raspberry Pi auf einer SD-Karte zu machen, dauert das importieren von SQLAlchemy mehr als eine Sekunde.

Code: Alles auswählen

from contextlib import closing
from datetime import date as Date

# in der Standardbibliothek gibt es einige Module,
# die sich nicht an die Namensgebungskonventionen halten.
# Macht aber nichts, kann man umbenennen
# date ist eine Klasse, also der erste Buchstabe Groß
from dataclasses import dataclass

# oftmals die einfachere Möglichkeit, vor allem dann, wenn
# man sehr viele Argumente für die Initialisierung der Instanz übergeben muss

@dataclass
class Abrechnung:
    """Klasse Abrechnung. Hier befinden sich die Methoden zur Berechnung der Verbrauchsdaten"""

    zaehlerstand_letzte_abrechnung: int
    grundpreis_netto: float
    mehrwertsteuer: float
    zu_zahlender_abschlag: int
    vertragsbeginn: Date
    aktueller_zaehlerstand: int

    @classmethod
    def from_database(cls, database, aktueller_zaehlerstand):
        with closing(database.cursor()) as cursor:
            # https://www.freeformatter.com/sql-formatter.html
            cursor.execute(
                """
                SELECT
                    zaehlerstand_letzte_abrechnung,
                    arbeitspreis_netto,
                    grundpreis_netto,
                    mehrwertsteuer,
                    zu_zahlender_abschlag,
                    vertragsbeginn 
                FROM
                    tarif,
                    vertragsdaten 
                WHERE
                    vertragsdaten.tarif_id = tarif.id 
                    AND bedingung_fuer_aktuellen_vertrag
                """
            )
            return cls(*cursor.fetchone())

    @property
    def arbeitspreis_netto(self):
        return self.aktueller_zaehlerstand * self.arbeitspreis / 100

    @property
    def arbeitspreis_brutto(self):
        return self.arbeitspreis_netto * self.mehrwertsteuer


def berechne(self):
    zaehlerstand = self.ui.spin_box_eingabe.value()

    with closing(sqlite3.connect(DATABASE_NAME)) as database:
        abrechnung = Abrechnung.from_database(database, zaehlerstand)

    self.ui.arbeitspreis_netto.setText(f"{abrechnung.arbeitspreis_netto:.2f} €")
    self.ui.arbeitspreis_brutto.setText(f"{abrechnung.arbeitspreis_brutto:.2f} €")
    ...

Hinweis: Ich verwende für die Formatierung ruff. Nach mehr als 10 Jahren kann man aber auch händisch Code schreiben, der durch Black/Ruff nicht mehr umformatiert wird.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Master_Shredder
User
Beiträge: 18
Registriert: Freitag 2. Februar 2024, 18:25

Hi @all,
Man spart sich viel Tipparbeit bei der __init__ Methode, wenn man eine dataclass verwendet.
Falls man etwas innerhalb der __init__ Methode machen muss, kann man die Methode __post_init__ verwenden.
@DeaD_EyE Ja danke, aber ich will es mir jetzt nicht noch komplizierter machen. Ich bin noch nicht all so gut und mache dies nur aus Hobby.
Und ich sollte ja auch alles verstehen was ich tue.

Aber nun ich habe ein anderes Problem. Ich habe mir mal SQLAlchemy ORM angesehen und finde es gut, es kann mir viel Arbeit sparen und ist besser wartbar.
Nur irgendwie bekomme ich es nicht zum arbeiten.

Code: Alles auswählen

from sqlalchemy import create_engine
from sqlalchemy.orm import Session


DATABASE_NAME = "Datenbank/testdatenbank.db"

engine = create_engine(DATABASE_NAME)
conn = engine.connect()



class Abrechnung:
    """ Klasse Abrechnung. Hier befinden sich die Methoden zur Berechnung der Verbrauchsdaten"""


    def from_database(self):
        with Session(engine) as session:
            person = session.get(Person, 1)
            print(person)
Ich habe auch ein Paar andere Methoden versucht. Abfragen über .query oder select
Aber irgendwie will es nicht. Es ist auch immer der Fall, dass meine IDE das Tabellen Argument nicht erkennt.
Und die Doc macht es mir auch nicht gerade leicht.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Master_Shredder: SQLAlchemy hat eine ziemlich gute Dokumentation. Man muss sich da halt einarbeiten.

Der Quelltext sieht irgendwie geraten aus. `conn` wird nirgends verwendet. `Person` ist nirgends definiert, das kann dann so natürlich nicht funktionieren. `engine` und `conn` sind auch keine Konstanten, sollten also nicht einfach so auf Modulebene als globale Variablen existieren.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Master_Shredder
User
Beiträge: 18
Registriert: Freitag 2. Februar 2024, 18:25

SQLAlchemy hat eine ziemlich gute Dokumentation.
Ja, das Glaube ich auch. Nur ein großes Problem ist das es Englisch ist, also nicht gerade ein einfaches.
Und mir ist kein, sage mal, direkter Weg ersichtlich.
`conn` wird nirgends verwendet.
Oh, ja das stimmt. Das ist wohl irgendwann bei den verschiedenen Versuchen untergegangen.

Also ich habe mir auch ein Paar Videos angesehen. Nur die beginnen da IMMER mit der Verbindung und schreiben dann die Tabelle. Und nicht machen einfach mal ein Paar Abfragen.
Da ist mir auch aufgefallen, dass SIE da eine Klasse schreiben die wie die Tabelle heißt, wie bei mir "Person".
Denke mal dann würde es vielleicht so funktionieren.

Aber dies brauche ich ja eigentlich alles nicht. Ich habe ja meine Tabellen und Spalten und möchte sie nur abfragen.
Benutzeravatar
snafu
User
Beiträge: 6743
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Die Doku von SQLAlchemy kann am Anfang tatsächlich etwas verwirrend sein, zumal nun auch die API in der Version 2.0 dazugekommen ist. Ganz grob gesagt erstellt man zuerst die Struktur für die Daten:

Code: Alles auswählen

from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

class User(Base):
    __tablename__ = "user_account"
    id = Column(Integer, primary_key=True)
    name = Column(String(30))
    fullname = Column(String)
    addresses = relationship(
        "Address", back_populates="user", cascade="all, delete-orphan"
    )
    def __repr__(self):
        return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

class Address(Base):
    __tablename__ = "address"
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey("user_account.id"), nullable=False)
    user = relationship("User", back_populates="addresses")
    def __repr__(self):
        return f"Address(id={self.id!r}, email_address={self.email_address!r})"
... dann erstellt man ein paar Objekte (normalerweise natürlich mit Nutzereingaben oder aus einer anderen Quelle):

Code: Alles auswählen

from sqlalchemy.orm import Session

with Session(engine) as session:
    spongebob = User(
        name="spongebob",
        fullname="Spongebob Squarepants",
        addresses=[Address(email_address="spongebob@sqlalchemy.org")],
    )
    sandy = User(
        name="sandy",
        fullname="Sandy Cheeks",
        addresses=[
            Address(email_address="sandy@sqlalchemy.org"),
            Address(email_address="sandy@squirrelpower.org"),
        ],
    )
    patrick = User(name="patrick", fullname="Patrick Star")
    session.add_all([spongebob, sandy, patrick])
    session.commit()
Und dann macht man die gewünschten Abfragen:

Code: Alles auswählen

from sqlalchemy import select

session = Session(engine)

stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))

for user in session.scalars(stmt):
    print(user)
Alle Code-Beispiele stammen von hier:
https://docs.sqlalchemy.org/en/14/orm/quickstart.html

Das kann ich auch zum Nachlesen wärmstens empfehlen.

Und für die API 2.0:
https://docs.sqlalchemy.org/en/20/orm/quickstart.html

Letzteres ist für mich ehrlich gesagt auch noch Neuland. Ich finde die alte API einfacher. Wird aber wohl alles seinen Sinn haben und man sollte sich die auch zeitnah aneignen.
Benutzeravatar
grubenfox
User
Beiträge: 432
Registriert: Freitag 2. Dezember 2022, 15:49

Master_Shredder hat geschrieben: Donnerstag 8. Februar 2024, 19:11 Aber dies brauche ich ja eigentlich alles nicht. Ich habe ja meine Tabellen und Spalten und möchte sie nur abfragen.
Der interessante Part in der Doku beginnt wohl hier:
https://docs.sqlalchemy.org/en/20/core/reflection.html
https://docs.sqlalchemy.org/en/14/core/reflection.html

Oberflächlich betrachtet (nur die ersten 2-3 Absätze angeschaut) scheint sich da zwischen den Versionen nichts geändert zu haben.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich persönlich schreibe die Klassen lieber selbst. Es ist dann auch gleichzeitig Dokumentatation und ”Behälter” für Dokumentation (DocStrings) und man kann den Klassen noch Properties und Methoden verpassen. Und bei bereits bestehenden Datenbanken auch mal Namen ”korrigieren”.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Master_Shredder
User
Beiträge: 18
Registriert: Freitag 2. Februar 2024, 18:25

Jaa, ich habe mal etwas hinbekommen. Ich habe es jetzt mal nach einem Video Tutorial gemacht. https://piped.kavin.rocks/playlist?list ... rqOeieFRWn

Also es funktioniert schon mal. Hoffe dies kann man grundsätzlich so verwenden?

Also ich habe eine extra Klasse für die "Verbindung" erstellt. Ich denke mal, dass ist was @ __blackjack__ meinte.
Persönlich finde ich sie auch nicht schlecht. Und ich kann mir auch gut vorstellen das sie auch die von @ __blackjack__ erwähnten Vorteile hat.

Datenbankverbindung.py:

Code: Alles auswählen

from sqlalchemy import create_engine, Column
from sqlalchemy.orm import declarative_base
from sqlalchemy.sql.sqltypes import Integer, Float

engine = create_engine('sqlite:///Datenbank/verbrauch.db')
base = declarative_base()
conn = engine.connect()


class Tarif(base):
    __tablename__ = 'tarif'

    tarif_id = Column(Integer, primary_key=True, autoincrement=True)
    arbeitspreis = Column(Float, nullable=False)
    grundpreis = Column(Float, nullable=False)
    mehrwertsteuer = Column(Float, nullable=False)
    zaehlerstand_letzte_abrechnung = Column(Float, nullable=False)
    zu_zahlender_abschlag = Column(Integer, nullable=False)

    def __init__(self, arbeitspreis: float, grundpreis: float, mehrwertsteuer: float,
                 zaehlerstand_letzte_abrechnung: float, zu_zahlender_abschlag: float):
        self.arbeitspreis = arbeitspreis
        self.grundpreis = grundpreis
        self.mehrwertsteuer = mehrwertsteuer
        self.zaehlerstand_letzte_abrechnung = zaehlerstand_letzte_abrechnung
        self.zu_zahlender_abschlag = zu_zahlender_abschlag


base.metadata.create_all(conn)
Hier habe ich dann die Klasse Abrechnung:

Code: Alles auswählen

from sqlalchemy.orm import Session
from Datenbankverbindung import Tarif, engine


class Abrechnung:
    """ Klasse Abrechnung. Hier befinden sich die Methoden zur Berechnung der Verbrauchsdaten"""

    def __init__(self, arbeitspreis, grundpreis, mehrwertsteuer):
        self.arbeitspreis = arbeitspreis
        self.grundpreis = grundpreis
        self.mehrwertsteuer = mehrwertsteuer

    @classmethod
    def from_database(cls, eingegebener_zaehlerstand):
        my_session = Session(bind=engine)
        tarif = my_session.get(Tarif, 1)
        return cls(tarif.arbeitspreis,
                   tarif.grundpreis, tarif.mehrwertsteuer)
Ja die Doc von SQLAlchemy ist etwas kompliziert geschrieben. Es ist so eine Puzzle Doc, ja da brauch man erst mal Durchblick. Das ist bei mir wie wenn ich in die Java Doc schaue, da bräuchte ich auch gar keine, da verstehe ich nichts :? . Muss mal einen Anfang bekommen, dann wird es wohl gehen. Also wie ich das hier nach dem Video Tutorial gemacht habe war es echt easy. Aber ja wie ich jetzt wieser gemerkt habe, gibt es jetzt da auch noch eine neuere Version. :ugeek:
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Master_Shredder: Die `Tarif.__init__()` ist überflüssig. Die Klasse erbt bereits eine sinvolle `__init__()`.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
snafu
User
Beiträge: 6743
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Das ist überhaupt sehr wirsch umgesetzt. Warum enthält der Tarif so Dinge wie den letzten Zählerstand und den Abschlag? Warum dupliziert die Abrechnung nochmal die Hälfte der Tarif-Attribute? Das sollten besser Properties der Abrechnung sein, wo der zu zahlende Abschlag sowie Guthaben/Nachzahlung sich aus den Angaben zum Tarif in Abhängigkeit vom Verbrauch ergeben. Auch die Umsatzsteuer würde man doch normalerweise erst in der eigentlichen Rechnung ermitteln.

Wenn man Attribute aus einer anderen Tabelle übernehmen möchte, dann macht man das übrigens mit einer Relation (in SQLAlchemy: ``relationship()``). Ich verstehe, dass das am Anfang vielleicht noch nicht alles so optimal sitzt, aber insbesondere das Design der beiden eingangs angesprochenen Klassen würde ich mal stark überdenken...
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

zum Tabellendesign: beim Tarif fehlt IMHO das Datum, ab wann dieser gilt. Und ggf. ein Enddatum, ab wann er nicht mehr gilt (gegolten hat). Warum ist der Abschlag ein Interger-Wert? Es ist IMHO nicht kategorisch gegeben, dass der Abschlag eine glatte Zahl sein muss. AFAIK war die Mehrwehrsteuer bis dato immer ein Integerwert - muss natürlich nicht bis in allen Ewigkeit so bleiben. Der Zählerstand sollte IMHO mit Datum in einen eigene Tabelle und dann ggf. noch zu jedem Eintrag ein Fremdschlüselbezug zum für diesen Wert gültigen Tarif.

Gruß, noisefloor
Master_Shredder
User
Beiträge: 18
Registriert: Freitag 2. Februar 2024, 18:25

Die `Tarif.__init__()` ist überflüssig. Die Klasse erbt bereits eine sinvolle `__init__()`.
Ah ok. Danke @__blackjack__

@die Frage warum ab und an etwas fehlt.
Ich weiß, dass ab und etwas fehlt oder so vielleicht nicht ganz korrekt ist.
Erstens kann ich mir nicht denken wie das Programm letzten Endes aussieht, jedenfalls nicht so, dass ich nach meiner Erfahrung weiß, dass es gleich so sein sollte.
Und Zweitens will ich mir nicht einen Haufen Baustellen reinbauen die dann auch nie fertig werden. Ja da fehlt auch noch der Versorger Name und und und.
Und ja will die Tabellen noch klein halten und es erst mal mit denen hinbekommen.

Ich arbeite ab und an mal daran, gerade mal etwas mehr. Dies ist ein Bastelobjekt auch noch für die Zukunft. Muss noch Erfahrung sammeln.
Warum ist der Abschlag ein Interger-Wert? Es ist IMHO nicht kategorisch gegeben
Ja, da gebe ich dir recht :!: guter Hinweis. Das war eigentlich weil ich immer Glatte Beträge zahle :D .

@snaf
Warum dupliziert die Abrechnung nochmal die Hälfte der Tarif-Attribute?
Wie meinst du das? Wo dupliziere ich sie?


Nachwort: Lieber klein und fein, wie groß, Fett und totales Chaos.
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

die Denke ist IMHO nicht richtig, wenn es um das Tabellendesign geht. Klar kann man evtl. jetzt noch nicht wissen, wo das ganze in X Jahren steht. Aber wenn man jetzt schon Fehler macht, im Sinne von keine Relationen einbauen wo welche hin gehören (z.B. indem der Zählerstand nicht direkt in einen eigene Tabelle kommt), dann endet das ziemlich sicher im Chaos. Im Nachhinein größere Änderungen an der DB-Struktur zu machen kann nämlich mega-viel Aufwand sein, wenn man Daten auf X Tabellen neu aufteilen muss.

Gruß, noisefloor
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Beim Tabellen Design stellt man sich die Frage, welche Daten ich in welchen Einheiten bekomme.
Du hast einen Tarif, der hat ein Startdatum, ein Grundpreis und einen Arbeitspreis.
Dann hast du einen Zählerstand, der besteht aus einem Datum und einem Zählerstand.
Das reicht schon an Daten die du in einer Datenbank speichern musst; die Abrechnung ergibt sich alleine durch diese Informationen.
Der Rest ist eine Funktion "Abrechnung", mit dem Argumenten Startdatum und Enddatum. Aus der Zählerstand Tabelle kannst du dann den Verbrauch pro Tag im Abrechnungszeitraum ausrechnen. Aus der Tariftabelle kommt dann für jeden Tag ein Grundpreis und ein Arbeitspreis. Aus dem Verbrauch an jedem einzelnen Tag vergeben sich dann die Kosten für einen Tag. Die Summe ist dann der zu zahlende Betrag, und in Monate umgerechnet der neue Abschlag.
Damit hast du ein sehr einfaches Datenbankdesign. Neue Informationen bedeuten immer nur einen neuen Eintrag in einer Tabelle. Der Rest ergibt sich durch einfache Berechnung.
Benutzeravatar
snafu
User
Beiträge: 6743
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Master_Shredder hat geschrieben: Freitag 9. Februar 2024, 22:34
@snafu
Warum dupliziert die Abrechnung nochmal die Hälfte der Tarif-Attribute?
Wie meinst du das? Wo dupliziere ich sie?
Duplizieren war vielleicht nicht ganz treffend. Ich meinte die Stelle, wo du einige Attribute vom Tarif an die Abrechnung übergibst. Da würde man besser das Tarif-Objekt als Ganzes übergeben und dann innerhalb der Abrechnung auf die benötigten Attribute zugreifen.
Antworten