Zum Inhalt springen

Dezimalzahlen

In manchen Fällen müssen Sie möglicherweise Dezimalzahlen mit Garantien bezüglich der Genauigkeit speichern können.

Dies ist besonders wichtig, wenn Sie Dinge wie Währungen, Preise, Konten und andere speichern, da Sie sicher sein möchten, dass keine Rundungsfehler auftreten.

Als Beispiel: Wenn Sie Python öffnen und 1.1 + 2.2 summieren, erwarten Sie 3.3 zu sehen, aber Sie erhalten tatsächlich 3.3000000000000003

>>> 1.1 + 2.2
3.3000000000000003

Das liegt an der Art und Weise, wie Zahlen in "Einsen und Nullen" (binär) gespeichert werden. Aber Python hat ein Modul und einige Typen, um strikte Dezimalwerte zu haben. Mehr dazu finden Sie in der offiziellen Python-Dokumentation für Decimal.

Da Datenbanken Daten auf die gleiche Weise wie Computer (binär) speichern, hätten sie die gleichen Probleme. Und deshalb haben sie auch einen speziellen Dezimal-Typ.

In den meisten Fällen wäre dies wahrscheinlich kein Problem, zum Beispiel beim Messen von Aufrufen eines Videos oder der Lebensleiste in einem Videospiel. Aber wie Sie sich vorstellen können, ist dies besonders wichtig, wenn es um Geld und Finanzen geht.

Dezimal-Typen

Pydantic bietet spezielle Unterstützung für Decimal-Typen.

Wenn Sie Decimal verwenden, können Sie die Anzahl der Ziffern und Dezimalstellen, die unterstützt werden sollen, in der Funktion Field() angeben. Diese werden von Pydantic validiert (z. B. bei Verwendung von FastAPI) und die gleichen Informationen werden auch für die Datenbankspalten verwendet.

Info

Für die Datenbank verwendet SQLModel SQLAlchemy's DECIMAL-Typ.

Dezimalzahlen in SQLModel

Nehmen wir an, jeder Held in der Datenbank hat einen Geldbetrag. Wir könnten dieses Feld zu einem Decimal-Typ machen, indem wir die Funktion condecimal() verwenden.

from decimal import Decimal

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: int | None = Field(default=None, index=True)
    money: Decimal = Field(default=0, max_digits=5, decimal_places=3)

# Code below omitted 👇
👀 Vorschau der vollständigen Datei
from decimal import Decimal

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: int | None = Field(default=None, index=True)
    money: Decimal = Field(default=0, max_digits=5, decimal_places=3)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1)
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)

        session.commit()


def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Deadpond")
        results = session.exec(statement)
        hero_1 = results.one()
        print("Hero 1:", hero_1)

        statement = select(Hero).where(Hero.name == "Rusty-Man")
        results = session.exec(statement)
        hero_2 = results.one()
        print("Hero 2:", hero_2)

        total_money = hero_1.money + hero_2.money
        print(f"Total money: {total_money}")


def main():
    create_db_and_tables()
    create_heroes()
    select_heroes()


if __name__ == "__main__":
    main()
🤓 Andere Versionen und Varianten
from decimal import Decimal
from typing import Optional

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: Optional[int] = Field(default=None, index=True)
    money: Decimal = Field(default=0, max_digits=5, decimal_places=3)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1)
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)

        session.commit()


def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Deadpond")
        results = session.exec(statement)
        hero_1 = results.one()
        print("Hero 1:", hero_1)

        statement = select(Hero).where(Hero.name == "Rusty-Man")
        results = session.exec(statement)
        hero_2 = results.one()
        print("Hero 2:", hero_2)

        total_money = hero_1.money + hero_2.money
        print(f"Total money: {total_money}")


def main():
    create_db_and_tables()
    create_heroes()
    select_heroes()


if __name__ == "__main__":
    main()

Hier sagen wir, dass money maximal 5 Ziffern mit max_digits haben kann, dies beinhaltet die Ganzzahlen (links vom Dezimalpunkt) und die Dezimalstellen (rechts vom Dezimalpunkt).

Wir sagen auch, dass die Anzahl der Dezimalstellen (rechts vom Dezimalpunkt) 3 ist, so dass wir 3 Dezimalziffern für diese Zahlen im money-Feld haben können. Das bedeutet, dass wir 2 Ziffern für den Ganzzahlteil und 3 Ziffern für den Dezimalteil haben werden.

✅ Also, zum Beispiel, sind dies alles gültige Zahlen für das money-Feld

  • 12.345
  • 12.3
  • 12
  • 1.2
  • 0.123
  • 0

🚫 Aber dies sind alles ungültige Zahlen für dieses money-Feld

  • 1.2345
  • Diese Zahl hat mehr als 3 Dezimalstellen.
  • 123.234
  • Diese Zahl hat insgesamt mehr als 5 Ziffern (Ganzzahl und Dezimalteil).
  • 123
  • Obwohl diese Zahl keine Dezimalstellen hat, haben wir trotzdem 3 Plätze dafür vorgesehen, was bedeutet, dass wir nur 2 Plätze für den Ganzzahlteil verwenden können, und diese Zahl hat 3 Ganzzahlziffern. Somit ist die erlaubte Anzahl von Ganzzahlziffern max_digits - decimal_places = 2.

Tipp

Stellen Sie sicher, dass Sie die Anzahl der Ziffern und Dezimalstellen an Ihre eigenen Bedürfnisse in Ihrer eigenen Anwendung anpassen. 🤓

Modelle mit Dezimalzahlen erstellen

Beim Erstellen neuer Modelle können Sie tatsächlich normale (float) Zahlen übergeben. Pydantic konvertiert sie automatisch in Decimal-Typen, und SQLModel speichert sie als Decimal-Typen in der Datenbank (mit SQLAlchemy).

# Code above omitted 👆

def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1)
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)

        session.commit()

# Code below omitted 👇
👀 Vorschau der vollständigen Datei
from decimal import Decimal

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: int | None = Field(default=None, index=True)
    money: Decimal = Field(default=0, max_digits=5, decimal_places=3)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1)
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)

        session.commit()


def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Deadpond")
        results = session.exec(statement)
        hero_1 = results.one()
        print("Hero 1:", hero_1)

        statement = select(Hero).where(Hero.name == "Rusty-Man")
        results = session.exec(statement)
        hero_2 = results.one()
        print("Hero 2:", hero_2)

        total_money = hero_1.money + hero_2.money
        print(f"Total money: {total_money}")


def main():
    create_db_and_tables()
    create_heroes()
    select_heroes()


if __name__ == "__main__":
    main()
🤓 Andere Versionen und Varianten
from decimal import Decimal
from typing import Optional

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: Optional[int] = Field(default=None, index=True)
    money: Decimal = Field(default=0, max_digits=5, decimal_places=3)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1)
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)

        session.commit()


def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Deadpond")
        results = session.exec(statement)
        hero_1 = results.one()
        print("Hero 1:", hero_1)

        statement = select(Hero).where(Hero.name == "Rusty-Man")
        results = session.exec(statement)
        hero_2 = results.one()
        print("Hero 2:", hero_2)

        total_money = hero_1.money + hero_2.money
        print(f"Total money: {total_money}")


def main():
    create_db_and_tables()
    create_heroes()
    select_heroes()


if __name__ == "__main__":
    main()

Dezimaldaten auswählen

Wenn Sie dann mit Decimal-Typen arbeiten, können Sie bestätigen, dass sie tatsächlich diese Rundungsfehler von Floats vermeiden.

# Code above omitted 👆

def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Deadpond")
        results = session.exec(statement)
        hero_1 = results.one()
        print("Hero 1:", hero_1)

        statement = select(Hero).where(Hero.name == "Rusty-Man")
        results = session.exec(statement)
        hero_2 = results.one()
        print("Hero 2:", hero_2)

        total_money = hero_1.money + hero_2.money
        print(f"Total money: {total_money}")

# Code below omitted 👇
👀 Vorschau der vollständigen Datei
from decimal import Decimal

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: int | None = Field(default=None, index=True)
    money: Decimal = Field(default=0, max_digits=5, decimal_places=3)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1)
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)

        session.commit()


def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Deadpond")
        results = session.exec(statement)
        hero_1 = results.one()
        print("Hero 1:", hero_1)

        statement = select(Hero).where(Hero.name == "Rusty-Man")
        results = session.exec(statement)
        hero_2 = results.one()
        print("Hero 2:", hero_2)

        total_money = hero_1.money + hero_2.money
        print(f"Total money: {total_money}")


def main():
    create_db_and_tables()
    create_heroes()
    select_heroes()


if __name__ == "__main__":
    main()
🤓 Andere Versionen und Varianten
from decimal import Decimal
from typing import Optional

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: Optional[int] = Field(default=None, index=True)
    money: Decimal = Field(default=0, max_digits=5, decimal_places=3)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def create_heroes():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1)
    hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001)
    hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2)

    with Session(engine) as session:
        session.add(hero_1)
        session.add(hero_2)
        session.add(hero_3)

        session.commit()


def select_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Deadpond")
        results = session.exec(statement)
        hero_1 = results.one()
        print("Hero 1:", hero_1)

        statement = select(Hero).where(Hero.name == "Rusty-Man")
        results = session.exec(statement)
        hero_2 = results.one()
        print("Hero 2:", hero_2)

        total_money = hero_1.money + hero_2.money
        print(f"Total money: {total_money}")


def main():
    create_db_and_tables()
    create_heroes()
    select_heroes()


if __name__ == "__main__":
    main()

Ergebnisse überprüfen

Wenn Sie dies jetzt ausführen, wird anstelle der unerwarteten Zahl 3.3000000000000003 die Zahl 3.300 ausgegeben.

$ python app.py

// Some boilerplate and previous output omitted 😉

// The type of money is Decimal('1.100')
Hero 1: id=1 secret_name='Dive Wilson' age=None name='Deadpond' money=Decimal('1.100')

// More output omitted here 🤓

// The type of money is Decimal('1.100')
Hero 2: id=3 secret_name='Tommy Sharp' age=48 name='Rusty-Man' money=Decimal('2.200')

// No rounding errors, just 3.3! 🎉
Total money: 3.300

Warnung

Obwohl Dezimal-Typen auf der Python-Seite unterstützt und verwendet werden, unterstützen nicht alle Datenbanken sie. Insbesondere SQLite unterstützt keine Dezimalzahlen und konvertiert sie in den gleichen Fließkomma-NUMERIC-Typ, den es unterstützt.

Aber Dezimalzahlen werden von den meisten anderen SQL-Datenbanken unterstützt. 🎉