-1

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
kramer65
  • 50,427
  • 120
  • 308
  • 488

5 Answers5

1

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")
AKX
  • 152,115
  • 15
  • 115
  • 172
  • 1
    It would make sense to put a variable `my_string_lower = my_string.lower()` before the loop. Otherwise it gets computed every time single time in the loop – Einliterflasche Sep 16 '21 at 12:05
  • @Einliterflasche Fixed, good point. – AKX Sep 16 '21 at 12:06
  • I think this is by far the most simple, concise, readable and thus elegant solution. Well done and thank you. – kramer65 Sep 17 '21 at 07:50
  • @kramer65 This solution is perfectly OK. However, I expected from your question much more. This is pretty much the same like what 'you asked a year ago' with only one difference -> using 'in' instead of '=='. Please, be careful with words like 'elegant' and 'readable'(your code is perfectly readable), because dictionary iteration is really not something elegant, its basic python feature. Dont take it personally, I dont mean it that seriously :), I just wouldnt even bother answering knowing that you are asking us really basic stuff and not something 'extra'. – Martin Sep 17 '21 at 10:09
0

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
balderman
  • 22,927
  • 7
  • 34
  • 52
0

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))()
U13-Forward
  • 69,221
  • 14
  • 89
  • 114
0

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.

bavaza
  • 10,319
  • 10
  • 64
  • 103
  • Why would you want to purposely confuse readers (incl. you down the line?) :) – AKX Sep 17 '21 at 07:54
  • Just kidding. When you get used to it, list compression is actually very readable. IMO the code I wrote is perfectly intelligible, and pretty 'Pyhtonic' (whatever that means). – bavaza Sep 17 '21 at 16:27
0

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"))
Martin
  • 3,333
  • 2
  • 18
  • 39
  • Not to be unkind, but this code-smells like overengineering and YAGNI for a simple requirement... – AKX Sep 17 '21 at 16:37
  • @AKX you are quite right. But its elegant, readable and customizable as its OOP. The reason why its overengineered is because I didnt expect that 40k user would duplicate question and consider dictionary iteration an elegant solution. I truly believed OP wants something more(his 'brain was empty for solutions'). This question only exist due to lack of syntax knowledge. Nothing against questioner... Just wanted to contribute with something nice and powerful – Martin Sep 17 '21 at 20:33
  • @AKX btw, everybody who answered here (beside me) has > 16k points. Question is really basic. Isn't this little bit of underengineering? – Martin Sep 17 '21 at 20:47