Python Aufgaben: Dataclasses, Pattern Matching

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
voldemort_73
User
Beiträge: 1
Registriert: Freitag 25. November 2022, 19:14

Guten Abend, könnte mir bitte jemand bei dem Vergleich des Datums via Pettern Matching helfen, Aufgabe b). Stehe gerade leider auf dem Schlauch.

In dieser Aufgabe sollen Sie die Lagerbestände eines Supermarkts modellieren. Der
Lagerbestand einer Ware ist entweder verzehrbar oder nicht-verzehrbar.
Sie können die gesamte Funktionalität mit dem beigefügten Skript test_supermarket.py
testen. Das Testskript soll nicht verändert werden.

(a) Erstellen Sie zunächst zwei Datenklassen Food und NonFood, wobei NonFood ei-
ne leere Klasse ist und Food ein Mindesthaltbarkeitsdatum enthält. Modellieren
Sie das Datum dabei als einen String, der für den 14. Februar 2021 das Format
"2021-02-14" hat1 . Dieses Format erlaubt es, die lexikographische Ordnung zu
verwenden, um herauszufinden ob eines von zwei Daten weiter in der Zukunft
liegt als das andere.
Erstellen Sie die Datenklasse Stock mit folgenden Feldern: Namen der Ware
name, Anzahl der gelagerten Waren units, Stückpreis in Cents price_per_unit
sowie kind. Das Feld kind soll eine Instanz der Datenklassen Food oder NonFood
enthalten, definieren Sie es also als Union-Typ über die beiden Datenklassen.

(b) Schreiben Sie eine Funktion is_expired, die einen Lagerbestand und ein Da-
tum als Argumente nimmt und zurückgibt, ob der Lagerbestand zu diesem
Datum abgelaufen ist. Verzehrbare Waren gelten dabei ab einem Tag nach ih-
rem Mindesthaltbarkeitsdatum als abgelaufen. Nicht-verzehrbare Waren gelten
nie als abgelaufen. Verwenden Sie Pattern Matching, um den Inhalt des Feldes
kind zu prüfen.


Hier mein Ansatz:

Code: Alles auswählen

from dataclasses import dataclass


@dataclass
class Food:
    mhd: str


@dataclass
class NonFood:
    pass


@dataclass
class Stock:
    name: str # name of the goods
    units: int # amount of the goods
    price_per_unit: int # in cents
    kind: Food | NonFood


def is_expired(stk: Stock, date: str):
    match stk.kind:
        case Food(mhd):
            print("lezter tag an dem nicht abgelaufen")
            print(Food(mhd), date)
        case Food.mhd:
            print("Hi")
        case NonFood():
            print("kann nicht ablaufen")


def get_expired(unit: list, date: str):
    abgelaufen = []
        

def buy():
    pass
Die Funktion sollte mit Folgender Datei getestet werden.

Code: Alles auswählen

from supermarket import Stock, Food, NonFood, get_expired, buy


def test_supermarket():
    # test Stock
    stocks: list[Stock] = [
        Stock("Chocolate", 12, 199, Food("2020-12-07"), ),
        Stock("Tooth Brush", 30, 299, NonFood())
    ]
    print(f"Stocks: {stocks}")
    assert stocks[0].name == "Chocolate"
    assert stocks[0].units == 12
    assert stocks[0].price_per_unit == 199
    #assert stocks[0].kind.expiration_date == "2020-12-07"

    assert stocks[1].name == "Tooth Brush"
    assert stocks[1].units == 30
    assert stocks[1].price_per_unit == 299

    # test get_expired
#    assert get_expired(stocks, "2020-12-05") == []
#    assert get_expired(stocks, "2020-12-09") == [stocks[0]]

    # test buy
#    stock: Stock = Stock("Chocolate", 12, 199, Food("2020-12-07"))
#    assert stock.units == 12
    #assert buy(stock, 5) == 5
#    assert stock.units == 7
#    assert buy(stock, 25) == 7
#    assert stock.units == 0


if __name__ == "__main__":
    test_supermarket()
narpfel
User
Beiträge: 643
Registriert: Freitag 20. Oktober 2017, 16:10

Was ist denn deine konkrete Frage? Was macht der Code? Gibt es eine Fehlermeldung? Wenn nicht, was erwartest du und an welcher Stelle beginnt deine Erwartung vom tatsächlichen Verhalten des Codes abzuweichen? Kannst du für jeden der drei `case`s erklären, warum du ihn geschrieben hast?

Als zusätzliche Anmerkung: Deine Typannotationen sind fehlerhaft:

Code: Alles auswählen

$ mypy --strict supermarket.py
supermarket.py:22: error: Function is missing a return type annotation  [no-untyped-def]
supermarket.py:33: error: Function is missing a return type annotation  [no-untyped-def]
supermarket.py:33: error: Missing type parameters for generic type "list"  [type-arg]
supermarket.py:34: error: Need type annotation for "abgelaufen" (hint: "abgelaufen: List[<type>] = ...")  [var-annotated]
supermarket.py:37: error: Function is missing a return type annotation  [no-untyped-def]
supermarket.py:37: note: Use "-> None" if function does not return a value
Found 5 errors in 1 file (checked 1 source file)
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Und die Typannotationen in der Testfunktion sind schlicht überflüssig. Das ist wie mit Kommentaren: Wenn die das offensichtliche aussagen, braucht man sie nicht. Bei einer *leeren* Liste kann es Sinn machen eine Typannotation zu schreiben, weil man an der Stelle sonst nicht weiss, was da rein kommen soll. Aber wenn nach dem ``=`` ein Ausdruck steht bei dem sonnenklar ist was der für einen Typ hat, macht es keinen Sinn das noch mal extra hin zu schreiben. Weder für den menschlichen Leser, noch für eine statische Typprüfung.

Das sollte IMHO auch nicht nur eine Testfunktion sein. Da werden zu viele und zu verschiedene Dinge für *eine* Funktion getestet.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Intern Sollte man ein Datum immer vom Typ datetime.date speichern, dafür Strings zu benutzen ist umständlich und fehleranfällig. Benutze keine kryptischen Abkürzungen, wenn Du `stock` meinst, schreibe nicht `stk`. Und statt `mhd` `best_before_date`.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Was für ein unschönes Beispiel, um Python zu lernen. Wenn die Klasse Stock geeignet implementiert wird, reduziert sich der Aufwand zum Test auf das Erreichen des Mindesthaltbarkeitsdatum auf:

Code: Alles auswählen

def is_expired(stock, date):
    return stock.is_food and stock.expiration < date
Dazu bedarf es weder Dataclasses, noch Pattern-Matching und auch auch keines Type-Pattern-Matching. Aber als Methode auf Stock wäre es sinnvoll.

Und inhaltlich ist die Aufgabenstellung auch falsch, denn das Mindesthaltbarkeitsdatum ist keineswegs gleichbedeutend mit dem Verfallsdatum von schnell verderblichen Waren. Insofern ist die Funktions-Bezeichnung 'is_expired' irreführend.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich würde sogar sagen, das `stock.is_food` ist hier irrelevant, weil es auch Haltbarkeits- oder Verfallsdaten bei Waren gibt die keine Nahrung sind. Also eher

Code: Alles auswählen

def is_expired(stock, date):
    return stock.expiration is not None and stock.expiration < date
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Ja, das hängt von der genaueren Spezifizierung von Stock ab. Es ist eben eine unschöne Aufgabe, da sie nicht eine elegante „pythonische“ Lösung wünscht, sondern das Gegenteil fordert.

Gute Übungen für dataclasses und type-pattern-matching zu finden ist noch nicht ganz einfach, da dies aus meiner Sicht zwei Lösungen sind, die sich gegenwärtig noch auf der Suche nach praktischen Problemen befinden.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1012
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Was mich am meisten stört, dass mhd ein str ist und kein date Obkekt.
Der Vergleich müsste auch mit str funktionieren, wenn strikt YYYY-MM-DD eingehalten wird. Ich vergleiche aber lieber date-instanzen. Des Weiteren könnte man auch ein ungültiges Datum verwenden (2022-02-29).

Code: Alles auswählen

from dataclasses import dataclass
from datetime import datetime as DateTime


@dataclass
class Food:
    mhd: str


@dataclass
class NonFood:
    pass


@dataclass
class Stock:
    name: str
    units: int
    price_per_unit: int
    kind: Food | NonFood


def is_expired(stk: Stock, date: str) -> bool:
    # NonFood hat kein mhd, kann aber als stk.kind vorkommen
    # also muss das zuerst abgefragt werden
    match stk.kind:
        case NonFood():
            # ist immer False, da NonFood nicht ablaufen kann
            # (davon z.B. ausgeschlossen: Amiga 500)
            return False
        case Food(mhd):
            # das ist ziemlich doof
            # wieso werden die strings nicht direkt in date-Objekte
            # umgewandelt und verwendet?
            return (
                DateTime.strptime(date, "%Y-%m-%d").date()
                > DateTime.strptime(mhd, "%Y-%m-%d").date()
            )


def get_expired(unit: list, date: str) -> list[Stock]:
    return [article for article in unit if is_expired(article, date)]


def buy(article: Stock, amount: int) -> int:
    """
    Keine Ahnung was diese Funktion machen soll
    Die tests scheinen wohl die gekaufte Menge als int zu erwarten.
    """
    amount = min(article.units, amount)
    article.units -= amount
    return amount

Tests:

Code: Alles auswählen

from supermarket import Stock, Food, NonFood, get_expired, buy


def test_supermarket():
    # test Stock
    stocks: list[Stock] = [
        Stock("Chocolate", 12, 199, Food("2020-12-07"), ),
        Stock("Tooth Brush", 30, 299, NonFood())
    ]
    print(f"Stocks: {stocks}")
    assert stocks[0].name == "Chocolate"
    assert stocks[0].units == 12
    assert stocks[0].price_per_unit == 199
    #assert stocks[0].kind.expiration_date == "2020-12-07"

    assert stocks[1].name == "Tooth Brush"
    assert stocks[1].units == 30
    assert stocks[1].price_per_unit == 299

    # test get_expired
    assert get_expired(stocks, "2020-12-05") == []
    assert get_expired(stocks, "2020-12-09") == [stocks[0]]

    # test buy
    stock: Stock = Stock("Chocolate", 12, 199, Food("2020-12-07"))
    assert stock.units == 12
    assert buy(stock, 5) == 5
    assert stock.units == 7
    assert buy(stock, 25) == 7
    assert stock.units == 0


if __name__ == "__main__":
    test_supermarket()

PS: Die Tests sind schlecht.
PPS: Die NonFood dataclass hat keinerlei Funktion
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
ThomasL
User
Beiträge: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

voldemort_73 hat geschrieben: Freitag 25. November 2022, 19:35 Das Feld kind soll eine Instanz der Datenklassen Food oder NonFood
enthalten, definieren Sie es also als Union-Typ über die beiden Datenklassen.
Ist zwar schon länger her das ich mal in diese andere Sprache reingeschnuppert habe aber dieser Satz hier ist imho ein Indiz dafür, dass dies keine Übung für Python ist sondern eher für Java.
Oder irre ich mich?
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
Benutzeravatar
noisefloor
User
Beiträge: 3843
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

nee, das Python typing-Modul kennt auch `Union`, siehe https://docs.python.org/3/library/typin ... ping.Union.

Im Kontext der Aufgabe macht das aber schon Sinn. Was ja nicht heißt, dass die Aufgabe im gesamten Sinn macht, siehe diverse vorangegangene Posts ;-)

Gruß, noisefloor
Benutzeravatar
DeaD_EyE
User
Beiträge: 1012
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

https://peps.python.org/pep-0604/ ersetzt typing.Union.

Wenn man immer brav

Code: Alles auswählen

from __future__ import annotations
nutzt, kann man auch Features von typing nutzen, die noch nicht bei der entsprechenden Python-Version verfügbar sind.
Die PEP604 ist z.B. mit Python 3.10 eingeführt worden, kann aber mit Python 3.9 auch genutzt werden:

Code: Alles auswählen

from __future__ import annotations



def foo(x: int | float) -> complex:
    return complex(x, x)
    
    
foo("a")
Wenn man dann mypy ausführt, bekommt man folgende Meldung:

Code: Alles auswählen

ppp.py:9: error: Argument 1 to "foo" has incompatible type "str"; expected "Union[int, float]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)
Wenn man den Code ausführt, bekommt man einen anderen Fehler:

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/andre/ppp.py", line 9, in <module>
    foo("a")
  File "/home/andre/ppp.py", line 6, in foo
    return complex(x, x)
TypeError: complex() can't take second arg if first is a string
Die Typen sind nur ein Hinweis für die IDE (oder Menschen), werden aber nicht zur Laufzeit erzwungen.

Es gibt Frameworks, die typehints nutzen, um zur Laufzeit die Typen zu prüfen. pydantic macht das z.B..
FastAPI nutzt die typehints auch zur Konvertierung, aber auch um bei Funktionen an bestimmte Objekte zu kommen (Request).
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
ThomasL
User
Beiträge: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

Danke für die Ausführungen. Habe ich wieder etwas dazu gelernt. :-)
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
Antworten