suche Ausgabe: int(List[index_of_character_*])

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.
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

Hallo, ich stecke gerade in meinem Programm fest, kann mir jemand bitte weiterhelfen?

Ich habe mehrere Rezepte die dieselbe Struktur aufweisen. Ich lese sie als Element einer Liste, d.h.: eine verkürzte Version sieht so aus:
recipe = ['Preparation time: 1 hour', '*\t5\t\tcarrots', '*\t1\tpinches\tsalt', 'Bake for 40 minutes.']
Im Original so:
Preparation time: 1 hour
* 5 carrots
* 1 pinches salt
Bake for 40 minutes.
Nun möchte ich recipe[3:] ausgeben, um nur die Anleitungen für das Rezept auszugeben ("Bake for ..."). Dafür habe ich recipe.reverse() durchgeführt, um das erste Element mit "*" zu finden und als Index anzuwenden, d.h. dann recipe[:index_of_*] und am Schluss nochmals ein [::-1] auf das Resultat, um die Anleitung in der richtigen Reihenfolge auszugeben.
Problem:

Code: Alles auswählen

recipe[index_of_*]
funktioniert nicht. Ich habe .index(*), .startswith(*), .find(*), any("*") und re.findall("*", recipe) ausprobiert und habe es noch nicht lösen können. Würde ich den ganzen String einlesen funktionieren diese Methoden, aber nicht wenn ich nur "*" von "*\t..." einlesen möchte.

Vielen Dank für eure Hilfe.
"Those who can imagine anything, can create the impossible." Alan Turing
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du mußt die Liste Element für Element durchgehen und mit startswith prüfen.
Z.B. so:

Code: Alles auswählen

list(itertools.dropwhile(lambda s: s.startswith('*'), recipe[1:]))
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

@Sirius3 Lieber Sirius3 vielen Dank für deine Rückmeldung, gute Idee, aber dein Code funktioniert aber auch nicht.
Ich weiss nicht, ob dies eine Rolle spielt, aber mein Code liegt in der Specialmethod __iter__() und wird in meinem main() als iter(a_recipe_name) aufgerufen. Die Methode __next__() habe ich noch nicht implementiert. Hast du evtl. noch einen anderen Tipp?
"Those who can imagine anything, can create the impossible." Alan Turing
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

Vielleicht hilft dies, so sieht mein Code aus:
Es kommen später noch weitere Sachen dazu, deswegen muss es im __iter__() sein.

Code: Alles auswählen

#class Recipe:

def __init__(self, recipe: str):
        self.recipe_string = recipe
        self.recipe_list = recipe.split('\n')
        
def __iter__(self) -> Iterator[str]:
        """Iterate over recipe instructions."""
        rev_list = self.recipe_list
        rev_list.reverse()
        new_list = []
        while "" in rev_list: #remove empty List elements
                rev_list.remove("")
                
       #NEXT LINE IS THE PROBLEM  -> replace "3" with index of first element in the List "rev_list" starting with a "*"
       res = rev_list[:3] #have to find the correct index for cutting -> on the first "*"
       res = res[::-1] #reverse list again
       return res
       
def __next__(self):
       pass

def main():
        iter(a_recipe_name)
        #next(iter(a_recipe_name))
"Those who can imagine anything, can create the impossible." Alan Turing
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Warum ist die Klasse-Zeile auskommentiert? Die Einrückungen sind kaputt. Zudem werden immer 4 Leerzeichen pro Ebene als Einrückung benutzt.
Das Rezept ist entweder eine Liste, oder ein String, aber warum speicherst Du beides?
Wenn Du Zeilen hast, dann ist split die falsche Methode, sondern spitlines. Dann hast Du auch keine unsinnigen leeren Strings, hoffentlich.
Warum benutzt Du einmal reverse und einmal [::-1]? Es gibt einen Grund dafür, aber bei Dir ist es genau falsch.
Warum liefert __iter__ die Anleitung und nicht die Zutaten? Dafür __iter__ zu nehmen, ist verwirrend, dafür würde man eine eigene Methode iter_instructions verwenden.
Und das sollte dann keine Liste sondern einen Iterator zurückliefern.
__next__ macht in der Klasse Recipe auch keinen Sinn.

Und zum Problem: wie hast Du versucht dropwhile einzubauen?
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

@Sirius3 Die Klasse ist in meinem Programm nicht auskommentiert. Ich habe es nur zur Vereinfachung weggelassen, um das main() mit der Objektinstanzierung für das Beispiel hier kürzer zu machen. Es gibt noch weitere Funktionen, welche ich hier auch "Einfachheitshalber" weggelassen habe, aber jetzt geht es nur um das __iter__()
"Those who can imagine anything, can create the impossible." Alan Turing
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

Sirius3 hat geschrieben: Donnerstag 17. Juni 2021, 11:59 -> die Klasse ist korrekt implementiert. Ich habe sie hier nur weggelassen, um das main() kürzer darzustellen. Ich habe sie als Info als Kommentar hinzugefügt, weil die anderen Funktionen wie __init__() usw. in einer Klasse Sinn machen.
-> Einrückungen: als ich hier meine Frage gepostet habe sind die Einrückungen etwas durcheinander geraden, in meinem Programm sind sie richtig
-> je nach Funktion brauche ich das Rezept als Liste oder String, deswegen habe ich noch beide drin.
-> Danke für "splitlines" :)
-> Ich benutze einmal .reverse() und einmal [::-1]. Ursprünglich wollte ich zwei Mal das selbe nehmen, aber es hat nicht funktioniert, deswegen habe ich zu zwei verschiede gewechselt, weil es so funktioniert. Weisst du wieso das so ist? Und wieso ist sie bei mir falsch?
-> eine andere Funktion berechnet bei mehrere Rezepten, wie viel von jeder Zutat benötigt wird, deswegen liefert __iter__ die Anleitung und nicht die Zutaten. Für Zutat ist get_shopping_list() zuständig und liefert eine Liste an Zutaten.
-> ich bin mir eben noch noch nicht sicher, ob ich eine __next__() brauche. Ich habe diese hinzugefügt, weil ich gelesen habe, dass y = iter(x) immer auch ein next(y) braucht. Deswegen steht momentan noch ein "pass" drin.
"Those who can imagine anything, can create the impossible." Alan Turing
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du hast nichts weggelassen, sondern eine Zeile auskommentiert, ohne die __init__, __iter__ und __next__ keinen Sinn machen. Ob da noch weitere Methoden sind, macht doch für die Frage gar keinen Unterschied. Dass `a_recipe_name` nicht definiert ist, da hast Du also zu viel gekürzt.

Ein vollständiges Minimalbeispiel könnte so aussehen:

Code: Alles auswählen

class Recipe:
    def __init__(self, recipe):
        self.recipe = recipe
        
    def __iter__(self):
        """Iterate over recipe instructions."""
        rev_list = self.recipe
        rev_list.reverse()
        new_list = []
        while "" in rev_list: #remove empty List elements
                rev_list.remove("")                
       #NEXT LINE IS THE PROBLEM  -> replace "3" with index of first element in the List "rev_list" starting with a "*"
       res = rev_list[:3] #have to find the correct index for cutting -> on the first "*"
       res = res[::-1] #reverse list again
       return res
       
def main():
    recipe = Recipe([
        'Preparation time: 1 hour',
        '*\t5\t\tcarrots',
        '*\t1\tpinches\tsalt',
        'Bake for 40 minutes.'
    ])

    for line in recipe:
        print(line)

if __name__ == "__main__":
    main()
Und das enthält auch all die Punkte, die ich beschrieben habe.
Wie sieht also der Code aus, den Du benutzt versucht hast?
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

@Sirius3 So sieht mein Ausführungsversuch deines Code-Tipps:

Code: Alles auswählen

>>> import itertools
>>> all = ['ba', 'ab', 'ab2', 'c', 'de', 'de', 'ba', 'ab']
>>> part = list(itertools.dropwhile(lambda s: s.startswith("a"), all[1:]))
>>> part
['c', 'de', 'de', 'ba', 'ab']
>>> i = part[0]
>>> i
'c'
>>> res = all.index(i)
>>> res
3
>>> all[res:]
['c', 'de', 'de', 'ba', 'ab']
>>> all[:res]
['ba', 'ab', 'ab2']
"Those who can imagine anything, can create the impossible." Alan Turing
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Und das heißt jetzt?
Warum benutzt Du index? `part` enthält doch schon das, was Du möchtest.
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

@Sirius3 #Danke für deine Ratschläge. Ich möchte zuerst das Problem lösen, danach optimiere ich "splitlines" usw.

Code: Alles auswählen

import itertools
from typing import Union, Iterator, Dict
from pathlib import Path

class Recipe:

    def __init__(self, recipe: str):
        self.recipe_string = recipe
        self.recipe_list = recipe.split('\n')
        
    def __iter__(self) -> Iterator[str]:
        """Iterate over recipe instructions."""
        while "" in self.recipe_list: #remove empty List elements
            self_recipe_list.remove("")
                
    #NEXT LINE IS THE PROBLEM  -> replace "3" with index of first element in the List "rev_list" starting with a "*"
        #res = self.recipe_list[:3] #have to find the correct index for cutting -> on the first "*"
        part = list(itertools.dropwhile(lambda s: s.startswith("*"), self.recipe_list[4:]))
        cutting_index = part[0]
        res = self.recipe_list.index(cutting_index)

        res = res[::-1] #reverse list again
        return res
       
    def __next__(self):
        pass

def main():
    paths = Path('C:/.../recipes') #es steht ein richtiger Path in meinem Programm
    all_recipes = []
    for a_file in paths.rglob('*.txt'):
        res = open(a_file, mode = 'r', encoding = 'utf-8').read()
        all_recipes.append(Recipe(res))

    carrot_recipe = all_recipes[0]
    iter(carrot_recipe)
    #next(iter(a_recipe_name))
"part" gibt nur den richtigen Output aus, wenn ich "[4:]" Element nehme. Es ist nicht der Sinn, dass ich manuell "4" eingebe, da es schliesslich auch für andere Rezepte, welche mehr oder weniger Zutaten, Anweisungen, usw. haben funktionieren soll. Mit Eingabe "[4:]" kommt der richtige Output:
>>>part #['Preheat oven to 200ºC. ', 'In a medium casserole dish, evenly coat the carrots', 'in butter, honey, salt, and pepper.', 'Bake for 25-30 minutes.']
>>>res #9
"Those who can imagine anything, can create the impossible." Alan Turing
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

Es hat bis jetzt nur so funktioniert, aber dies ist keine saubere Programmierung bzw. elegante Lösung. Hast du einen Tipp?

Code: Alles auswählen

...
def __iter__(self):
        while '' in self.recipe_list:
            self.recipe_list.remove('')

        for ele in self.recipe_list[::-1]:
            if ele.startswith("*"):
                i = int(self.recipe_list.index(''.join(list(ele))) + 1)
                return iter(self.recipe_list[i:])
 ...
"Those who can imagine anything, can create the impossible." Alan Turing
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Ich kann halt nur für das Beispiel, das Du angegeben hast, ein Lösung zeigen. Dafür funktioniert es. Was Du tatsächlich als Werte hast, kann ich ja nicht wissen.
Probleme am Code repariert man am besten sofort. Dazu gehört die doppelte Datenhaltung, das falsche Remise an der falschen Stelle und auch der falsche Rückgabewert von __iter__ ,mit dem das Programm ja erst gar nicht läuft.
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

@Sirius3 Vielen Dank für deine Hilfe. Das Problem hat sich soweit gelöst, zwar noch unschön. Ich werde die Punkte, die du erwähnt hast noch versuchen umzusetzen. Ich verstehe, dass deine Lösung für die "Vereinfachung" hier funktioniert, doch es ging mir darum, wenn die Daten beispielsweise wechseln, dass der Code immer noch läuft. Der Code soll schliesslich nicht an einer Datei gebunden sein (jedes Rezept hat zwar die gleiche Struktur, doch manche haben mehr unterschiedliche Zutaten, Answeisungen usw. als andere), sonst macht es ja auch keinen Sinn ein Programm dafür zu schreiben. Aber die Diskussion hier hat mir geholfen zu finden, wo noch Schwachstellen und Verbesserungspotential sind, danke :)
"Those who can imagine anything, can create the impossible." Alan Turing
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

So wie Du das gelöst hast, ist es im Prinzip schon in Ordnung, wenn es wirklich darum geht, die letzte Zeile mit einem * zu finden.
Was umständlich ist, ist eine Zeile (die Du aus irgend einem Grund ele nennst, wie Elefant) in eine Liste aus einzelnen Zeichen umzuwandeln, um diese dann sofort wieder mit join in den selben String umzuwandeln.
Was auch umständlich ist oder besser gesagt falsch, ist, den Index anhand des Strings zu suchen, statt ihn gleich mitzuzählen. index liefert schon ein int, das per int in ein int umzuwandeln ist unsinnig.
Was falsch ist, ist das Rezept in zwei Varianten zu speichern und innerhalb einer __iter__-Methode die Liste auch noch zu ändern.
Unnötig teuer ist es, eine Liste mit umgekehrten Reihenfolge zu erzeugen, statt einfach reversed zu benutzen.

Code: Alles auswählen

from itertools import islice

class Recipe:
    def __init__(self, recipe: str):
            self.recipe = recipe.splitlines()

    def __iter__(self):
        for index, line in enumerate(reversed(self.recipe)):
            if line.startswith("*"):
                return islice(self.recipe, len(self.recipe) - index, None)
Das generelle Problem ist, dass Du die Methode __iter__ benutzt, um einen ganz spezifischen Teil Deines Rezeptes zurückzugeben. Das erwartet niemand, dafür sollte es eine spezielle Funktion geben. Noch besser wäre es, das Rezept gleich in __init__ in die einzelnen Teile zu parsen.
GabrielleChanel
User
Beiträge: 42
Registriert: Dienstag 13. April 2021, 11:54

@Sirius3 Oh danke, das ist eine super Idee das Rezept gleich in __init__ in die einzelnen Teile zu parsen, das macht alles viel überschaubarer und kann auch einfacher wieder verwendet werden, ohne dublizieren zu müssen.
Und ja genau, es geht darum die letzte Zeile mit einem * zu finden. Und sry, "ele" hatte ich als kürzere Form von "element" verwendet. Meine Version sieht ein bisschen "gebastelt" aus, ich habe join hinzugefügt, weil es alls Zeichen teilte, deswegen. Aber ich werde es wieder "entbasteln", sauberer machen.
Und danke, das mit dem zwei Mal speichern war mir vor unseren Diskussion gar nicht bewusst. Und super wegen "reversed" ich hatte .reverse() verwendet was nicht funktionierte und dann mit [::-1] ersetzt, aber stimmt, dass ist unnötig teuer. Danke dir. Und danke auch für deine Beispielcodes.
"Those who can imagine anything, can create the impossible." Alan Turing
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

IMHO noch besser das Parsen nicht in der `__init__()` zu machen sondern in einer Funktion oder Klassenmethode. Dann kann man `Recipe`-Objekte auch aus anderen Quellen erstellen und/oder für Tests einfach so direkt erstellen, ohne das man das parsen immer mittesten muss.

Beispiel:

Code: Alles auswählen

#!/usr/bin/env python3
import re
from datetime import timedelta as TimeDelta

from attr import attrib, attrs
from more_itertools import split_when


EXAMPLE_RECIPE = """\
Preparation time: 1 hour
*\t5\t\tcarrots
*\t1\tpinches\tsalt
Bake for 40 minutes.
"""

PREPARATION_TIME_RE = re.compile(r"Preparation time: (?P<hour>\d+) hour")


@attrs(frozen=True)
class Recipe:
    preparation_time = attrib()
    ingredients = attrib()
    instructions = attrib()

    @classmethod
    def parse(cls, text):
        (
            preparation_time_lines,
            ingredient_lines,
            instruction_lines,
        ) = split_when(
            text.splitlines(),
            lambda line_a, line_b: (
                line_a.startswith("*") + line_b.startswith("*") == 1
            ),
        )
        if len(preparation_time_lines) != 1:
            raise ValueError(
                f"more than one line for preparation_time:"
                f" {preparation_time_lines}"
            )

        match = PREPARATION_TIME_RE.fullmatch(preparation_time_lines[0])
        if not match:
            raise ValueError(
                f"preparation time has unexpected format:"
                f" {preparation_time_lines[0]!r}"
            )

        preparation_time = TimeDelta(hours=int(match.group("hour")))

        # ...

        return cls(preparation_time, ingredient_lines, instruction_lines)


def main():
    recipe = Recipe.parse(EXAMPLE_RECIPE)
    print(recipe)
    for line in recipe.instructions:
        print(line)


if __name__ == "__main__":
    main()
Das parsen ist wahrscheinlich noch nicht vollständig. Die Zubereitungszeit wird wahrscheinlich nicht nur in Stunden angegeben. Aus den Zutaten möchte man vielleicht die "*" entfernen den Rest vielleicht in Anzahl, Einheit, und Bezeichnung aufteilen und in einem eigenen Typ speichern. Und die Zubereitung vielleicht dann wieder als eine Zeichenkette, statt einzelnen Zeilen speichern.

Ausgabe von dem Beispiel:

Code: Alles auswählen

$ ./forum22.py 
Recipe(preparation_time=datetime.timedelta(0, 3600), ingredients=['*\t5\t\tcarrots', '*\t1\tpinches\tsalt'], instructions=['Bake for 40 minutes.'])
Bake for 40 minutes.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Ich frage mich die ganze Zeit warum ihr das nicht mit einer list comprehension macht.
Habe ich etwas missverstanden?
Es geht doch nur darum, das nächste Listenelement nach der letzten Zeile, die mit '*' beginnt auszugeben, oder?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@rogerb: Ich vermute die *eine* Zeile ist nur als Beispiel und die Anweisungen sind länger. Wie würdest Du das mit einer list comprehension machen? Die sind dazu da um Listen zu erstellen. Hier soll ja aber eine Liste eher zerlegt werden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Wenn es darum geht eine Liste nach einfachen Regeln in eine neue Liste umzuwandeln, währe mein erster Ansatz immer eine list comprehension, (bzw. analog generator expression oder dictionary comprehension)

Code: Alles auswählen

lines_after_ingredients = next(recipe[index+1:] for index, (prev_line, next_line) in enumerate(zip(recipe[:-1], recipe[1:])) if prev_line.startswith("*") and not next_line.startswith("*"))
Das geht wahrscheinlich noch in vielen weiteren Varianten mit itertools, usw.

nach etwas Überlegung, würde ich die Daten aber wohl eher in einen Dictionary parsen:

Code: Alles auswählen

recipe_dict = {"remarks": [], "ingredients": [], "instructions": []}
Da hätte ich dann viele Möglichkeiten die Daten weiterzuverarbeiten.
Das in etwa soll wohl auch deine Klasse anbieten. Ich persönlich würde den dictionary aber aufgrund seiner Einfachheit vorziehen.
Antworten