31

I'm trying to compare a string called facility to multiple possible strings to test if it is valid. The valid strings are:

auth, authpriv, daemon, cron, ftp, lpr, kern, mail, news, syslog, user, uucp, local0, ... , local7

Is there an efficient way of doing this other than:

if facility == "auth" or facility == "authpriv" ...
3
  • For looking for a substring, try stackoverflow.com/questions/3389574/… Commented Jan 8, 2020 at 10:35
  • Many newcomers are tripped by the fact that if facility == "auth" or "authpriv" doesn't do what they want (it checks if facility == "auth" is true or if "authpriv" is not an empty string). Commented Feb 4, 2020 at 12:32
  • 1
    For dupe searchers, if you find this and someone is doing a if variable == literal or literal or literal: sort of test, Why does "a == x or y or z" always evaluate to True? is a better dupe target. Commented Mar 15, 2022 at 23:10

3 Answers 3

64

If, OTOH, your list of strings is indeed hideously long, use a set:

accepted_strings = {'auth', 'authpriv', 'daemon'}

if facility in accepted_strings:
    do_stuff()

Testing for containment in a set is O(1) on average.

Sign up to request clarification or add additional context in comments.

4 Comments

Yes, that would be the way to go. wiki.python.org/moin/PythonSpeed is pretty good reading for anyone interested in a general overview of efficiency in python. Though you wouldn't happen to know the average time for set() would you?
One potential downside with this is that the order of iteration over them becomes unpredictable, but that's only a problem if you're using them for anything else (such as to print the list of accepted strings in a help message).
In python2.7/3 you can write accepted_strings = {'auth', 'authpriv', 'daemon'} so that no list is created prior to building a set.
In more modern Python, you'll benefit a bit by inlining the test to if facility in {'auth', 'authpriv', 'daemon'}: (as long as all options are constant literals as in this case); it'll be converted to a frozenset when the function doing it is compiled and stored in the function's constants, where this rebuilds the set each time you reach the accepted_strings = {'auth', 'authpriv', 'daemon'} line.
12

Unless your list of strings gets hideously long, something like this is probably best:

accepted_strings = ['auth', 'authpriv', 'daemon'] # etc etc 

if facility in accepted_strings:
    do_stuff()

4 Comments

oh awesome thank you. what happens if my list does actually get really long?
That was just a small joke, since you wouldn't want to type in a list of 10,000 strings by hand.
This is the option I originally used, but as my application might grow I'm going to accept @pillmucher's answer. Thanks +1
No probs. His is probably the safer one, though as a note you're going to have to have lists orders of magnitude bigger than what you have now before the difference between set and list containment will really start to be noticeable. Just remember premature optimization is the root of all evil. ;)
3

To efficiently check if a string matches one of many, use this:

allowed = set(('a', 'b', 'c'))
if foo in allowed:
    bar()

set()s are hashed, unordered collections of items optimized for determining whether a given item is in them.

3 Comments

If you're worried about speed it's slightly faster to assemble a tuple than a list to iterate over to create the set.
I have no idea why. My specialty is CPython byte code manipulation.
FWIW, in Python 2.7 and 3, you can use the set literal syntax: {'a', 'b', 'c'}