(Python) Zuweisung in IF
Harlequin
- programmiertechnik
0 dedlfix4 Tim Tepaße
Yerf!
Ich steh grad auf'm Schlauch...
Python-Manual sagt:
"Note that in Python, unlike C, assignment cannot occur inside expressions. C programmers may grumble about this, but it avoids a common class of problems encountered in C programs: typing = in an expression when == was intended."
Wie kann ich trotzdem so etwas in der Art elegant lösen:
if (m = re.search('^\s*String (\w)*\s*{\w*$', line)) != None:
identifier = m.group(1)
elif (m = re.search('^\s*Text [(\w*)] (\w)*\s*{\w*$', line)) != None:
text = m.group(1);
content = m.group(2);
[...weitere elif dieser Art]
Was ich damit möchte: ich muss ein Textfile parsen und dabei abhängig vom Type der Zeile (erkenne ich am Aufbau) mir bestimmte Teile herauskopieren.
Derzeit benutze ich eben einen RegExp und Search um den Aufbau der Zeile zu ermitteln (es gibt einen Match, wenn der Aufbau passt) und möchte mir dann aus den Groups die gesuchten Informationen holen. Das Problem ist aber das der oben beschriebene Code so nicht funktioniert, da die Zuweisung des MatchObjektes nach m im IF nicht möglich ist. Muss ich wirklich den Search 2 mal ausführen, einmal um zu ermitteln ob er passt und dann im Körper des IF nochmal um an die Groups zu kommen?
Gruß,
Harlequin
echo $begrüßung;
Wie kann ich trotzdem so etwas in der Art elegant lösen:
if (m = re.search('^\s*String (\w)*\s*{\w*$', line)) != None:
identifier = m.group(1)
elif (m = re.search('^\s*Text [(\w*)] (\w)*\s*{\w*$', line)) != None:
text = m.group(1);
content = m.group(2);
[...weitere elif dieser Art]
Da fällt mir momentan nur das ein:
m = re.search('^\\s*String (\\w)*\\s*{\\w*$', line)
if m is not None:
identifier = m.group(1)
else:
m = re.search('^\\s*Text [(\\w*)] (\\w)*\\s*{\\w*$', line)
if m is not None:
text = m.group(1)
content = m.group(2)
else:
pass # usw.
Nebenbei: Lass doch die Semikolons weg, wenn du nur eine Anweisung in der Zeile hast.
echo "$verabschiedung $name";
Yerf!
Da fällt mir momentan nur das ein:
Hm, aber das wird ne recht üble Schachtelungstiefe, da gibts so 10 Fälle (grob geschätzt)...
Hm, vielleicht fällt mir nochwas ein.
Nebenbei: Lass doch die Semikolons weg, wenn du nur eine Anweisung in der Zeile hast.
Die wird er mir beim Ausführen doch eh als Fehler anmeckern... das war grad mal so auf die Schnelle hier reingetippt und mein übliches Problem, da ich ansonsten C# programmiere...
Gruß,
Harlequin
echo $begrüßung;
Nebenbei: Lass doch die Semikolons weg, wenn du nur eine Anweisung in der Zeile hast.
Die wird er mir beim Ausführen doch eh als Fehler anmeckern...
Nö, sie gehören zur Syntax, sind aber optional und im Prinzip nur bei mehreren Anweisungen pro Zeile sinnvoll.
if foo: bar(); baz()
echo "$verabschiedung $name";
Hallo Harlequin,
Wie kann ich trotzdem so etwas in der Art elegant lösen:
Eleganz bedeutet manchmal für unterschiedliche Leute unterschiedliche Dinge. Manchmal ist es möglich nah dran am konkreten Ablauf der Dinge dran zu sein wie in Deiner Schleife, manchmal bedeutet es, das eigene Denken im Programmcode wiedergespiegelt zu sehen. Ich neige in den letzten Jahren instinktiv immer zu einem etwas funktionaleren Programmierstil. Python kommt mir da sehr entgegen, auch mit den nicht ganz so funktionalen Bestandteilen wie Generatoren. Aber das ist eine Sichtweise von Eleganz in Python; es gibt auch andere.
Ich würde so vorgehen:
• Du hast offenkundig einen Iterator, der Zeilen ausspuckt.
• Für jede dieser Zeilen möchtest Du eine _Liste_ von Mustern ausprobieren.
• Vom ersten Muster aus dieser Liste, das passt, möchtest Du bestimmte _benannte Submuster_ zurück geben.
Um das ganze mal umgekehrt aufzubauen: Das erste Stichwort ist „benanntes Submuster“. Man kann ja in Regexps mit (?P<name>...) Gruppen benennen. Das kann man toll nutzen, in Deinem zweiten Regexp z.B. so:
re.compile(r'^\s*Text [(?P<text>\w*)] (?P<content>\w)*\s*{\w*$')
Das Schöne daran ist, dass man aus dem Match-Object mit der Methode m.groupdict() ein Dictionary mit den gefundenen Sub-Mustern herausbekommt. Beachte auch das r vor dem Regexp-String. Das zeigt an, dass der String ein sogenannter Rawstring ist, was heisst, dass in diesem String Maskierungen mit dem Backslash nicht interpretiert werden. Man erspart sich dadurch die Verdoppelung der Backslashs; Rawstrings sind also sehr für Regexps beliebt.
Zweiter Punkt: Du hast eine „Liste“ von Suchmustern. Es macht oftmals mehr Spaß, tatsächlich eine Liste zu benutzen als die verschiedenen gleichen Teile an unterschiedlichen Quellen im Code zu verstreuen. Unter anderem, weil man auf dieser Liste mit anderem Code operieren kann. Mal beispielhaft:
~~~python
patterns = [
('string', r'^\sString (?P<identifier>\w)\s*{\w*$'),
('text' , r'^\sText [(?P<text>\w)] (?P<content>\w)\s{\w*$'),
('...' , r'...')
]
Ich hab hier eine Liste von Zweier-Tupeln, das erste Element ist ein kurzes Label, das zweite das Suchmuster. Das Label ist eigentlich unnötig; ich pack's trotzdem mal rein zur Selbstdokumentation und zum anderen, falls man das Label später noch mal braucht, z.B. als der erkannte Typ der zum Suchmuster passenden Zeile. Ich verwandel diese Datenstruktur gleich mal daraufhin mit einer list comprehension etwas Praktischeres:
`patterns = [(label, re.compile(pattern)) for label, pattern in patterns]`{:.language-python}
Warum? Kompilierte Regexps sind etwas schneller, also sollte man gleich am Anfang das kompilieren, besonders wenn man sie öfter verwendet. Ist aber nervig zu tippen, deswegen als zweiter, kleinerer Schritt, der die Liste nur in diesem Aspekt verändert bzw. hier neu erstellt. Die Verwendung der Datenstruktur lohnt sich schon allein aus tipp-ökonomischen Gründen. ;)
Jetzt kommt der spassige Teil, eine Funktion, die eine Zeile Text bekommt, der Reihe nach die Suchmuster drauf anwendet und beim ersten passenden Muster deren gefundene Submuster als Dictionary zurück gibt:
~~~python
def first_match(line):
for label, regex in patterns:
match = regex.search(line)
if match:
return (label, match.groupdict())
# Und wenn wir nix finden?
else:
raise SomeErrorThingy
Die Fehlerbehandlung sollte natürlich ausgebaut werden. ;) Ich geb hier wieder ein Tupel mit dem Label zurück, man weiß ja nie, ob man's nicht noch brauchen kann.
Der Rest ist klassisch. Man kann es so verwenden ...
~~~python
for line in open("file.txt"):
label, data = first_match(line)
doSomething()
... oder so für eine schöne Liste der herausgeholten Daten ...
`data = map(first_match, open("file.txt"))`{:.language-python}
... oder etwas unnötigerweise ausführlicher so ...
~~~python
def parse(filepath):
with open(filepath) as f: # Python 2.5
for label, data in itertools.imap(first_match, f):
yield (label, data)
raise StopIteration
for label, data in parse("file.txt"):
yadayadayada(data)
(Für ein anderes, fast gleiches aber schöneres Beispiel siehe Chapter 17: Dynamic functions aus Mark Pilgrims sehr empfehlenswerten freiem Buch „Dive into Python“.)
Tim
Yerf!
Eleganz bedeutet manchmal für unterschiedliche Leute unterschiedliche Dinge. Manchmal ist es möglich nah dran am konkreten Ablauf der Dinge dran zu sein wie in Deiner Schleife, manchmal bedeutet es, das eigene Denken im Programmcode wiedergespiegelt zu sehen.
Manchmal ist es auch Betriebsblindheit, dass man nicht merkt, dass man ausgehend von einem einfachen Tool, in das man immer mehr Funktionalität reinsteckt, irgendwann an den Punkt gelangt, wo ein anderer Ansatz als der ursprünglich gewählte wesentlich besser wäre. (Ursprünglich war das Tool nur ein ganz einfacher Syntaxcheck für dieses Fileformat)
• Du hast offenkundig einen Iterator, der Zeilen ausspuckt.
• Für jede dieser Zeilen möchtest Du eine _Liste_ von Mustern ausprobieren.
• Vom ersten Muster aus dieser Liste, das passt, möchtest Du bestimmte _benannte Submuster_ zurück geben.
Was noch fehlt ist eine gewisse Kontextabhängigkeit, sprich ausgehend von früher gefundenen Mustern unterschiedlichn zu reagieren, aber dass lässt sich noch einbauen. (Die gestellte Aufgabe lässt sich grob mit dem Parsen eines JSON-Files vergleichen, nur mit etwas anderer Syntax)
Um das ganze mal umgekehrt aufzubauen: Das erste Stichwort ist „benanntes Submuster“. Man kann ja in Regexps mit (?P<name>...) Gruppen benennen. Das kann man toll nutzen, in Deinem zweiten Regexp z.B. so:
re.compile(r'^\s*Text [(?P<text>\w*)] (?P<content>\w)*\s*{\w*$')
Das Schöne daran ist, dass man aus dem Match-Object mit der Methode m.groupdict() ein Dictionary mit den gefundenen Sub-Mustern herausbekommt.
Ja, das macht das ganze auch später noch gut Verständlich gegenüber den nummerierten Submustern.
Beachte auch das r vor dem Regexp-String. Das zeigt an, dass der String ein sogenannter Rawstring ist, was heisst, dass in diesem String Maskierungen mit dem Backslash nicht interpretiert werden. Man erspart sich dadurch die Verdoppelung der Backslashs; Rawstrings sind also sehr für Regexps beliebt.
Guter Tipp, danke. Soweit bin ich noch nicht in die Documentation von Python vorgedrungen (ich bastel derzeit nur ab und zu ein Hilfsskript damit)
Ich hab hier eine Liste von Zweier-Tupeln, das erste Element ist ein kurzes Label, das zweite das Suchmuster. Das Label ist eigentlich unnötig; ich pack's trotzdem mal rein zur Selbstdokumentation und zum anderen, falls man das Label später noch mal braucht, z.B. als der erkannte Typ der zum Suchmuster passenden Zeile.
Das Label werd ich brauchen um den Kontext (z.B. ob es die Zeile mit der ID oder eine mit zugehörigen Name-Value-Paar) zu ermitteln, von daher ganz gut, dass es vorhanden ist.
Ich verwandel diese Datenstruktur gleich mal daraufhin mit einer list comprehension etwas Praktischeres:
patterns = [(label, re.compile(pattern)) for label, pattern in patterns]
Warum? Kompilierte Regexps sind etwas schneller, also sollte man gleich am Anfang das kompilieren, besonders wenn man sie öfter verwendet. Ist aber nervig zu tippen, deswegen als zweiter, kleinerer Schritt, der die Liste nur in diesem Aspekt verändert bzw. hier neu erstellt. Die Verwendung der Datenstruktur lohnt sich schon allein aus tipp-ökonomischen Gründen. ;)
Performance ist für das Script zwar absolut unwichtig, aber auf die Weise ists natürlich auch recht schnell eingebaut.
Jetzt kommt der spassige Teil, eine Funktion, die eine Zeile Text bekommt, der Reihe nach die Suchmuster drauf anwendet und beim ersten passenden Muster deren gefundene Submuster als Dictionary zurück gibt:
Und das ist jetzt genau der Punkt mit dem wesentlich besseren Ansatz ;-)
Das mir das nicht gleich komisch vorkam mit der ellenlangen Liste an IFs die ich da hatte...
Die Fehlerbehandlung sollte natürlich ausgebaut werden. ;) Ich geb hier wieder ein Tupel mit dem Label zurück, man weiß ja nie, ob man's nicht noch brauchen kann.
Fehlerbehandlung kann recht rudimentär ausfallen, ist nur intern für mich um nicht händisch per Copy&Paste arbeiten zu müssen ;-)
(sprich: das ganze wird eine Art Converter zwischen 2 Formaten, aber wer weis... vielleicht brauch ichs ja für noch mehr)
Der Rest ist klassisch. Man kann es so verwenden ...
~~~python
for line in open("file.txt"):
label, data = first_match(line)
doSomething()
Hier kommt dann für mich noch der Teil mit dem beachten des Kontext und dem Aufbau des verschachtelten Dictionarys das ich am Ende haben will. Sollte aber auch kein großes Problem mehr sein.
> (Für ein anderes, fast gleiches aber schöneres Beispiel siehe [Chapter 17: Dynamic functions](http://diveintopython.org/dynamic_functions/index.html) aus Mark Pilgrims sehr empfehlenswerten freiem Buch „Dive into Python“.)
Werd ich mir mal anschauen.
Danke für die Ausführliche Antwort und die guten Ideen.
Gruß,
Harlequin
PS: grad beim Korrekturlesen von meinem Posting ist mir noch eine Idee gekommen, wie ich meinen ursprünglichen Ansatz hätte lösen können: Da jeweils immer nur ein Suchmuster zutrifft kann ich statt elif-Ketten auch einfach jeden IF-Fall mit einem continue für die äußere Schleife beenden...
Ich werd aber denk ich trotzdem deinen Ansatz aufgreifen, nur für den Fall, dass ich das Tool mit noch mehr Funktionalität ausstatten muss.
--
<!--[if IE]>This page is best viewed with a webbrowser. [Get one today!](http://www.opera.com)<![endif]-->