Strings an beliebigen Zeichen splitten

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
Benutzeravatar
Arthur Dent
User
Beiträge: 23
Registriert: Montag 12. September 2011, 09:51

Hallo

Ich hab mir gestern Abend mal überlegt einen simplen Parser für mathematische Ausdrücke zu schreiben. Ich weiß daß es da schon diverse fertige gibt, mir geht es aber im wesentlichen um den Lerneffekt und nicht darum, das Ding irgendwie zu benutzen.
Das erste Problem auf das ich dabei gestoßen bin ist den Ausdruck in sinnvolle "Päckchen" zu zerlegen. Mein erster Versuch war es das irgendwie mit regular expressions zu machen. An der Stelle hat mich die Doku aber mehr verwirrt als geholfen.
Ich hab mir jetzt ne Funktion zusammengebaut die über alle Zeichen des Ausdrucks iteriert und dann an beliebigen Zeichen trennt:

Code: Alles auswählen

def splitExpression(string, characters):
    """ splits a string by a sequence of characters """
    
    # convert the given expression into a list
    stringList = list(expression)

    # a container for the output
    output = ["["]
    
    """ remark:
        the expression must start with a spliting character.
        Therefore the first character of the output string is set to a opening square bracket
        Furthermore the set af splitting characters is extended by a square bracket. """
    
    # iterate over each character
    for char in stringList:
        # if actual character is a splitter append list
        if char in characters:
            output.append(char)
        # if actual character is not a splitter, check the prior one 
        else:
            # if the prior character is a splitter, append list
            if output[len(output)-1]  in characters:
                output.append(char)
            # if not, join the strings     
            else:
                output[len(output)-1] += char
                
    # Finaly the output ends with a closing square bracket            
    output.append("]")
            
    return output
Das Ergebnis passt auch:

Code: Alles auswählen

expression = "11+5/(548+6)-sin(8)"
splitter = ["+", "-", "(", ")","/", "*" ]
result = splitExpression(expression, splitter)
print result

>>> 
['[', '11', '+', '5', '/', '(', '548', '+', '6', ')', '-', 'sin', '(', '8', ')', ']']
Aber das geht doch bestimmt irgendwie besser !?

Mag mir vieleicht grad mal jemand zeigen wie das mit den regex richtig gemacht wird?

Gruß Arzhur Dent
Optimismus ist, bei Gewitter auf dem höchsten Berg in einer Kupferrüstung zu stehen und "scheiß Götter" zu rufen

Terry Pratchett
BlackJack

@Arthur Dent: Erst einmal funktioniert die Funktion nur wenn man ausserhalb der Funktion auf Modulebene auch etwas an den Namen `expression` gebunden hat. Solche Fehler passieren wenn man Code auf Modulebene hat, der etwas anderes macht ausser Funktionen, Klassen, und andere "Konstanten" zu definieren.

Wenn man das "zu Fuss" macht, würde man einfach immer solange Zeichen sammeln bis man an ein Trennzeichen gekommen ist. Dann muss man nicht in dem bisher geparsten `output` wieder zurück schauen ob das letzte ein Trennzeichen war und auch keine "künstlichen" Trennzeichen dort hinein schreiben.

Solange Deine Trennzeichen wirklich nur einzelne Zeichen sind, geht zum Beispiel dieser reguläre Ausdruck:

Code: Alles auswählen

In [115]: re.findall(r'[^-+*/()]+|.', expression)
Out[115]: ['11', '+', '5', '/', '(', '548', '+', '6', ')', '-', 'sin', '(', '8', ')']
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Hui... da sind einige Dinge drin, die nicht wirklich pythonisch sind ;-)

Fangen wir erst einmal mit der `re`-Funktionalität an. Generell sind BlackJack und sma hier die `re`-Junkies, speziell sma zaubert immer wieder herrliche RegExps aus dem Hut. Was Du gebaut hast, nennt man übrigens "Lexer". sma hatte mal einen kleinen BASIC-Interpreter in Python gebaut; in diesem Screencast siehst Du gut, wie man einen solchen Lexer-RegExp aufbauen kann.

Hier mal mein (naiver) Ansatz:

Code: Alles auswählen

In [66]: import re

In [67]: re.compile("\d+|\w+|\+|-|\(|\)|/")
Out[67]: <_sre.SRE_Pattern object at 0x9c2be08>

In [68]: TOKENS = re.compile("\d+|\w+|\+|-|\(|\)|/")

In [69]: expression = "11+5/(548+6)-sin(8)"

In [70]: re.findall(TOKENS, expression)
Out[70]: ['11', '+', '5', '/', '(', '548', '+', '6', ')', '-', 'sin', '(', '8', ')']
Das sollte mit Deinem Ergebnis übereinstimmen - nur ist es kürzer :-D

Eigentlich musst Du Dir nur mal die Doku zum `re`-Modul angucken. Dort ist auch auf einen längeren externen Artikel verlinkt, der eine gute Einführung für Anfänger ist. Generell finde ich die Artikel von Doug Hellmann auch toll: Link

Mein Lexer basiert auf einem re-Ausdruck, der lauter Alternativen `|` für ein Suchmuster anbietet. Der Eingabestring wird nun Zeichen(gruppe) für Zeichen durchsucht und dabei werden sukzessive die verschiedenen Muster geprüft. Als erstes teste ich auf Zifferngruppen (für Zahlen), dann auf Wörter (für Funktionen wie "sin"), usw.

Haken an diesem Ansatz: Ungültige Zeichen(gruppen) werden einfach ignoriert, d.h. man erhält so keinen Syntaxcheck:

Code: Alles auswählen

In [73]: expression = "11!2"

In [74]: re.findall(TOKENS, expression)
Out[74]: ['11', '2']
Das ist natürlich schlecht. Da bin ich mir im Moment auch nicht sicher, wie man das am besten lösen kann. Bei Dir würde es ggf. reichen, das Ergebnis wieder zu "joinen" und mit der Eingabeexpression zu vergleichen:

Code: Alles auswählen

In [116]: expression = "11!2"

In [117]: res = re.findall(TOKENS, expression)

In [118]: "".join(res)
Out[118]: '112'

In [119]: "".join(res) == expression
Out[119]: False
Damit wüsste man, dass ein ungültiges Zeichen in dem Ausdruck enthalten war. Bei komplexeren Sprachen ist das natürlich nicht zu empfehlen - zudem ist es ziemlich unperformant.

Kurz noch zu Deinem Code:

- Beachte PEP8. Vor allem bei Deinen Namen. Die sehen nach Java aus ;-)

- Strings sind Iterables! `list(expression)` ist somit unnötig.

- Dieses Konstrukt `output[len(output)-1]` ist unschön. Du kannst auch so slicen:

Code: Alles auswählen

In [121]: expression
Out[121]: '11!2'

In [122]: expression[-1]
Out[122]: '2'
- Strings sollte man nicht per "+" zusammensetzen. Merke sie Dir in einer Liste und füge sie am Schluss per "".join() zusammen.


Ok, BlackJack war schneller und auch sinnvoller - naja, ich hab so viele Infos hier drinnen, da poste ich es dennoch ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
Arthur Dent
User
Beiträge: 23
Registriert: Montag 12. September 2011, 09:51

Vielen Dank für die Hinweise. So hatte ich mir das Vorgestellt.
@Arthur Dent: Erst einmal funktioniert die Funktion nur wenn man ausserhalb der Funktion auf Modulebene auch etwas an den Namen `expression` gebunden hat. Solche Fehler passieren wenn man Code auf Modulebene hat, der etwas anderes macht ausser Funktionen, Klassen, und andere "Konstanten" zu definieren.
Natürlich nicht !
Ich Hatte das nur mal schnell in die Shell getippt um zu sehen ob es prinzipiell das macht was ich erwarte. Zwischendurch hab ich noch mal die Namen der Bezeichner geändert, da hab ich wohl einen vergessen. Danke für den Hinweis.
Man sollte wohl nach Weihnachtsmarkt mit Glühwein lieber nicht mehr über sowas Nachdenken :wink:

- Strings sind Iterables! `list(expression)` ist somit unnötig.
Alles Roger!
Hatte das in meinem Eifer verwechselt. Meine Erste Idee war es nem String direkt zu beeinflussen und an jedem Übergang ein Leerzeichen Einzufügen um dann hinterher am Leerzeichen zu splitten. Das geht aber nicht weil Strings nicht veränderbar sind. Irgendwie war ich dann der Meinung ich muss den unbedingt in eine Liste umwandeln. Schnapsidee :K
- Dieses Konstrukt `output[len(output)-1]` ist unschön. Du kannst auch so slicen:

Code: Alles auswählen

In [121]: expression
Out[121]: '11!2'

In [122]: expression[-1]
Out[122]: '2'
Ich fands auch unschön.
Danke für den Tip, kannte ich noch nicht.

Was Codingstyle angeht habt ihr natürlich recht. Ich möchte jedoch anmerken, daß ich kein Programmierer bin, und das eher so Hobbymäßig mache. Ich geb mir trotzdem alle Mühe lesbaren Code zu schreiben. Vieleicht schau ich trotzdem mal öfter in den Styleguide.

Habt nochmal Dank für eure Antworten!
Ich wünsche schonmal ein frohes Fest,
Gruß Arthur!
Optimismus ist, bei Gewitter auf dem höchsten Berg in einer Kupferrüstung zu stehen und "scheiß Götter" zu rufen

Terry Pratchett
Benutzeravatar
/me
User
Beiträge: 3555
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Arthur Dent hat geschrieben:Was Codingstyle angeht habt ihr natürlich recht. Ich möchte jedoch anmerken, daß ich kein Programmierer bin, und das eher so Hobbymäßig mache.
"Was das Abschalten der Sicherung angeht habt ihr natürlich recht. Ich möchte jedoch anmerken, dass ich kein Elektriker bin, und das eher so Hobbymäßig mache." :shock:
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

@ Hyperion Um ungültige Zeichen zu finden, füge einfach noch ein catch-all "." zum RE hinzu und prüfe dann, ob dieser getroffen wurde:

Code: Alles auswählen

def make_lexer(source):
    for m in re.finditer(r"(\d+|\w+|[-+*/()])|.", source):
        if m.group(1):
            yield m.group()
        else:
            raise SyntaxError(m.group())
Den selben Trick kann man auch benutzen, um Token-Typen (Zahl, Name, Syntax) zu unterscheiden:

Code: Alles auswählen

def make_lexer(source):
    for m in re.finditer(r"(\d+)|(\w+)|([-+*/()])|.", source):
        if m.lastindex:
            yield m.lastindex, m.group()
        else:
            raise SyntaxError(m.group())
Stefan

PS: In Python sind REs ein recht effizienter Weg, Strings aufzubrechen. In anderen compilierten Sprachen mag es anders sein. Häufig werden aber dort (z.B. bei modernen JavaScript-Engines) auch die REs in Maschinencode kompiliert.

Stefan
Benutzeravatar
Arthur Dent
User
Beiträge: 23
Registriert: Montag 12. September 2011, 09:51

/me hat geschrieben: "Was das Abschalten der Sicherung angeht habt ihr natürlich recht. Ich möchte jedoch anmerken, dass ich kein Elektriker bin, und das eher so Hobbymäßig mache." :shock:
Sicherlich ein witziges Gleichnis, allerdings hinkt es ein bisschen.
Ich maße mir schließlich nicht an irgendjemanden zu verpflichten meinen Code zu benutzen, vor allem nicht für Geld.
Wenn sich jemand einen Amateur holt um professionelle Arbeit erledigen zu lassen ist er halt selbst schuld wenns hinterher Ärger gibt.

Ich kenn das zu genüge aus meinem Beruf (ich bin Baustatiker). Bauen kann halt jeder, aber wenn sich hinterher die Decke bis zum Fußboden durchbiegt ist das geheule groß.

ps.

ist Nur ne Anmerkung, bitte nicht als Genörgel oder Rechtfertigung auffassen.
Optimismus ist, bei Gewitter auf dem höchsten Berg in einer Kupferrüstung zu stehen und "scheiß Götter" zu rufen

Terry Pratchett
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

@sma: Narf. Danke für den Tipp. Ich habe immer mit `.*` rumgebastelt, als ich das obige Posting erstellte - dass ein (ungültiges) Zeichen reicht, um den Syntaxfehler zu erkennen, habe ich total verpeilt.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Antworten