Cocktail Automat

Du hast eine Idee für ein Projekt?
undeat
User
Beiträge: 8
Registriert: Dienstag 3. Dezember 2019, 21:53

Hallo,

ich bin neu hier im Forum. Eigentlich bin ich beruflich SPS Programmierer aufgrund diverser privaten Interessen beschäftige ich immer mal wieder mit "richtigen" Programmiersprachen, je nachdem, was für mein Projekt gerade am Sinnvollsten ist. Ich bin M 33 Jahre alt. Zu meinen Hobbys ist alles gesagt :D

So nun zum Thema: (Keine Frage, informell)

ich bin schon seit geraumer Zeit dabei einen Cocktail Automaten zu bauen. Der funktioniert jetzt auch schon nur will ich den Code jetzt einmal einem Redesighn unterziehen, da bei der Entwicklung doch recht viel gewachsen ist und ein ziemlicher Spagetticode die Folge war.

Grundsätzlich funktioniert das Ganze zur Zeit so:
Eine einfache HTLM Seite für mit einem PHP Skript erstellt. PHP Schaut in einer Datenbank nach Hinterlegten Cocktails. Diese werden ausgelistet. In der Datenbank ist Hinterlegt, wie viel von welcher Zutat in den Cocktail gehört, an welchem Anschluss sich die Zutat befindet, wie schnell der Anschluss fördert und noch ein paar Andere dinge, auf die ich jetzt nicht eingehen will.
Auf der Webseite kann eine Schaltfläche bedient werden. Das ruft ein PhP Skript auf, dieses Schreib die ID vom angeforderten Cocktail in eine DB. Ein Pyhton Skript Pollt die ID in der Datenbak. (Gefällt mir nicht)
Wird eine ID vom Python erkannt, werden über eine Serielle Schnittstelle und eine Relais Platine bis zu 12 Ventile für eine bestimmte Zeit angesteuert. Durch die Ventile kommt dann die Entsprechende Menge der jeweiligen Zutat. Also rein Funktionell ist das schon richtig Geil ;).

Nun habe ich erstmal das Python Skript angefangen zu überarbeiten. Ich will von der Datenbank weg und mit csv Dateien Arbeiten. Diese will ich später "am Stück" auf die Webseite Laden und dort mit HTLM5 Manier eine "Offline" Webseite erstellen. Ein anderer Grund für CSV ist die bessere Editierbarkeit am PC und das Einfachere Backuppen.

Hier ist erstmal mein Python 3.6 Code:

import csv

class AktorType:
Name = ""
Durchfluss = 0
Stromverbrauch = 0
Einschaltzeit = 0.0

class ZutatType:
Name = ""
Menge = 0.0
Faktor = 0.0
Alkohol = 0
Anteil = 0.0
ZielMenge = 0.0
AlkoholMenge = 0.0
Anschluss = None

class AnschlussType:
Name = ""
Bit = 0
Faktor = 0
Akotor = None

class CocktailType:
Name = ""
AuffüllenMenge = 0
Menge = 0
MaxMenge = 0
ZielMenge = 0
Alkohol = 0.0
AlkoholAnteil = 0.0
Zutaten = []

"Schleife 1: Für alle Zutaten, die im Cocktail sind, den ZutatType Anlegen"
"Schleife 2: Beim Anlegen des ZutatType muss der Anschluss gefunden und der AnschlussType definiert werden"
"Schleife 3: Beim Anlegen des AnschlussType muss der Aktor gefunden und der AktorType definiert werden"
def GetZutat(Name):
with open('Zutaten.csv') as csvfile:
readCSV = csv.reader(csvfile,delimiter=';')
LineNumber = 1
Zutat = ZutatType()
for row in readCSV:
if row[0] == Name:
Zutat.Name = row[0]
Zutat.Faktor = float(row[1])
Zutat.Alkohol = float(row[2])
Zutat.Anschluss = GetAnschluss(Zutat.Name)
return Zutat

#Zutat nicht gefunden
print ("Fehler: Zutat " + Name + " wurde nicht gefunden")
Zutat.Name = Name + "(nicht gefunden)"
Zutat.Faktor = 1.0
Zutat.Alkohol = 0.0
Zutat.Anschluss = AnschlussType()
Zutat.Anschluss.Name = ""
Zutat.Anschluss.Bit = 0
Zutat.Anschluss.Faktor = 1.0
Zutat.Anschluss.Aktor = AktorType()
Zutat.Anschluss.Aktor.Name = ""
Zutat.Anschluss.Aktor.Durchfluss = 1.0
Zutat.Anschluss.Aktor.Stromverbrauch = 1.0
return Zutat

def GetAnschluss(Name):
with open('Anschlüsse.csv') as csvfile:
readCSV = csv.reader(csvfile,delimiter=';')
LineNumber = 1
for row in readCSV:
if row[4] == Name:
Anschluss = AnschlussType()
Anschluss.Name = row[0]
Anschluss.Bit = int(row[1])
Anschluss.Akotor = GetAktor(row[2])
Anschluss.Faktor = float(row[3])
return Anschluss

#Anschluss für Zutat nicht gefunden
print ("Fehler: Anschluss für " + Name + " wurde nicht gefunden")
Anschluss = AnschlussType()
Anschluss.Name = ""
Anschluss.Bit = 0
Anschluss.Faktor = 1.0
Anschluss.Aktor = AktorType()
Anschluss.Aktor.Name = ""
Anschluss.Aktor.Durchfluss = 1.0
Anschluss.Aktor.Stromverbrauch = 1.0
return Anschluss

def GetAktor(Name):
with open('Aktoren.csv') as csvfile:
readCSV = csv.reader(csvfile,delimiter=';')
LineNumber = 1
for row in readCSV:
if row[0] == Name:
Aktor = AktorType()
Aktor.Name = row[0]
Aktor.Durchfluss = float(row[1])
Aktor.Stromverbrauch = float(row[2])
return Aktor

#Anschluss für Zutat nicht gefunden
print ("Fehler: Aktor " + Name + " wurde nicht gefunden")
Aktor = AktorType()
Aktor.Name = ""
Aktor.Durchfluss = 1.0
Aktor.Stromverbrauch = 1.0
return Aktor

def MixeCocktail(Name):
Cocktail = CocktailType() #Cocktail Datenstruktur Anlegen
DateiName = "Cocktails\\" + Name + ".csv"
with open(DateiName) as csvfile:
readCSV = csv.reader(csvfile,delimiter=';')
LineNumber = 1
for row in readCSV: #Cocktail.csv Zeilenweise auslsen
if LineNumber == 4:
Cocktail.AuffüllenMenge = float(row[1])
Cocktail.Menge = Cocktail.Menge + Cocktail.AuffüllenMenge #Auffülen Menge zur Gesamtmenge hinzuaddieren

if LineNumber == 5:
Cocktail.MaxMenge = float(row[1])

if LineNumber > 10: #Ab Zeile 10 Zutaten: Name, Menge, Schritt
Zutat = GetZutat(row[0]) #Zutat eigenschaften aus Zutaten.csv lesen inkl. Anschluss und Aktor Eigenschaften
Zutat.Menge = float(row[1])
Zutat.Schritt = int(row[2])
Cocktail.Menge = Cocktail.Menge + Zutat.Menge #Alle Mengen der Zutaten addieren für Gesamtmenge
Cocktail.Zutaten.append(Zutat)
LineNumber = LineNumber + 1

#Alle für den Cocktail benötigten Informationen sind jetzt in der Cocktail Datenstruktur

#Festlegen der Zielmenge für den Cocktail
if Cocktail.Menge > Cocktail.MaxMenge:
Cocktail.ZielMenge = Cocktail.MaxMenge
else:
Cocktail.ZielMenge = Cocktail.Menge

#Später auf Glasgröße begrenzen

#Berechnen, zu welchem Anteil welche Zutat im Cocktail vorhanden ist
#Alkoholgehalt ermitteln
#Einschaltzeiten berechnen
for i in range(len(Cocktail.Zutaten)):
Cocktail.Zutaten.Anteil = Cocktail.Zutaten.Menge / Cocktail.Menge
Cocktail.Zutaten.ZielMenge = Cocktail.Zutaten.Anteil * Cocktail.ZielMenge
Cocktail.Zutaten.AlkoholMenge = Cocktail.Zutaten.ZielMenge * (Cocktail.Zutaten.Alkohol/100)
Cocktail.Alkohol = Cocktail.Alkohol + Cocktail.Zutaten.AlkoholMenge
Cocktail.Zutaten.Anschluss.Akotor.Einschaltzeit = (Cocktail.Zutaten.ZielMenge / Cocktail.Zutaten[i].Anschluss.Akotor.Durchfluss) * Cocktail.Zutaten[i].Faktor * Cocktail.Zutaten[i].Anschluss.Faktor
#Alkoholanteil vom Cocktail berechnen
Cocktail.AlkoholAnteil = (Cocktail.Alkohol/Cocktail.ZielMenge)*100.0

#Ergebnisse erstmal nur als Text ausgeben
print("Gesamtmenge: " + str(Cocktail.Menge))
print("ZielMenge: " + str(Cocktail.ZielMenge))
print("Alkohol gehalt: " + str(Cocktail.Alkohol) + "ml")
print("Alkohol Anteil: " + str(Cocktail.AlkoholAnteil) + "%")
for i in range(len(Cocktail.Zutaten)):
print("Zutat " + str(i+1) + ": " + str(Cocktail.Zutaten[i].ZielMenge) + "ml " + Cocktail.Zutaten[i].Name + ", " + Cocktail.Zutaten[i].Anschluss.Akotor.Name + " an " + Cocktail.Zutaten[i].Anschluss.Name + " im Schritt " + str(Cocktail.Zutaten[i].Schritt) + " für " + str(Cocktail.Zutaten[i].Anschluss.Akotor.Einschaltzeit) + "s einschalten")

MixeCocktail("Cocktail")
Benutzeravatar
__blackjack__
User
Beiträge: 13113
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@undeat: Von einer Datenbank würde ich nicht ohne Not weg gehen. Man kann ja auch SQLite verwenden, da sind Sicherungskopien kein Problem und es gibt auch Programme um das grafisch zu bearbeiten, sowohl für den Desktop als auch als Webanwendung.

Zum Code: Du benutzt Klassen komplett falsch. Du hast da keine einzige richtige Klasse. Als erstes mal ist das `Type` in den Namen unsinnig weil eine Klasse dazu da ist einen Datentyp zu definieren. Das ist bei jeder Klasse so, als ist der Namenszusatz `Type` nichts was dem Leser irgendwas bringen würde. Das ist anscheinend ”nötig” weil Du alles ausser Klassen falsch schreibst. Namen werden in Python klein_mit_unterstrichen geschrieben. Die beiden Ausnahmen von der Regel sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Dann braucht man bei ``Cocktail = CocktailType()` den `Type`-Zusatz nicht, denn das heisst ``cocktail = Cocktail()``. Das es sich bei `Cocktail` um eine Klasse/einen Datentyp handelt, sieht man an der Schreibweise.

Dann sind die Klassenattribute falsch weil die nicht verwendet werden. Auf Klassenebene gehören als Daten eher nur Konstanten. Eine Klasse sollte eine `__init__()`-Methode haben und die sollte die Attribute auf dem *Exemplar* anlegen. Nach dem erstellen sollte ein Objekt in einem benutzbaren Zustand sein. Das heisst alles was ein Objekt dazu braucht wird beim *erstellen* des Objekts als Argument übergeben und in der `__init__()` an das Objekt gebunden. Man erstellt nicht leere Objekte und setzt denen dann nach nach die Datenattribute von aussen. Schon gar nicht über mehrere Ebenen hinweg. So etwas wie ``Zutat.Anschluss.Aktor.Stromverbrauch = 1.0`` ist gruselig. Ich sehe auch semantisch nicht das von einer Cocktailzutat so ein weg zum Stromverbrauch führen sollte.

Wenn man reine Datenobjekte haben möchte kann man für unveränderbare Objekte `collections.namedtuple()` verwenden um einen Datentyp dafür zu erstellen. Ich persönlich mag das externe `attr`-Modul ganz gerne um Arbeit/”Boilerplate” bei Klassen zu vermeiden. Und gibt es wirklich *nur* passive Datenklassen? Kann natürlich sein, aber je mehr man davon hat, desto wahrscheinlicher programmiert man kein Python.

Literale Zeichenketten sind keine Kommentare. Kommentare werden mit dem ``#`` eingeleitet. Das was da steht ist aber auch kein sinnvoller Kommentar. Faustregel: Kommentare beschreiben nicht *was* passiert, denn das steht da bereits als Code, sondern *warum* das *so* passiert, sofern das nicht offensichtlich ist.

`readCSV` wäre ein guter Name für eine Funktion, denn er beschreibt eine Tätigkeit, eher nicht für ein Reader-Objekt, das ja keine Tätigkeit ist, sondern ein iterierbares Objekt über die Datensätze. Wenn man einen davon `row` nennt, könnte man den Reader beispielsweise `rows` nennen. Oder auch gar kein Name, weil man den eh nur an einer Stelle braucht, wo man auch den Code für die Erstellung des Readers direkt schreiben könnte.

`LineNumber` in `GetZutat()` wird nicht verwendet.

Wenn ein Fehler auftritt dann gibt man das nicht aus und macht dann mit irgendeinem Dummy-Objekt mit ”besonderen” Werten weiter, sondern löst eine Ausnahme aus. Wenn eine Zutat, ein Anschluss, … nicht gefunden werden kann, dann kann man doch gar nicht sinnvoll weiterarbeiten. Und wenn das nicht deutlich signalisiert wird, muss man an anderer Stelle auf diese besonderen Werte prüfen, und da auch immer dran denken wo und unter welchen Umständen da vielleicht nicht *echte* Werte erzeugt werden konnten. Das ist alles supernervig, fehleranfällig, und schwer zu warten.

So könnte eine `get_zutat()` aussehen:

Code: Alles auswählen

def get_zutat(gesuchter_name):
    with open('Zutaten.csv') as csv_file:
        for name, faktor, alkohol in csv.reader(csv_file, delimiter=';'):
            if name == gesuchter_name:
                return ZutatType(
                    name, float(faktor), float(alkohol), get_anschluss(name)
                )

    raise KeyError(f"zutat {gesuchter_name!r} nicht gefunden")
Das auch in anderen Funktionen ein ungenutztes `LineNumber` vorkommt, und die verdammt ähnlich aussehen, ist ein Warnzeichen das hier Code kopiert wurde. Wenn man Code immer wieder kopiert und anpasst, macht man etwas falsch und sollte die gemeinsamen Teile herausziehen, so dass man keinen kopierten Code hat.

Pfadteile setzt man nicht mit ``+`` zusammen. Denn ``"Cocktails\\" + Name + ".csv"`` wird weder unter Linux noch auf dem Mac funktionieren. Dafür gibt es `pathlib`.

Da wo `LineNumber` dann mal wirklich benutzt wird ist das äusserst unschön das die CSV-Datei so unregelmässig ist, und anscheinend auch noch mit Zeilen in denen nichts oder etwas steht was gar nicht verarbeitet wird. Wenn da wirklich nur die Datensätze drin stehen würden die auch eingelesen werden müssen, könnte man wenigstens auf `LineNumber` verzichten und mit `next()` und/oder `itertools.islice()` die einzelnen Blöcke von verschiedenen Datensätzen verarbeiten. Das wäre übersichtlicher, weniger fehleranfällig, und man bräuchte die Zeilennummer nicht.

``for i in range(len(sequence)):`` nur im dann über `i` auf die Elemente von der Sequenz zuzugreifen ist in Python ein „anti-pattern“. Man kann direkt über die Elemente iterieren, ohne den Umweg über einen Index.


Aus diesem Indexzugriffslastigen Code:

Code: Alles auswählen

    for i in range(len(Cocktail.Zutaten)):
        Cocktail.Zutaten[i].Anteil = Cocktail.Zutaten[i].Menge / Cocktail.Menge
        Cocktail.Zutaten[i].ZielMenge = (
            Cocktail.Zutaten[i].Anteil * Cocktail.ZielMenge
        )
        Cocktail.Zutaten[i].AlkoholMenge = Cocktail.Zutaten[i].ZielMenge * (
            Cocktail.Zutaten[i].Alkohol / 100
        )
        Cocktail.Alkohol = Cocktail.Alkohol + Cocktail.Zutaten[i].AlkoholMenge
        Cocktail.Zutaten[i].Anschluss.Akotor.Einschaltzeit = (
            (
                Cocktail.Zutaten[i].ZielMenge
                / Cocktail.Zutaten[i].Anschluss.Akotor.Durchfluss
            )
            * Cocktail.Zutaten[i].Faktor
            * Cocktail.Zutaten[i].Anschluss.Faktor
        )
wird das hier:

Code: Alles auswählen

    for Zutat in Cocktail.Zutaten:
        Zutat.Anteil = Zutat.Menge / Cocktail.Menge
        Zutat.ZielMenge = Zutat.Anteil * Cocktail.ZielMenge
        Zutat.AlkoholMenge = Zutat.ZielMenge * (Zutat.Alkohol / 100)
        Cocktail.Alkohol = Cocktail.Alkohol + Zutat.AlkoholMenge
        Zutat.Anschluss.Akotor.Einschaltzeit = (
            (Zutat.ZielMenge / Zutat.Anschluss.Akotor.Durchfluss)
            * Zutat.Faktor
            * Zutat.Anschluss.Faktor
        )
Falls man *zusätzlich* zu den Elementen noch eine laufende Zahl braucht, gibt es die `enumerate()`-Funktion.

Bei dem Code fällt dann auch ins Auge das da etwas gemacht wird das der Cocktail ”selbst” können sollte: seinen eigenen Alkoholgehalt berechnen. Ich sehe da mindestens Methoden, wenn nicht gar Properties auf `Cocktail`-Objekten.

Zusammenstückeln von Zeichenketten und Werten mit ``+`` und `str()` ist eher BASIC als Python. Python kennt dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten und ab Python 3.6 f-Zeichenkettenliterale.
Aus dem hier:

Code: Alles auswählen

    for i in range(len(Cocktail.Zutaten)):
        print(
            "Zutat "
            + str(i + 1)
            + ": "
            + str(Cocktail.Zutaten[i].ZielMenge)
            + "ml "
            + Cocktail.Zutaten[i].Name
            + ", "
            + Cocktail.Zutaten[i].Anschluss.Akotor.Name
            + " an "
            + Cocktail.Zutaten[i].Anschluss.Name
            + " im Schritt "
            + str(Cocktail.Zutaten[i].Schritt)
            + " für "
            + str(Cocktail.Zutaten[i].Anschluss.Akotor.Einschaltzeit)
            + "s einschalten"
        )
wird das hier:

Code: Alles auswählen

    for i, Zutat in enumerate(Cocktail.Zutaten, 1):
        print(
            f"Zutat {i}: {Zutat.Zielmenge}ml {Zutat.Name},"
            f" {Zutat.Anschluss.Akotor.Name} an {Zutat.Anschluss.Name}"
            f" im Schritt {Zutat.Schritt}"
            f" für {Zutat.Anschluss.Akotor.Einschaltzeit}s einschalten"
        )
Wie gesagt ich würde mir das gut überlegen eine SQL-Datenbank selbst mit CSV-Dateien nachzuprogrammieren. Mehr Sinn würde es IMHO machen ein ORM wie SQLAlchemy zu verwenden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
undeat
User
Beiträge: 8
Registriert: Dienstag 3. Dezember 2019, 21:53

Wow du hast dir ja viel Mühe gegeben. Vielen Dank dafür. In der Tat sind deine Hinweise Sinnvoll. Ich habe jetzt gerade nicht de Zeit gabt, alles genau nach zu vollziehen aber das mache ich auf jeden Fall. Python hat ja echt viele Möglichkeiten die selben Funktionen mit anders Methodik zu programmieren.

Das mit den Klassen finde ich auch unschön. Mich wundert das Python es überhaupt zulässt ein Objekt ohne Konstruktor anzulegen. Eigentlich wollte ich eine "Struct" aus C verwenden. Darum habe ich die Dinger auch Type genannt, weil es eigentlich keine Klasse sein soll.

Du hast natürlich die CSV Dateien nicht darum ist es schwierig alles nachzuvollziehen.

Ziel ist es ja einen Cocktail zu machen. Um das zu machen, muss ich diverse Ausgänge für eine Bestimmte Zeit in einer bestimmten Reihenfolge ansteuern. Ein Cocktail besteht zunächst mal aus Zutaten. Jede Zutat ist ein Aktor zugeordnet. Ich habe Ventile, die alle gleich sein, aber ein Arbeitskollege hat kleine und große Pumpen. Diese haben unterschiedlichen Stromverbrauch und wir mussten darauf achten, dass nicht zu viele Pumpen gleichzeitig laufen. Daher kamen diese Daten. Eine Zutat hat also einen Aktor, der hat einen Stromverbrauch und der Aktor ist an einem Anschluss. Diese verwendeten China Pumpen und die unterschiedlichen Schläuche führten dazu, dass eine Anpassung der Fördermenge je Pumpe vorgenommen werden musste. Daher der Faktor am Anschluss.

Bevor ich das ganze jetzt noch einmal überarbeite, gibt es soetwas wie eine Struktur was ich anstelle der Klassen verwenden könnte? Oder geht das alles mit dem collections.namedtuple()? Das klingt für mich doch recht sperrig und ich muss die Daten ja später noch verändern können weil ich die Teilweise ja erst später befüllen kann.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Du hast aber keine einfache Tabellen-Struktur, daher ist CSV schlecht geeignet.
Wenn es sich nur um statische Daten handelt, könntest Du Dir yaml anschauen.
Das Laden gibt dann automatisch Wörterbücher, eins mit Aktuatoren, wo die Keys z.B. die Flüssigkeiten sein können mit all den Hardware-Parametern, die Du brauchst. Ein anderes wären dann die Cocktails, wo drinsteht, welche Zutaten in welcher Menge gebraucht werden.
Diese Wörterbücher sind statisch. Werden also nie verändert.

Dann brauchst Du nur noch eine Funktion schreiben, die die Zutaten des Cocktails mit den Aktuatoren verknüpft und eine neue Datenstruktur, diesmal wohl am besten eine Klasse, weil darauf kann man ja wirklich arbeiten und verschiedene Dinge abfragen, die erst berechnet werden.

Nochmal zum Mitscheiben: versuche das Programm so aufzubauen, dass Du nie etwas später befüllen mußt. Das macht das Programm undurchsichtig.
undeat
User
Beiträge: 8
Registriert: Dienstag 3. Dezember 2019, 21:53

Ja klar ist CSV keine Datenbank sämtliche Errungenschaften der Technik werden ignoriert man geht wieder auf ganz einfaches Textformat. Aber der Vorteil für mich liegt darin, dass ich (und vor allem andere) diese Dateien wunderbar bearbeiten kann. Für jeden Cocktail gibt es einige eigene Datei. Wenn jemand ein paar Cocktails vorbereiten möchte, bzw. für seinen Automaten erstellt hat, kann ich die einfach übernehmen. Wenn er sein Zutaten anders benannt hat, kann ich dies mit suchen/ersetzen korrigieren. Vorher bei der DB habe ich eine Tabelle mit ID Cocktail / ID Zutat gemacht für die Zuordnung. Das gleiche mit ID Anschluss / ID Zutat. Wenn da irgendwelche IDs verschoben werden, fängt man von vorne an. Excel oder Libre hat jeder drauf und zur not kann man csv mit dem texteditor bearbeiten. sqlLite ist kein Stück besser. Da haben sich leute viel mühe gegeben, um die SQL Befehle auf Daten anwenden zu können, die in einer einzelnen Datei liegen. Diese wird auch beim start erstmal geparst. Dazu hat das ganze riesig viel Overhead. Ich kann ich ein zweites Python Skript schreiben, das ich nur per Import einbinde und ich nur die getCocktail(FileName) von außerhalb aufrufe. Dann gibt es keine Verwirrung weil am ende dieser Funktion ist die komplette Datenstruktur ausgefüllt. Ich sehe da den Mehrwert für mich nicht. Ich lese doch erstmal alles ein und dann berechne ich und dann würde die Ausgabe kommen. Da ist nichts dran durcheinander oder undurchsichtig. Finde ich jedenfalls. Wenn ich alles in SQL Daten vorliegen habe, muss ich genau so erstmal Schleifen durchlaufen bis ich alles beisammen habe, was ich brauche. Die Skalierung der Mengen kann ich eh erst machen, wenn ich die Gesamtmenge habe und alle Zutaten durchlaufen habe.

Zum Fehlerhandling: Ich würde das Sktipt später im Falle eines Fehlers abbrechen und einen Output String erzeugen. Das Skript wird später über php aufgerufen und php kann den Output String entgegen nehmen und dann auf dem Webserver darstellen. Der Raspberry läuft ohne Ausgabegerät und da bekäme man sonst nichts mit.
Benutzeravatar
__blackjack__
User
Beiträge: 13113
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@undeat: Man kein keine Klassen ohne Initialisierungsmethode erstellen, denn letztlich erbt alles von `object` und `object` hat eine `__init__()`, somit hat jede Klasse eine `__init__()`.

``struct`` aus C sind Datenklassen in Python, also Klassen die einfach ausser der `__init__()`, die die Attribute setzt, keine weiteren Methoden hat. Wie gesagt, kann ich da das `attr`-Modul wärmstens ans Herz legen. Wenig Schreibarbeit und es werden eine `__init__()`, eine `__repr__()`, die Vergleichsmethoden, usw. einfach aus den Attributangaben erstellt.

Der Nachteil von Dateien die jeder bearbeiten kann, ist das die jeder bearbeiten kann — und dabei alle möglichen Fehler machen kann. Und wenn Du Programme zum umwandeln, importieren in Deinen CSV-Zoo schreiben kannst, könntest Du auch welche schreiben die solche Dateien in die Datenbank einlesen.

Was meinst Du mit verschobenen IDs? Wie soll *das* bei einer Datenbank passieren? Ich sehe wie so etwas bei selbst gebastelten CSV-Dateien + Code passieren kann, weil man sich da plötzlich selbst um so Sachen wie Eindeutigkeit von Schlüsseln und der Integrität über verschiedene Dateien hinweg kümmern muss.

Der Vorteil von SQLite ist der das leicht Sicherungskopien erstellt werden können und dass das ganze leichgewichtig ist. Das mit den Sicherungskopien war ja ein Kritikpunkt den Du bei DBs geäussert hattest.

Und von welchem Overhead sprichst Du bitte? Welche Datei wird da beim Start geparst? Die Datenbankdatei jedenfalls nicht. Das was an ”Overhead” da ist ist *da*, das braucht man nicht noch mal selbst in schlechter getestet und weniger Funktionalität mit einzelnen CSV-Dateien nachprogrammieren.

Wenn man ein ORM verwendet, und das mache ich so gut wie immer wenn ich mit SQL zu tun habe, braucht man auch nicht viel Code schreiben um die Daten aus der Datenbank abzufragen, denn dafür hat man ja das ORM, das man sich um diesen Kram mit SQL nicht selbst kümmern braucht.

Wenn man ein `Session`-Objekt für die Datenbank erstellt hat, kann man bei SQLAlchemy zum Beispiel so einen Cocktail über den Namen erstellen und dann alle Zutaten ausgeben:

Code: Alles auswählen

    cocktail = session.query(Cocktail).filter_by(name="Mochito").one()
    for ingredient in cocktail.ingredients:
        print(f"{ingredient.name} ...")
Die Abfrage des Cocktails ist noch explizit, aber wesentlich einfach als SQL schreiben und aus dem Ergebnis dann ein `Cocktail`-Objekt zu erstellen. Die Zutaten werden dagegen dann transparent abgefragt. Im Default-Fall wenn man das erste mal darauf zugreift, man kann die Beziehungen zwischen den Klassen aber auch so konfigurieren, dass da bei der Abfrage von `Cocktail`-Objekten schon die dazugehörigen Zutaten mit abgefragt werden.

Das mit dem Berechnen von aussen *ist* unübersichtlich und fehleranfällig, denn einiges davon hängt ja von den Zutaten ab, das heisst die Attribute sind nicht mehr korrekt wenn man etwas zu den Zutaten hinzufügt, entfernt, oder in den Zutaten ändert. Auf der sicheren Seite wäre man wenn man das in den Objekten selbst berechnen würde, und zwar dann wenn man es braucht *aktuell*, so dass auch Änderungen in den Daten berücksichtigt werden. Stichwort wäre hier `property()`.

Was an der Berechnung von aussen auch nicht gut ist, ist die Vermischung der Datenhaltung mit den Berechnungen. Die sind ja unabhängig davon ob die Daten aus CSV-Dateien kommen, aus YAML-Dateien, aus XML-Dateien, aus einer SQL-Datenbank, oder sonst wo her.

Wie hättest Du das mit der Fehlerbehandlung denn gemacht mit diesen Dummy-Objekten?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
undeat
User
Beiträge: 8
Registriert: Dienstag 3. Dezember 2019, 21:53

ich habe mal wieder etwas Zeit gehabt. Auch wenn das "neue" Skript als Ergebnis genau das gleiche liefert, wie das alte, wollte ich mir noch mal die Mühe machen und versuchen alle deine Vorschläge auszuprobieren. So sieht das jetzt aus.

Code: Alles auswählen

import csv

file_name_zutaten = 'zutaten.csv'
file_name_aktoren = 'Aktoren.csv'
file_name_anschluesse = 'Anschluesse.csv'
path_cocktails = 'Cocktails\\'

class aktor:
	name = ""
	durchflussmenge = 0.0
	stromaufnahme = 0.0
	einschaltzeit = 0.0
	
	def __init__(self,aktorname):
		with open(file_name_aktoren) as csv_file:
			reader = csv.DictReader(csv_file,delimiter=';')
			for row in reader:
				if row['Name'] == aktorname:
					self.name = row['Name']
					self.durchflussmenge = float(row['Durchfluss [ml/s]'])
					self.stromaufnahme = float(row['Stromaufnahme  [A]'])
					
	def calculate(self,menge):
		self.einschaltzeit = menge/self.durchflussmenge
		
class anschluss:
	name = ""
	bit = 0
	faktor = 0
	aktor = None
	
	def __init__(self,Zutat):
		with open(file_name_anschluesse) as csv_file:
			reader = csv.DictReader(csv_file,delimiter=';')
			for row in reader:
				if row['Zutat'] == Zutat:
					self.name = row['Name']
					self.bit = int(row['Bit'])
					self.faktor = float(row['Faktor'])
					self.aktor = aktor(row['Aktor'])
					
	def calculate (self,menge):
		self.aktor.calculate(menge*self.faktor)

class Zutat:
	name = ""
	anschluss = None
	faktor = 0.0
	alkohol_anteil = 0.0
	alkoholgehalt = 0.0
	grund_menge = 0.0
	menge = 0.0
	schritt = 0
	
	def __init__(self,cocktail_row):
		self.grund_menge = float(cocktail_row[1])
		self.schritt = int(cocktail_row[2])
		with open(file_name_zutaten) as csv_file:
			reader = csv.DictReader(csv_file,delimiter=';')
			for row in reader:
				if row['Name'] == cocktail_row[0]:
					self.name = cocktail_row[0];
					self.faktor = float(row['Faktor'])
					self.alkohol_anteil = float(row['Alkohol [%]'])
					self.anschluss = anschluss(cocktail_row[0])
					
	def calculate(self,ziel_menge):
		self.menge = (self.grund_menge/ziel_menge)*ziel_menge
		self.alkoholgehalt = self.menge*(self.alkohol_anteil/100)
		self.anschluss.calculate(self.menge*self.faktor)
	
				
class Cocktail:

	auffuellen_menge = 0
	gesamt_menge = 0
	max_menge = 0
	ziel_menge = 0
	gesamt_menge_alkohol = 0.0
	alkohol_anteil = 0.0
	zutaten = []
	
	def __init__(self,name):
		with open(path_cocktails + name + '.csv') as csv_file:
			reader = csv.reader(csv_file,delimiter=';')
			for row in reader:
				if reader.line_num == 4: #Zeile 1 bis 3 fuer Script nicht relevant
					self.auffuellen_menge = float(row[1])
					self.gesamt_menge += self.auffuellen_menge #Mit skalieren, damit spaeter genuegend platz im Glas zum Auffuellen bleibt
				elif reader.line_num == 5: 
					self.max_menge = float(row[1])
				elif reader.line_num > 10: #Ab hier Zutatenliste
					self.add_zutat(row)
		self.calculate()
	
	def add_zutat(self,row):
		self.gesamt_menge += float(row[1])
		self.zutaten.append(Zutat(row))
		
					
	def calculate(self):
		if self.gesamt_menge > self.max_menge:
			self.ziel_menge = self.max_menge
		else:
			self.ziel_menge = self.gesamt_menge
			
		for Zutat in self.zutaten:
			Zutat.calculate(self.ziel_menge)
			self.gesamt_menge_alkohol += Zutat.alkoholgehalt
			
		self.alkohol_anteil = (self.gesamt_menge_alkohol/self.ziel_menge)*100


		
cocktail = Cocktail('Mochito')

print(f"gesamt_menge: {cocktail.gesamt_menge}")
print(f"ziel_menge: {cocktail.ziel_menge}")
print(f"Alkohol gehalt: {cocktail.gesamt_menge_alkohol}ml")
print(f"Alkohol Anteil: {cocktail.alkohol_anteil}%")
	
for i, zutat in enumerate(cocktail.zutaten,1):
	print(
		f"Zutat {i}: {zutat.menge}ml {zutat.name},"
		f" {zutat.anschluss.aktor.name} an {zutat.anschluss.name}"
		f" im Schritt {zutat.schritt}"
		f" fuer {zutat.anschluss.aktor.einschaltzeit}s einschalten"
	)
Ich habe viel dazu gelernt. Python ist im Vergleich zu C ja dann doch deutlich komfortabler, mann muss aber die Tricks erstmal alle kennen.

Code: Alles auswählen

for i, zutat in enumerate(cocktail.zutaten,1):
Genial, wäre ich aber so schnell nicht drauf gekommen.

Deinen Hinweis mit der pathlib muss ich mir noch ansehen. Aber auf meinem RPI mit Linux funktioniert das Skript auch so
Dann sind die Klassenattribute falsch weil die nicht verwendet werden. Auf Klassenebene gehören als Daten eher nur Konstanten. Eine Klasse sollte eine `__init__()`-Methode haben und die sollte die Attribute auf dem *Exemplar* anlegen.
Das verstehe ich noch nicht ganz. Ich muss doch auf der Klassenebene die Datentypen anlegen, weil sonst kann ich später nichts rein schreiben auch nicht mit der __init__(). Oder benutze ich das noch ganz falsch. So ähnlich wie ich es gemacht habe ist es eben im C. Gibt es eigentlich gar kein private, protected, public? Warum ist der erste Parameter, der einer Methode automatisch übergeben wird das Objekt selbst? Das finde ich im C eigentlich Sinnvoller. Gut der Vorteil im Python ist, dass es keine Probleme mit globalen Variablen Namen geben kann, die eventuell in der Klasse ebenfalls verwendet werden, aber das kann doch nicht alles sein.

Und nun habe ich noch Fehlerbehandlung vor mir. Ich muss ja nicht nur Fehler erzeugen, sondern auch welche abfangen, die im csv.reader ausgelöst werden. Das Kapitel ist aber was für eine anderen Abend :)
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Zur Namenskonvention: Konstanten werden komplett groß geschrieben, Klassen mit großem Anfangsbuchstaben. Eingerückt wird immer mit vier Leerzeichen pro Ebene, keine Tabs.
C ist deutlich anders als Python. Man muss keine Attribute definieren. Sie werden einfach in __init__ angelegt. Es gibt auch keine Zugriffsbeschränkungen. Diese sind unnötig, solange sich alle an die Konventionen halten z.b. dass man nicht von außen auf Attribute zugreift, die mit einem Unterstrich anfangen.

Deine __init__-Methoden machen zu viel. Dort solltest du nur die Attribute mit Werten füllen und nicht jedes Mal die komplette CSV Datei lesen, nur um einen Eintrag daraus zu verwenden. Stattdessen solltest du eine Funktion schreiben, die die Datei liest und z.b. eine Liste aller Aktoren zurückliefert.

Noch mal der klare Rat: trenne statische Daten, wie Anschluss, Aktor oder Zutat vom eigentlichen Rezept das für jede Zutat Menge oder für jeden Aktor die Einschaltzeit enthält.
Benutzeravatar
__blackjack__
User
Beiträge: 13113
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@undeat: Man muss keine Datentypen auf Klassenebene anlegen, was auch immer das heissen soll. Und ”rein schreiben” vermittelt ein falsches Bild. Namen/Attribute stehen in Python *nicht* für Speicher wo man Werte rein schreibt, wie in C. Bei C deklariert man mit Namen den Typ und den Speicherort und das Objekt besteht nur aus dem Wert. In Python ist der Name einfach nur ein Name, Datentyp und Speicherort sind Eigenschaften vom Objekt, die nichts mit dem/den Namen zu tun haben über die dieses Objekt erreichbar sind.

Attribute auf Klassenebene kann man sich als ``static``-Felder in einem ``struct`` vorstellen, wenn es so etwas in C gäbe. Die sind für Sachen die bei allen Exemplaren gleich sind, weil es sie nur einmal gibt, eben auf der Klasse. Insofern gilt das gleiche wie für Variablen auf Modulebene: globale Variablen machen nur Probleme und Programme unflexibel und fehleranfällig. Also werden auf Klassen in der Regel auch nur Konstanten definiert die zu dieser Klasse gehören.

In C++ kann man ``struct`` statische Felder verpassen, was dann so aussieht:

Code: Alles auswählen

#include <iostream>

struct S {
    static int a;
    int b;
};

int S::a = 0;

int main(void)
{
    S x = { 1 };
    S y = { 2 };
    
    x.a = 23;
    
    std::cout << "x: " << x.a << ' ' << x.b << std::endl;
    std::cout << "y: " << y.a << ' ' << y.b << std::endl;
    
    y.a = 42;
    
    std::cout << "x: " << x.a << ' ' << x.b << std::endl;
    std::cout << "y: " << y.a << ' ' << y.b << std::endl;
    
    return 0;
}
Ausgabe:

Code: Alles auswählen

x: 23 1
y: 23 2
x: 42 1
y: 42 2
In Python kann es den gleichen Namen sowohl auf Klassenebene als auch auf den einzelnen Exemplaren geben. Das ist aber verwirrend, weil dann in ``instance.counter += 1`` wenn es auf dem Exemplar vorher kein `counter` gab, aber auf der Klasse schon, bei dieser Operation der Wert zum erhöhen von der Klasse gelesen, das Ergebnis dann aber auf dem Exemplar geschrieben wird.

Klassen in Python sind halt deutlich anders als ``struct`` in C. In C legt man damit fest welche Felder ein Exemplar davon hat und welchen Typ die haben. In Python steht im ``class``-Körper der Code der Attribute auf der Klasse definiert. Also in der Regel die Methoden und manchmal (konstante) Werte die keine Methoden sind. Die Attribute die ein Exemplar davon hat werden in der `__init__()` definiert. Technisch kann man in der Regel auch von anderen Methoden aus oder gar von aussen aus neue Attribute erstellen. Das sollte man aber alles nicht machen, weil Programme sonst sehr schwer nachzuvollziehen sind. Nach Ablauf der `__init__()` sollte sich ein Objekt in der Regel in einem vollständigen, benutzbaren Zustand befinden.

Diese Richtlinie gilt für C und ``struct`` im Grunde ja auch. Bei komplexen ``struct``\s schreibt man eine Initialisierungsgfunktion nach deren Ablauf das gesamte ``struct`` sinnvoll gefüllt ist und bei weniger Komplexen hat man in der Regel eine Zuweisung von allen Feldern der Form ``S x = { … };`` wenn es keine Initialisierungsgfunktion gibt. Die es zumindest bei mir eigentlich fast immer gibt, denn sonst wird es zu einem “Albtraum“ wenn man mal ein Feld hinzufügt und dann im ganzen Code suchen darf wo man das überall jetzt noch initialisieren muss. Die Frage stellt sich nicht wenn man eine Initialisierungsgfunktion, denn dann ist das der einzige Punkt im Programm.

Es gibt keinen statischen Zugriffsschutz in Python. Den könnte man sehr wahrscheinlich sowieso leicht aushebeln. Es reicht Implementierungsdetails mit *einem* führenden Unterstrich zu kennzeichnen.

Bei C ist das mit dem Objekt selbst sinnvoller gelöst? Also man kann sich da natürlich aussuchen an welcher Stelle man es übergibt, es muss also nicht das erste Argument sein, aber man muss da ja auch beim Aufruf einen Zeiger auf das Objekt explizit selbst übergeben und man muss auch selbst die passende Funktion wählen sofern sie nicht ”virtuell” ist. Und in dem Fall muss man Code schreiben der die passende Funktion in dem Objekt-``struct`` setzt, oder in dem ``struct`` das die Klasse repräsentiert — je nach dem wie detailliert man welches Objektmodell in C manuell implementieren will. Was die Position angeht entscheide ich mich in C auch immer für das erste Argument, auch wenn man natürlich die Wahl hat, aber das erscheint mir einfach ”natürlich” das ``struct`` was die Hauptrolle spielt auch als erstes in der Argumentliste zu haben.

Ich finde das bei Python gut gelöst das die ”Magie”, also das binden des Objekts an das erste Argument nicht schon in der Klasse sichtbar ist, denn so gibt es keinen Unterschied zwischen Funktionen und ungebundenen Methoden auf Klassenebene. Das ist konzeptuell schön einfach.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Das gesagte mal in Beispielen.

Attribute werden in __init__ angelegt:

Code: Alles auswählen

class Aktor:
    def __init__(self, aktorname):
        with open(file_name_aktoren) as csv_file:
            reader = csv.DictReader(csv_file,delimiter=';')
            for row in reader:
                if row['Name'] == aktorname:
                    break
            else:
                raise KeyError('actor name not found')
        self.name = row['Name']
        self.durchflussmenge = float(row['Durchfluss [ml/s]'])
        self.stromaufnahme = float(row['Stromaufnahme  [A]'])
        self.einschaltzeit = 0.0

    def calculate(self,menge):
        self.einschaltzeit = menge/self.durchflussmenge
Das Lesen sollte aber in einer getrennten Funktion stattfinden:

Code: Alles auswählen

class Aktor:
    def __init__(self, name, durchflussmenge, stromaufnahme):
        self.name = name
        self.durchflussmenge = durchflussmenge
        self.stromaufnahme = stromaufnahme
        self.einschaltzeit = 0.0

    def calculate(self,menge):
        self.einschaltzeit = menge/self.durchflussmenge

def read_actors(filename):
    actors = {}
    with open(filename) as csv_file:
        reader = csv.DictReader(csv_file,delimiter=';')
        for row in reader:
            actors[row['Name']] = Aktor(row['Name'],
                float(row['Durchfluss [ml/s]']),
                float(row['Stromaufnahme  [A]'])
            )
    return actors
Das ganze zusammengefasst:

Code: Alles auswählen

import csv
from pathlib import Path

FILENAME_AKTOREN = 'Aktoren.csv'
FILENAME_ANSCHLUESSE = 'Anschluesse.csv'
FILENAME_ZUTATEN = 'zutaten.csv'
PATH_COCKTAILS = Path(__file__).parent / 'Cocktails'

class Aktor:
    def __init__(self, name, durchflussmenge, stromaufnahme):
        self.name = name
        self.durchflussmenge = durchflussmenge
        self.stromaufnahme = stromaufnahme

    def calculate_einschaltzeit(self, menge):
        return menge/self.durchflussmenge

def read_actors(filename):
    actors = {}
    with open(filename) as csv_file:
        reader = csv.DictReader(csv_file,delimiter=';')
        for row in reader:
            actors[row['Name']] = Aktor(row['Name'],
                float(row['Durchfluss [ml/s]']),
                float(row['Stromaufnahme  [A]'])
            )
    return actors

class Anschluss:
    def __init__(self, name, bit, faktor, aktor):
        self.name = name
        self.bit = bit
        self.faktor = faktor
        self.aktor = aktor

    def calculate_einschaltzeit(self, menge):
        return self.aktor.calculate_einschaltzeit(menge * self.faktor)

def read_anschluesse(filename, actors):
    anschluesse = {}
    with open(filename) as csv_file:
        reader = csv.DictReader(csv_file, delimiter=';')
        for row in reader:
            anschluesse[row['Zutat']] = Anschluss(
                row['Name'],
                int(row['Bit']),
                float(row['Faktor']),
                actors[row['Aktor']]
            )
    return anschluesse

class Zutat:
    def __init__(self, name, faktor, alkohol_anteil, anschluss):
        self.name = name
        self.faktor = faktor
        self.alkohol_anteil = alkohol_anteil
        self.anschluss = anschluss
                    
    def calculate_einschaltzeit(self, ziel_menge):
        return self.anschluss.calculate_einschaltzeit(ziel_menge * self.faktor)
    
    def calculate_alkoholgehalt(self, ziel_menge):
        return ziel_menge*(self.alkohol_anteil/100)

def read_zutaten(filename, anschluesse):
    zutaten = {}
    with open(filename) as csv_file:
        reader = csv.DictReader(csv_file,delimiter=';')
        for row in reader:
            zutaten[row['Name']] = Zutat(
                row['Name'],
                float(row['Faktor']),
                float(row['Alkohol [%]']),
                anschluesse[row['Name']],
            )
    return zutaten

class RezeptZutat:
    def __init__(self, zutat, grund_menge, schritt):
        self.zutat = zutat
        self.grund_menge = grund_menge
        self.schritt = schritt

    def calculate_einschaltzeit(self, ziel_menge, gesamt_menge):
        return self.zutat.calculate_einschaltzeit(ziel_menge/gesamt_menge * self.grund_menge)
    
    def calculate_alkoholgehalt(self, ziel_menge, gesamt_menge):
        return self.zutat.calculate_alkoholgehalt(ziel_menge/gesamt_menge * self.grund_menge)

    def calculate(self,ziel_menge):
        self.menge = (self.grund_menge/ziel_menge)*ziel_menge
        self.alkoholgehalt = self.menge*(self.alkohol_anteil/100)
        self.anschluss.calculate(self.menge*self.faktor)


class Cocktail:
    auffuellen_menge = 0
    gesamt_menge = 0
    max_menge = 0
    ziel_menge = 0
    gesamt_menge_alkohol = 0.0
    alkohol_anteil = 0.0
    zutaten = []
    
    def __init__(self, name, auffuellen_menge, max_menge, rezept):
        self.name = name
        self.auffuellen_menge = auffuellen_menge
        self.max_menge = max_menge
        self.rezept = rezept

def read_cocktail(name, zutaten):
    with (PATH_COCKTAILS / name + '.csv').open() as csv_file:
        reader = csv.reader(csv_file,delimiter=';')
        # Zeile 1 bis 3 fuer Script nicht relevant
        _ = next(reader), next(reader), next(reader)
        auffuellen_menge = float(next(reader)[1])
        max_menge = float(next(reader)[1])
        # Zeile 6 bis 10 fuer Script nicht relevant
        _ = next(reader), next(reader), next(reader), next(reader), next(reader)
        # Ab hier Zutatenliste
        rezept = []
        for row in reader:
            rezept.append(RezeptZutat(
                row[0],
                float(row[1]),
                zutaten[row[2]]
            ))
    return Cocktail(name, auffuellen_menge, max_menge, rezept)


aktoren = read_actors(FILENAME_AKTOREN)
anschluesse = read_anschluesse(FILENAME_ANSCHLUESSE, aktoren)
zutaten = read_zutaten(FILENAME_ZUTATEN, anschluesse)
cocktail = read_cocktail('Mochito', zutaten)
Benutzeravatar
__blackjack__
User
Beiträge: 13113
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wobei hier in `RezeptZutat` noch Attribute in `calculate()` neu eingeführt werden und die Attribute auf der `Cocktail`-Klasse müssen noch weg.

Bei den `calculate_*()`-Methoden würde ich auch schauen ob das nicht besser Properties wären.

Edit: Die `read_*()`-Funktionen würde ich als Klassenmethoden modellieren.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
undeat
User
Beiträge: 8
Registriert: Dienstag 3. Dezember 2019, 21:53

So ich habe mal wieder was geschafft.
Ich habe jetzt das Ganze noch etwas mehr der Realität angepasst und einen Cocktail und einen Cocktailautomaten erstellt. Der Cocktailautomat mixt dann den ihm übergebenen Cocktail.

Code: Alles auswählen

import csv

FILE_NAME_ZUTATEN = 'zutaten.csv'
FILE_NAME_AKTOREN = 'Aktoren.csv'
FILE_NAME_ANSCHLUESSE = 'Anschluesse.csv'
PATH_COCKTAILS = 'Cocktails\\'


class Aktor:    
    def __init__(self,name,durchflussmenge,stromaufnahme):
        self.name = name
        self.durchflussmenge = durchflussmenge
        self.stromaufnahme = stromaufnahme
        
        
class Anschluss:
    def __init__(self,name,bit,faktor,aktor,zutat):
        self.name = name
        self.bit = bit
        self.faktor = faktor
        self.aktor = aktor
        self.zutat = zutat
        
    def dosieren(self,menge,faktor_zutat):
        print (f"{self.name} mit {self.aktor.name} soll {menge}ml foerdern. Bit {self.bit} fuer {(menge*faktor_zutat*self.faktor)/self.aktor.durchflussmenge}s einschalten")    
                    
class Zutat:
    def __init__(self,name,faktor,alkohol):
        self.name = name
        self.faktor = faktor
        self.alkohol_anteil = alkohol
                    
class RezeptEintrag():
    def __init__(self,zutat,menge,schritt):
        self.zutat = zutat
        self.menge = menge
        self.schritt = schritt
        
class Cocktail:

    def __init__(self,name, path_cocktails, path_zutaten):
        self.read_cocktail(name,path_cocktails,self.read_zutaten(path_zutaten))
        
    def read_cocktail(self,name,path_cocktails,zutaten):
        with open(path_cocktails + name + '.csv') as csv_file:
            reader = csv.reader(csv_file,delimiter=';')
            # Zeile 1 bis 3 fuer Script nicht relevant
            _ = next(reader), next(reader), next(reader)
            auffuellen_menge = float(next(reader)[1])
            max_menge = float(next(reader)[1])
            # Zeile 6 bis 10 fuer Script nicht relevant
            _ = next(reader), next(reader), next(reader), next(reader), next(reader)
            # Ab hier Zutatenliste
            rezept = []
            for row in reader:
                rezept.append(RezeptEintrag(zutaten[row[0]],float(row[1]),float(row[2])))
        
        self.name = name
        self.auffuellen_menge = auffuellen_menge
        self.max_menge = max_menge
        self.rezept = rezept        
        
    def read_zutaten(self,filename):
        zutaten = {}
        with open(filename) as csv_file:
            reader = csv.DictReader(csv_file,delimiter=';')
            for row in reader:
                zutaten[row['Name']] = Zutat(row['Name'],float(row['Faktor']),float(row['Alkohol [%]']))
        return zutaten    
        
class Cocktailautomat:
    _MAX_SCHRITTE = 10
    _MAX_STROMAUFNAHME = 100
    def __init__(self,file_name_anschluesse,file_name_aktoren):
        self.anschluesse = self.read_anschluesse(file_name_anschluesse,self.read_aktoren(file_name_aktoren))
        
    def dosieren(self,cocktail,menge):
        grund_menge = 0
        for rezepteintrag in cocktail.rezept:
            grund_menge += rezepteintrag.menge
        
        if menge > cocktail.max_menge:
            ziel_menge = cocktail.max_menge
        else:
            ziel_menge = menge
            
        skalierungsfaktor = ziel_menge/grund_menge
        
        for rezepteintrag in cocktail.rezept:
            zutat_menge = rezepteintrag.menge*skalierungsfaktor
            anschluss = self.find_anschluss(rezepteintrag.zutat.name)
            anschluss.dosieren(zutat_menge,rezepteintrag.zutat.faktor)

    def find_anschluss(self,name):
        for anschluss in self.anschluesse:
            if self.anschluesse[anschluss].zutat == name:
                return self.anschluesse[anschluss]
            
    def read_aktoren(self,filename):
        aktoren = {}
        with open(filename) as csv_file:
            reader = csv.DictReader(csv_file,delimiter=';')
            for row in reader:
                aktoren[row['Name']] = Aktor(row['Name'],float(row['Durchfluss [ml/s]']),float(row['Stromaufnahme  [A]']))
        return aktoren
    
    def read_anschluesse(self,filename,aktoren):
        anschluesse = {}
        with open(filename) as csv_file:
            reader = csv.DictReader(csv_file,delimiter=';')
            for row in reader:
                anschluesse[row['Name']] = Anschluss(row['Name'],int(row['Bit']),float(row['Faktor']),aktoren[row['Aktor']],row['Zutat'])
        return anschluesse    
            

cocktailautomat = Cocktailautomat(FILE_NAME_ANSCHLUESSE,FILE_NAME_AKTOREN)
cocktail = Cocktail('Mochito',PATH_COCKTAILS,FILE_NAME_ZUTATEN)

cocktailautomat.dosieren(cocktail,180.0)

print("done")
ich habe die Methode find_anschluss im Cocktailautomaten. Die for schleife dort liefert mit einen String mit dem Anschlussnamen anstatt eines Anschlussobjektes. Warum ist das so?

Im nächsten Schritt muss ich mir noch was weiteres Überlegen. Um die Anschlüsse zu steuern muss mein Cocktailautomat über RS232 ein einem Word die Bits der Anschlüsse setzten und an einen Arduino senden.
Ich brauche also ein RS232 Objekt. Das würde ich im Cocktailautomaten ansiedeln. Dieser soll aber nicht, wenn die for schleife zum dosieren durchlaufen wird immer senden, sondern nur am ende der for schleife einmal den Zustand des gesamten Wortes. Vor allem aber muss das auch wieder abschalten. Und schwierig wird, dass ich kontrolliere, dass der maximale Strom nicht überschritten wird und die Zutaten der Schrittnummer nach dosiert werden. Manche Cocktails haben nur einen Schritt, in dem alles gleichzeitig rein kommt. Dann kann in einem Schritt vielleicht mehr Strom verbracht werden als zulässig, dann müssen in diesem Schritt die Zutaten nacheinander dosiert werden. Eine Zutat kann dann direkt dosiert werden, wenn Stromreserven frei sind. Ansonsten, ist ein Schritt erst zu Ende, wenn alle Zutaten des Schritts fertig dosiert sind.

Ich könnte die Einschaltzeiten,Schrittnummern und Stromverbräuche wieder in einem Dict im Cocktailautomaten sichern, wenn ich die Methode "Dosieren" aufrufe.
Dann eine Zeit gesteuerte Methode (While mit Wait im extra Thread oder Timer mit Callback??) würde das Ansteuern der RS232 Schnittstelle dann übernehmen. Was meint ihr?
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Da gibt es noch einiges zu verbessern. Unter anderem würde ich das Auslesen der Schlüssel und die Typumwandlungen in die jeweiligen Klassen verlagern. Als Beispiel für die Zutaten:

Code: Alles auswählen

class Zutat:
    def __init__(self, name, faktor, alkohol):
        self.name = name
        self.faktor = faktor
        self.alkohol_anteil = alkohol

    @classmethod
    def from_dict(cls, data):
        return cls(
            data['Name'],
            float(data['Faktor']),
            float(data['Alkohol [%]'])
        )

# Aufruf dann:
zutaten[row['Name']] = Zutat.from_dict(row)
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der Pfad fuer die Cocktails ist ganz bestimmt falsch - der PI laesst ja kein Windows laufen.

Generell ist es eine seltsame Entscheidung, explizit einen Cocktail mit Namen anzulegen, der dann ein dazugehoeriges Rezept einlesen will unter Verwendung genau diesen Namens. Stattdessen wuerde man ueber die Liste der Dateien im Cocktail-Verzeichnis iterieren, und alle dort praesentierten Cocktails einlesen.

__blackjack__ hat das glaube ich schon mehrfach angemerkt, aber diese super simplen Datenklassen wie Zutat & Co sind ein gefundenes Fressen fuer deklarative Ansaetze wie zB attrs oder auch das eingebaute collections.namedtuple.

Code: Alles auswählen

Zutat = collections.namedtuple("Zutat", "name faktor alkohol_anteil")
ist alles was man dann braucht. Von denen kann man ggf. auch ableiten, wenn denn noch Methoden die was machen dazu kommen:

Code: Alles auswählen

from collections import namedtuple


class Zutat(namedtuple("Zutat", "name alkoholgehalt")):

    def wasauchimmer(self):
        print("wasauchimmer", self)


rohrzucker = Zutat("rohrzucker", 0)
rohrzucker.wasauchimmer()
Und was dein Hauptproblem, das ansteuern des Mischers angeht: die Aufgabe, nur bei einer tatsaechlichen Aenderung etwas zu schreiben ist ja trivial, indem du dir einfach merkst, was beim letzten mal geschrieben wurde, und das dann nur tust, wenn es eine Aenderung gegeben hat. Allerdings verstehe ich dieses Requirement auch nicht so richtig, der Arduino selbst kann und sollte das erledigen, und nicht empfindlich darauf reagieren, wenn da mehrfach hintereinander das gleiche kommt. Wenn er das nicht tut, ist er eigentlich wertlos und man kann flexibler direkt mit den GPIOs arbeiten.

Das treiben des Mischers sollte durch eine Methode geschehen, die regelmaessig aufgerufen wird. Wie oft haengt ein bisschen davon ab, wie praezise die ganze Nummer denn sein muss . Ich kann mir aber nicht vorstellen, dass es da auf mehr als eine 10tel Sekunde ankommt. Ob diese Methode dann durch einen Thread oder einen Timer-Callback aufgerufen wird, ist eigentlich nebensaechlich.

Alternativ - und ich wuerde das 100%ig so machen - solltest du das aufbereitete Rezept (dazu gleich mehr) von dem garantiert ist, dass es das System nicht ueberlastet, einfach in einem Rutsch an den Arduino uebergeben. Denn der hat ein deutlich praeziseres Timing, und laeuft robuster. Damit musst du nur noch darauf warten, dass er Vollzug meldet. In Python (und Linux generell) ist es naemlich nicht garantiert, dass ein timer auch dann feuert, wenn man sich das wuenscht. Da kann es durchaus zu laengeren Verzoegerungen kommen im Ernstfall, und auch zB ein Absturz des Programms kann so nicht dazu fuehren, dass die Maschine einfach leerlaeuft. Ein simples Format, in dem du zb eine Liste von Timestamps und einzuschaltendenen Anschluessen kommunizierst, ist einfach implementiert und parsiert.

Womit wir zur groessten Aufgabe kommen, dem erzeugen der Zustandsliste mit Zeitstempeln. Egal ob man das aus dem Arduino komplett heraus macht, oder trotzdem auf den PI setzt, der Schritt bleibt gleich. Um dafuer ein besseres Gefuehl zu bekommen, haette ich gerne mal einen konkreten Cocktail, bei dem da gestaffelt und potentiell zu viel gleichzeitig gemischt werden muss. Hast du mal so ein aufbereitetes Rezept?
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nachtrag: "timer feuert" ist uebrigens KEIN Unterschied zu einem Thread & time.sleep. Aus Sicht des OS ist das beides mehr oder minder die gleiche Sache - weck den Prozess auf zu einem bestimmten Zeitpunkt. Sowas ist bei einem normalen Linux nicht garantiert, und selbst wenn man da auf PREEMPT_RT setzt, ist das in Konjuktion mit einer Sprache wie Python auch eher nicht wirklich sinnvoll einsetzbar.
undeat
User
Beiträge: 8
Registriert: Dienstag 3. Dezember 2019, 21:53

ich lese nur einen expliziten Cocktail ein, weil das Programm nach derzeitiger Planung nur einen Cocktail mixt und sich dann beendet. Den Namen werde ich in den Args übergeben. Das Programm wird wahrscheinlich von einem PHP file aufgerufen, wenn von der Webseite ein entsprechendes Req kommt. Ich nutze nicht die GPIOs vom PI, weil die beim Booten willkürlich angesteuert werden, bzw. die die es nicht tuen reichen nicht.

Klar könnte ich bei jeder Zutat in dem Rezept in der For Schleife direkt veranlassen, dass die RS232 schnittstelle sendet, aber dann würde die For Schleife viel zu lange laufen und es gibt keinen Sinn. Ich muss ich eh einen zweiten Task haben, der das Ansteuern der Ausgänge übernimmt, weil ich diese ja auch irgendwann wieder abschalten muss.

Die Echtzeitfähigkeit vom Linux ist ausreichend genug. Ich habe ja bereits alles am Laufen und bin begeistert wie gut das geht. Nur ich wollte es jetzt ordentlicher machen und von der SQL DB weg. Über den Schritt mit der DB habe ich viel nachgedacht und mir die Argumente angenommen, aber ich bin doch noch dafür es mit den CSV zu machen.

Wie viele Zutaten der PI gleichzeitig mischen muss ist so ziemlich egal, ich muss nach nur im Thread eine Forschleife über jeden Anschluss (16 stk) laufen lassen und prüfen ob die aktuelle Syszeit > als die Ausschaltzeit ist oder eben nicht und die tatsächlichen Einschalt Zeitpunkte müssen noch anhand des Schrittes bestimmt werden.

Ich wollte so wenig Logik wie möglich im Arduino haben, weil ich den nicht immer umflashen wollten. Das ist quasi nur ein Port Expander. Erst wollte ich eine I²C Relaiskarte nehmen aber da hatte damals irgendwas nicht geklappt. Ist schon ein paar Jahre her :)

Zum sonstigen Setup:

Da ist der RPi3, der über UART an einen Arduino zwei Byte schickt. Jedes Bit repräsentiert ein Relais, welches am Arduino angeschlossen ist. Der Arduino schaltet die Relais, die Relais schalten Ventile und durch Überdruck kommt die Flüssigkeit ins Glas. Auf dem RPI läuft ein webserver mit php und mysql. Die Rezept, Einstellungen etc. stehen alle zur Zeit in der DB. Der websever durchsucht die DB nach allen Cocktails und schaut, für welche Cocktails alle benötigten Zutaten auf den Anschlüssen liegen. Diese werden aufgelistet mit "Schaltflächen" (<input type="submit">). Wenn so eine Schaltfläche betätigt wird, dann wird die ID des zu mixenden Cocktails an php übergeben. Das Php Skript trägt diese id in eine sonst leere Tabelle in der DB ein. Das Python pollt diesen Eintrag und sobald eine ID vorhanden ist, wird der Cocktail aus der DB geladen und gemixt. Sollte der PI mal abstürzen (Was noch nicht passiert ist) habe ich noch einen Kill Schalter, der die Masse der Ventile weg schaltet.

Am aller besten wäre es, wenn mein Python Skript nachher gestartet werden kann und dann erstmal nichts macht. Es könnte dann auf sowas wie eine socket Verbindung auf einem Port reagieren und ich könnte nachher von der geplanten HTLM5 Seite direkt mit der Java Anwendung über die Socket Verbindung mit dem Python kommunizieren.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Dann würde ich dir als ersten Schritt empfehlen den Webserver mit PHP durch einen einfachen Webserver in Python zu ersetzen. Auch das Polling über eine Datenbank ist unnötig kompliziert. Warum rufst du nicht einfach das Python-Programm direkt auf, mit dem Cocktailnamen als Argument?

Ich verstehe auch nicht, warum du die Datenbank loswerden willst, wo du doch typische Datenbank-Aufgaben wie "Suchen nach allen Zutaten die in einem Cocktail vorkommen" hast.

Wie kommt jetzt im letzten Satz Java mit ins Spiel?
Benutzeravatar
__blackjack__
User
Beiträge: 13113
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Und für Datenbank spricht auch das man mit SQLAlchemy einen ähnlichen ”deklarativen” Ansatz für die Datenklassen hat wie mit `attr`.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das du einen nebenlaeufigen Thread oder Timer brauchst, wenn gleichzeitig das Programm nur in einem "fire-and-forget"-Modus gestartet wird, ist nicht schluessig. Und die Entscheidung, den Arduino "dumm" zu halten, ist auch nicht nachvollziehbar. Gerade wenn die Daten so wie von mir angemerkt geschlossen da raufgeschickt werden, kann man sich Python komplett sparen, und ALLES in PHP machen. Auch die Begruendung, das senden koste zuviel Zeit, immer zu senden, ist Unfug. Selbst bei einer absurd niedrigen Baudrate von 300 waeren das immer noch 18 Schaltvorgaenge pro Sekunde - mehr als ausreichend, um einen Cocktail zu mixen.

Wenn stattdessen PHP rausfliegt, und Python zB mit Tornado oder einem anderen async-faehigen Framework die Webseite liefert, kann es zeitgleich die Schaltvorgaenge durchfuehren, und Rueckmeldung geben via Websocket etc. Dann faellt allerdings wieder die Begruendung flach, das man ja nur *ein* Rezept einlesen wollen wuerde. Dann muessen das schon alle sein.
undeat
User
Beiträge: 8
Registriert: Dienstag 3. Dezember 2019, 21:53

Naja ich bin ja kein Programmier Guru und kenne schon lange nicht alle Möglichkeiten. Webserver mit Python z.B. hab ich noch nichts von gehört. Es darf ist ja auch nicht verwechselt werden, dass ich den Ist Zustand von der funktionierenden Maschine beschrieben habe und ich mich da für das Polling entschieden habe, weil ich nicht wusste ich das python script sonst starten soll. Jetzt weiß ich das es irgendwie geht und ich wollte genau so machen. Wobei mir ja lieber wäre, wenn das python Programm am laufen bleiben würde und ich nur eine Funktion aufrufe wenn ich tatsächlich mixen will.

Klar man könnte das ansteuern der Ventile auf den Arudino legen. Es ist historisch so gewachsten, weil der erst nicht eingeplant war. Ich hatte erst die gpios vom PI nehmen wollen. Das ging aber nicht weil die beim Booten an und aus gingen und erstmal als DO konfiguriert werden mussten, bevor sie dann wirklich aus waren. Dann wollte ich einen i²C Expander nehmen, damit kam ich auch nicht weiter weil irgendwas mit dem i²C nicht klappte. Dann habe ich quasi mit dem arduino einen UART Expander erzeugt.
Vor allem finde ich aber an der Lösung mit Python besser, dass ich einfach das Skript über eine Samba Freigabe tauschen kann und schon hab ich ein Update. Der Code wird mittlerweile noch von zwei Arbeitskollegen benutzt und die können gar nicht Programmieren geschweige denn einen Arduino flashen.

Viele Wege führen ja nach Rom. Mit irgendwelchen Attr hin und her in SQL kenne ich mich auch nicht aus.
Tornado schon mal gar nicht.

In meiner derzeitigen Datenbank Lösung gibt es die Tabelle Cocktails, die Tabelle Zutaten und eine CockZut. In der CockZut steht die ID vom Cocktail, die von der Zutat, die Menge und der Schritt. Genau so, wie in der CSV Datei ab Zeile 10. Die Zeile 4 und 5 und der Name stehen entsprechend in der Cocktail Tabelle. Jetzt hat ein son depp en Wodka aus den Zutaten gelöscht. Den konnte ich zwar dann neu anlegen, aber er hatte eine neue ID. Damit muss ich die Datenbank erstmal wieder "reparieren". FÜr mich mit phpmyadmin ging das aber einer der noch weniger Ahnung hat als ich kann das nicht. Gut der erste Fehler war vielleicht schon, dass ich im Programm überhaupt löschen angestoßen habe und nicht irgendein deleted flag gesetzt habe. Hinter ist man immer schlauer. Dann wollte jemand aber unbedingt nur "Seine" Cocktails auf einer feier anbieten. Also muss wieder pypmyadmin ran und dort die Tabelle sicher, clearen und die neuen Cocktails anlegen. Dazu gehört dann ja auch immer die CockZut Tabelle und zwischen einzelnen Backups darf sich nichts an den IDs ändern sonst passt die Zuordnung nicht mehr. Wie gesagt für jemanden der dann keine Ahnung hat ist es dann gar nicht möglich Backups zu machen es sei denn ich stelle Funktionen dafür auf der Webseite bereit. Jetzt Verknüpfe ich alles über den Namen und nicht mehr über IDs. Das hätte ich in der DB natürlich auch machen können. Hab ich dann aber nicht aufgrund der anderen Gründe die für mich gegen eine DB sprachen.

Auf java kam ich, weil ich mir mal kurz angeschaut habe, was es mit html 5 und webapps auf auf sich hat. Ich wollte daher eine webapp erstellen, die dann nicht ständig neue Anfragen an den Server schickt, sondern sich nur einmal läd und dann nur noch selten mit dem Server kommuniziert. Klar man könnte ja auch den Webclient mit dem Java alle Einschaltzeiten berechnen lassen und dass dann bereits aufbereitet an den PI schicken und der sendet das dann an den Arduino und der kümmert sich um das Timing. Oder das python Skript auf dem PI macht nur noch das ansteuern der Ausgänge.
Antworten