What is a pythonic way of checking for words in a string and setting a variable accordingly?

Question:

I might be overthinking this, but I’m currently facing this code:

result = None
if 'word_a' in my_string.lower():
    result = do_action_A()
elif 'word_b' in my_string.lower():
    result = do_action_B()
elif 'word_c' in my_string.lower():
    result = do_action_C()
etc.

I’m trying to think of a nicer / more pythonic /readable way of doing this, but my brain seems empty for solutions. It is somehow related to this question I asked years ago, but it’s not exactly the same, since it’s not simply a string comparison which can be substituted with a dictionary lookup.

Is there a more Pythonic way of doing this?

To be clear, I’m not specifically looking for more performance, but rather for elegant/readable code.

Asked By: kramer65

||

Answers:

Use a dictionary (could be a list of 2-tuples too for this purpose), a for loop since you can’t just directly access it with the substring match, and for:else: to figure out if you didn’t get a match.

actions = {
    "word_a": do_action_A,
    "word_b": do_action_B,
    "word_c": do_action_C,
}

result = None
my_string_lower = my_string.lower()
for word, action in actions.items():
    if word in my_string_lower:
        result = action()
        break
else:
    print("oh no, no match")
Answered By: AKX

something like the below

from typing import NamedTuple,Callable,List

class Pair(NamedTuple):
    word: str
    func: Callable

def do_action_A():
    pass


def do_action_B():
    pass


def do_action_C():
    pass

result = None
my_string = 'Somthing'
my_string = my_string.lower()
pairs: List[Pair] = [Pair('word_a', do_action_A), Pair('word_b', do_action_B), Pair('word_c', do_action_C)]
for pair in pairs:
    if pair.word in my_string:
        result = pair.func()
        break
Answered By: balderman

With regex this could be really much shorter:

import re
actions = {
    "word_a": do_action_A,
    "word_b": do_action_B,
    "word_c": do_action_C,
}
result = actions.get(next(iter(re.findall(f'(?:{"|".join(actions)})', s)), None))()
Answered By: U12-Forward

Here’s a one-liner to really get your readers confused 🙂

actions = {
    'word_a': do_action_A,
    'word_b': do_action_B,
    'word_c': do_action_C,
}
my_string = 'has_word_a_yea'
result = next(func for action,func in actions.items() if action in my_string.lower())()

Will raise StopIteration if no match.

Answered By: bavaza

Customizable code for easy changes and future demands.
2 Different attitudes.

You can subclass it, change regex parser, act functions and etc..

import re    

class Executor:
    def __init__(self, action_dict):
        self.action_dict = action_dict
        self.keys = list(action_dict.keys())

    def regex_parser(self, regex):
        return regex

    def string_parser(self, string):
        return string.lower()

    def match(self, string):
        for key in self.keys:
            regex = re.compile(self.regex_parser(key))
            if regex.search(self.string_parser(string)) is not None:
                return self.act(key, string)

        return None

    def act(self, key, string):
        func = self.action_dict[key]
        return func(string)


executor = Executor(
    {
        "test": print,
        "some": lambda s: print(s + " Hello matcher"),
        "end$": lambda s: print("End"),
    }
)

executor.match("testing")
executor.match("something")
executor.match("LegenD")

>>> testing
>>> something Hello matcher
>>> End

Second attitude is more verbose, but benefit is that each Matcher class can have its own set of rules and evaluation.

import re

class DefaultMatcher:
    regex = "default"

    def regex_parser(self, regex):
        return regex

    def string_parser(self, string):
        return string.lower()

    def function(self, string, *args, **kwargs):
        """This function is the action."""
        return args, kwargs

    def match(self, string, *args, **kwargs):
        # Returns something or None
        return self.compiled_regex.search(self.string_parser(string))

    def __init__(self, *args, **kwargs):
        self.compiled_regex = re.compile(self.regex_parser(self.regex))

    def __call__(self, string, *args, **kwargs):
        parsed_string = self.string_parser(string)
        if self.match(string):
            return self.function(string, *args, **kwargs)
        else:
            return None


class Matcher1(DefaultMatcher):
    regex = "test1"

    def function(self, *args, **kwargs):
        return "Matcher_1"


class Matcher2(DefaultMatcher):
    regex = "test2"

    def function(self, *args, **kwargs):
        return "Matcher_2"


class Matcher3(DefaultMatcher):
    regex = "o"

    def regex_parser(self, regex):
        super_regex = super().regex_parser(regex)
        return super_regex + "$"

    def function(self, *args, **kwargs):
        return "Matcher_3"


class DefaultExecutor:
    def __init__(self, list_of_matcher, *args, **kwargs):
        self.list_of_matcher = [matcher(*args, **kwargs) for matcher in list_of_matcher]

    def __call__(self, string, *args, **kwargs):
        for matcher in self.list_of_matcher:
            result = matcher(string, *args, **kwargs)
            if result is not None:
                return result


executor = DefaultExecutor([Matcher1, Matcher2, Matcher3])


print(executor("test1"))
print(executor("Otto"))
print(executor("test2"))
Answered By: Martin