Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

Unlike Perl, you can't to my knowledge match a regular expression inside an if statement in Python and assign the result to a varaible at the same moment. This leads to typical constructs like this:

match = re.search(REGEX, STRING)
if match:
    # do something

So far, so Python. But what if I want to iterate through a file / array of lines, check each line for a few regexes, and fire a catch-all when none has matched? I can't think my way around a rather unwieldy and deeply nested if-else-if-else...-construction:

import re
strings = ["abc zzz", "y", "#comment"]
for s in strings:
    match = re.search("(\S+) (\S+)", s)
    if match:
        print "Multiword: %s+%s" % (match.group(1), match.group(2))
    else:
        match = re.match("y$", s)
        if match:
            print "Positive"
        else:
            match = re.match("n$", s)
            if match:
                print "Negative"
            else:
                # a few more matches possible in real life script,
                # and then the last catch-all:
                print "No match found, line skipped"

Isn't there any way to put this in a much nicer looking elif-construction or something? The following doesn't work in Python, because if-clauses take only expressions, not statements. However, something along these lines would strike me as pythonic, or am I blind to something obvious here?

if match = re.search(" ", s):
    print "Multiword: %s+%s" % (match.group(1), match.group(2))
elif match = re.match("y$", s):
    print "Positive"
else:
    print "No match found, line skipped"
share|improve this question

migrated from stackoverflow.com Mar 24 at 14:52

This question came from our site for professional and enthusiast programmers.

9 Answers 9

up vote 19 down vote accepted

Just use the continue keyword to force the advancement to evaluate the next string. Code after the if statement will only execute if the previous statement was false.

import re
strings = ["abc zzz", "y", "#comment"]
for s in strings:
    match = re.search("(\S+) (\S+)", s)
    if match:
        print "Multiword: %s+%s" % (match.group(1), match.group(2))
        continue
    match = re.match("y$", s)
    if match:
        print "Positive"
        continue
    match = re.match("n$", s)
    if match:
        print "Negative"
        continue

    # a few more matches possible in real life script,
    # and then the last catch-all:
    print "No match found, line skipped"
share|improve this answer
    
On the one hand, this doesn't look perfectly "clean" to me, on the other it's easy to understand and I'd say it'd keep easy to maintain. I'll have to check this a bit more in-depth tomorrow, but thanks for the reply! –  Thor Mar 24 at 15:11
    
@Thor the only problem with this structure, which I somehow like, is that it's so easy to forget to add a continue in a new clause you add later –  Lohoris Mar 24 at 15:48

Why not using a list of tuple (re, lambda match: action), that is something like

actions = [("(\S+) (\S+)", lambda match: "Multiword: %s+%s" % (match.group(1), match.group(2))),
           ("y$", lambda match: "Positive"),
           ("n$", lambda match: "Negative")]

and then:

for rex, action in actions:
     match = re.match(rex, s)
     if match: 
          print action(match)

If you need to mix search and match then you can use a list of tuple:

(matchmethod, rex, action)

as in

actions = [
    (re.search, "(\S+) (\S+)", lambda match: "Multiword: %s+%s"%(match.group(1), match.group(2)) ),
    (re.match, "y$", lambda match: "Positive"),
    (re.match, "n$", lambda match: "Negative")]

And of course:

for matchtype, rex, action in actions:
     match = matchtype(rex, s)
     if match: 
          print action(match)
share|improve this answer
1  
Misses the fact that one of the calls was to re.search, another to re.match. –  RemcoGerlich Mar 24 at 15:04
    
@RemcoGerlich : The solution can be easily adapted. See my edit. –  hivert Mar 24 at 15:08
    
I know, and it's a valid method. I think it'll depend on the number of tests and how much their code has in common, for deciding if this is a good idea. In general I think it makes the code less readable, but it has the potential to make it a lot shorter, and that's good. –  RemcoGerlich Mar 24 at 15:10
1  
This is much more complex in understanding then I'm used to, but it seems pretty pythonic. I'll experiment with this, thank you! –  Thor Mar 24 at 15:15
    
@thor: this is just pushing further Python's switch statement –  hivert Mar 24 at 15:20

I'd put it in a function and return from it when a match was found, that way you don't have all the indents for the else: cases, just a list of tests and returns for them:

import re
strings = ["abc zzz", "y", "#comment"]

def run_tests(s)
    match = re.search("(\S+) (\S+)", s)
    if match:
        print "Multiword: %s+%s" % (match.group(1), match.group(2))
        return

    if re.match("y$", s):
        print "Positive"
        return

    if re.match("n$", s):
        print "Negative"
        return

    # a few more matches possible in real life script,
    # and then the last catch-all:
    print "No match found, line skipped"

for s in strings:
    run_tests(s)

I'd try to put the list of tests into some data structure to loop over (like the messages and the patterns to test for), but because the code is slightly different (search vs match, simple print vs doing something with match) it's clearer just to keep it like this.

share|improve this answer
    
This works fine until I want to fill different variables under the different matches - then I'd always have to pass the full list of possible variables back and forth between this function and the caller, right? –  Thor Mar 24 at 15:14
2  
You can always make your code arbitrarily complex, yes :-). In such cases maybe all of your variables should be part of some sort of State class, and a state variable could be passed to a set of test_ functions, or they could be methods of that class, or et cetera. –  RemcoGerlich Mar 24 at 17:30

I like @hivert's approach, but would formalize it a bit more:

import re

tests = [
    ("(\S+) (\S+)", "Multiword: {0}+{1}"),
    ("^y$",         "Positive"),
    ("^n$",         "Negative")
]

def get_first_match(s, tests=tests, none_match="No match found, line skipped"):
    for reg,fmt in tests:
        match = re.search(reg, s)
        if match:
            return fmt.format(*match.groups())
    return none_match

then

strings = ["abc zzz", "y", "#comment"]
for s in strings:
    print(get_first_match(s))
share|improve this answer
    
The first one was re.search, the others are re.match –  Izkata Mar 25 at 19:52
    
@Izkata: that is why I prepended a "^" to them - it forces .search to behave like .match (only match at start of line). –  Hugh Bothwell Mar 25 at 20:01

This is a known issue with regexps. But you can just put the options in a container, such as a list, and then use a for loop:

import re
strings = ["abc zzz", "y", "#comment"]
regexps_and_messages = [
    ("(\S+) (\S+)", "Multiword: %s+%s"),
    ("y$", "Positive"),
    ("n$", "Negative"),
]

for s in strings:
    for regexp, message in regexps_and_messages:
        m = re.match(regexp, s)
        if m is not None:
            print message % m.groups()
            break
    else: # if no break in above loop
        print "No match found, line skipped"
share|improve this answer
    
Not that I particularly need the re.match vs re.search distinction, but this makes it impossible to retain it. Also, the for ... else construction is, to my experience, so rarely used that most maintainers wouldn't know right away what this does - or is that just my own ignorance speaking? –  Thor Mar 24 at 15:13
    
@Thor not really, if you do need the behavior of match just prepend ^ to the applicable regular expressions, e.g. "^y$" and "^n$". The behavior will differ slightly from that of match when using the multiline regex flag (it will also match after every newline instead of only at the start of the string being searched), but as you're checking line-by-line anyway that shouldn't make a difference (and in those cases $ will match before any newlines as well so you'd have to worry about that one too, unless such behavior is desirable). –  JAB Mar 24 at 17:07

To go even further on the approaches suggested that put the regexes in a list you could join the regexes together with | and then match the line against all possible patterns in one go.

import re

class LineMatcher(object):
    def __init__(self, patterns):
        # In order to match each string, we build a regex combining which can match
        # all the parts. 
        # Something like: ((\S+) (\S+))|(y$)|(n$)|(.*))
        # When we match against it, we can figure out which pattern was matched
        self._groups = {}
        regexes = []

        # because groups could contain groups, we need to keep track of the current
        # group index so that we know which index each pattern will end up with.
        current_group_index = 1
        for pattern, handler in patterns:
            group_count = re.compile(pattern).groups
            self._groups[current_group_index] = (group_count, handler)
            regexes.append("(%s)" % pattern)
            current_group_index += group_count + 1

        self._regex = re.compile("|".join(regexes))

    def match(self, string):
        match = self._regex.match(string)
        group_count, handler = self._groups[match.lastindex]
        captures = match.groups()[match.lastindex:match.lastindex + group_count]
        return handler(*captures)


matcher = LineMatcher([
    ("(\S+) (\S+)", lambda first, second: "Multiword: %s+%s"),
    ("y$", lambda: "Positive"),
    ("n$", lambda: "Negative"),
    (".*", lambda: "No match found, line skipped")
])


strings = ["abc zzz", "y", "#comment"]
for s in strings:
    print matcher.match(s)
share|improve this answer
    
I like this solution because it avoids having to re-execute the regular expression searches for each possibility; you will still have a similar number of conditionals, essentially, but short-circuited checks –  JAB Mar 24 at 17:14
    
I like this solution, it seems very pythonic, extensible, and "cleaner" than the continue-approach of @Calpratt. On the other hand, I can grasp that approach within the first glance of the code, whereas here I have to work my way through it. I guess my question now is: If you stumbled across something like this in code you have to maintain, which would make life easier for you? With this in mind, I'll choose the cruder but easier continue-approach. –  Thor Mar 25 at 10:54
    
It should be noted that this approach only works because the regexes are all mutually exclusive. If you needed it to be multi-word and end in a 'y', for example, this can't be used. –  Gabe Mar 25 at 14:21
    
@Gabe, actually in my code above its not mutually exclusive. Not that .* matches anything. I think that in the case of ambiguity it always returns the first match. –  Winston Ewert Mar 25 at 20:37

Calpratt's answer without continue:

import re
strings = ["abc zzz", "y", "#comment"]
for s in strings:
    match = re.search("(\S+) (\S+)", s)
    if match:
        print "Multiword: %s+%s" % (match.group(1), match.group(2))
    elif re.match("y$", s):
        print "Positive"
    elif re.match("n$", s):
        print "Negative"
    else:
        print "No match found, line skipped"
share|improve this answer
1  
Yes, but this works only for exactly this situation. From the first elif onwards you have no chance to access the match result. There's even the danger of trying to access the match object in the elif branches, which would lead to working on wrong results. No downvote as this is not wrong per se, but it's not as flexible as the other approaches. –  Thor Mar 25 at 10:49

Once you recognized the pattern in your code, you can wrap it as a base class to avoid boilerplates. This also makes the code more maintainable.

import re

class Handler:
    PATTERN = ''
    def __init__(self):
        self._pattern = re.compile(self.PATTERN)

    def match(self, *args, **kwargs):
        return self._pattern.match(*args, **kwargs)

    def handle(self, matched):
        pass

class MultiwordHandler(Handler):
    PATTERN = '(\S+) (\S+)'

    def match(self, *args, **kwargs):
        return self._pattern.search(*args, **kwargs)

    def handle(self, matched):
        print 'Multiword: %s+%s' % (matched.group(1), matched.group(2))

class PositiveHandler(Handler):
    PATTERN = 'y$'

    def handle(self, matched):
        print 'Positive'

class NegativeHandler(Handler):
    PATTERN = 'n$'

    def handle(self, matched):
        print 'Negative'

And use them like this:

handlers = [MultiwordHandler(), PositiveHandler(), NegativeHandler()]

strings = ["abc zzz", "y", "#comment"]

for s in strings:
    for handler in handlers:
        matched = handler.match(s)
        if matched:
            handler.handle(matched)
            break
    else:
        print "No match found, line skipped"
share|improve this answer

You can make flow of your application more linear by either return-ing from the method when failing or do it Python way using Exceptions, e.g.:

try:
    if not do_action_1:
        raise Exception('Action 1 failed')
    if not do_action_2:
        raise Exception('Action 2 failed')
    if not do_action_3:
        raise Exception('Action 3 failed')
    if not do_action_4:
        raise Exception('Action 4 failed')
    if not do_action_5:
        raise Exception('Action 5 failed')
except Exception as e:
    print 'Whoops: %s' % e
share|improve this answer
6  
I'd rather call this an abuse of exceptions. –  RemcoGerlich Mar 24 at 14:58
    
Apart from feeling that I have to agree with @RemcoGerlich on this, I also have to admit I don't quite see how to apply this to my situation. Are the do_action_x-parts all supposed to be individual functions, one for each match I'm trying to check? –  Thor Mar 24 at 15:01

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.