Categories
boolean-logic comparison if-statement match python

How to test multiple variables for equality against a single value?

799

I’m trying to make a function that will compare multiple variables to an integer and output a string of three letters. I was wondering if there was a way to translate this into Python. So say:

x = 0
y = 1
z = 3
mylist = []

if x or y or z == 0:
    mylist.append("c")
if x or y or z == 1:
    mylist.append("d")
if x or y or z == 2:
    mylist.append("e")
if x or y or z == 3: 
    mylist.append("f")

which would return a list of:

["c", "d", "f"]

8

  • 8

    use 1 in (tuple)

    – user9011445

    Dec 5, 2017 at 21:49

  • 7

    When you want to evaluate a list of statements in a any/all manner you can use any/all functions. For example: all([1, 2, 3, 4, False]) will return False all([True, 1, 2, 3]) will return True any([False, 0, 0, False]) will return False any([False, 0, True, False]) will return True

    – eddd

    Jun 4, 2018 at 16:17


  • 10

    This question is a very popular duplicate target, but I think it’s suboptimal for that purpose. Most people try to do something like if x == 0 or 1:, which is of course similar to if x or y == 0:, but might be a little confusing for newbies nonetheless. Given the sheer volume of “Why isn’t my x == 0 or 1 working?” questions, I would much rather use this question as our canonical duplicate target for these questions.

    – Aran-Fey

    Apr 10, 2019 at 10:06

  • 2

    Take extra care when comparing to “falsey” values like 0, 0.0 or False. You can easily write wrong code which gives the “right” answer.

    – smci

    Apr 10, 2019 at 10:09

  • 4

    For the opposite, see Comparing a string to multiple items in Python

    – tripleee

    Jan 8, 2020 at 10:27

1064

+500

You misunderstand how boolean expressions work; they don’t work like an English sentence and guess that you are talking about the same comparison for all names here. You are looking for:

if x == 1 or y == 1 or z == 1:

x and y are otherwise evaluated on their own (False if 0, True otherwise).

You can shorten that using a containment test against a tuple:

if 1 in (x, y, z):

or better still:

if 1 in {x, y, z}:

using a set to take advantage of the constant-cost membership test (i.e. in takes a fixed amount of time whatever the left-hand operand is).

Explanation

When you use or, python sees each side of the operator as separate expressions. The expression x or y == 1 is treated as first a boolean test for x, then if that is False, the expression y == 1 is tested.

This is due to operator precedence. The or operator has a lower precedence than the == test, so the latter is evaluated first.

However, even if this were not the case, and the expression x or y or z == 1 was actually interpreted as (x or y or z) == 1 instead, this would still not do what you expect it to do.

x or y or z would evaluate to the first argument that is ‘truthy’, e.g. not False, numeric 0 or empty (see boolean expressions for details on what Python considers false in a boolean context).

So for the values x = 2; y = 1; z = 0, x or y or z would resolve to 2, because that is the first true-like value in the arguments. Then 2 == 1 would be False, even though y == 1 would be True.

The same would apply to the inverse; testing multiple values against a single variable; x == 1 or 2 or 3 would fail for the same reasons. Use x == 1 or x == 2 or x == 3 or x in {1, 2, 3}.

19

  • 150

    I wouldn’t be so quick to go for the set version. Tuple’s are very cheap to create and iterate over. On my machine at least, tuples are faster than sets so long as the size of the tuple is around 4-8 elements. If you have to scan more than that, use a set, but if you are looking for an item out of 2-4 possibilities, a tuple is still faster! If you can arrange for the most likely case to be first in the tuple, the win is even bigger: (my test: timeit.timeit('0 in {seq}'.format(seq=tuple(range(9, -1, -1)))))

    Oct 24, 2013 at 15:27


  • 70

    @dequestarmappartialsetattr: In Python 3.3 and up, the set is stored as a constant, bypassing the creation time altogether, eliminating the creation time. Tuples can be cheap to create as Python caches a bundle of them to avoid memory churn, making that the biggest difference with sets here.

    – Martijn Pieters

    Oct 24, 2013 at 15:29


  • 18

    @dequestarmappartialsetattr: If you time just the membership test, for integers sets and tuples are equally fast for the ideal scenario; matching the first element. After that tuples lose out to sets.

    – Martijn Pieters

    Oct 24, 2013 at 15:37

  • 25

    @MartijnPieters: Using the set literal notation for this test isn’t a savings unless the contents of the set literal are also literals, right? if 1 in {x, y, z}: can’t cache the set, because x, y and z could change, so either solution needs to build a tuple or set from scratch, and I suspect whatever lookup savings you might get when checking for membership would be swamped by greater set creation time.

    Sep 4, 2016 at 0:37

  • 14

    @ShadowRanger: yes, peephole optimisation (be it for in [...] or in {...}) only works if the contents of the list or set are immutable literals too.

    – Martijn Pieters

    Sep 4, 2016 at 7:58

116

Your problem is more easily addressed with a dictionary structure like:

x = 0
y = 1
z = 3
d = {0: 'c', 1:'d', 2:'e', 3:'f'}
mylist = [d[k] for k in [x, y, z]]

3

  • 27

    Or even d = "cdef" which leads to MyList = ["cdef"[k] for k in [x, y, z]]

    – aragaer

    Oct 24, 2013 at 15:39

  • 11

    or map(lambda i: 'cdef'[i], [x, y, z])

    – dansalmo

    May 8, 2014 at 14:36

  • 2

    Aside from the list comprehension which I’m not yet fully accustomed to, most of us had the same reflex: build that dict !

    Mar 10, 2019 at 18:57

76

As stated by Martijn Pieters, the correct, and fastest, format is:

if 1 in {x, y, z}:

Using his advice you would now have separate if-statements so that Python will read each statement whether the former were True or False. Such as:

if 0 in {x, y, z}:
    mylist.append("c")
if 1 in {x, y, z}:
    mylist.append("d")
if 2 in {x, y, z}:
    mylist.append("e")
...

This will work, but if you are comfortable using dictionaries (see what I did there), you can clean this up by making an initial dictionary mapping the numbers to the letters you want, then just using a for-loop:

num_to_letters = {0: "c", 1: "d", 2: "e", 3: "f"}
for number in num_to_letters:
    if number in {x, y, z}:
        mylist.append(num_to_letters[number])

2

  • 1

    @VisioN You mean for number in num_to_letters? You don’t need .keys(), dicts iterate over keys by default. Regarding using a string, you mean something like this, right? for i, c in enumerate('cdef'): if i in {x, y, z}: mylist.append(c) Agreed, that would be simpler. Or better yet, s = 'cdef'; mylist = [s[i] for i in [x, y, z]]

    – wjandrea

    Nov 28, 2020 at 22:03


  • @wjandrea Yes, you are right, it’s my mistake! I completely forgot about the default behaviour. Unfortunately, I cannot edit my comment, so I have deleted it since you have highlighted the better approach in your comment.

    – VisioN

    Nov 29, 2020 at 11:09