Equality check difference in Python
Question:
Suppose we want some block of code to be executed when both ‘a’ and ‘b’ are equal to say 5. Then we can write like:
if a == 5 and b == 5:
# Do something
But a few days ago, I just involuntarily wrote a similar condition check as:
if a == b and b == 5:
# Do something
which made me think, is there any difference between the two?
Also, there is one other way,
if a == b == 5:
# Do something
Is there a difference, a difference in terms of process of evaluation or execution or time taken? And also which one is the better or which is better to use?
Is it related to the concept of transitivity?
Answers:
Since they are basically equivalent, you could also consider the way you read/think about the code:
if a == 5 and b == 5:
# do something
can be read as “if a
equals 5
and b
equals 5
, then do …”. You have to think/conclude, that then also a
will be equal to b
.
This is opposite to the next example:
if a == b and b == 5:
# do something
This reads as “if a
is equal to b
and b
equal to 5
” and you have to conclude that then also a
will be equal to 5
This is why I prefer the last example:
if a == b == 5:
# do something
If you are familiar with Python (thanks to Itzkata) it is immediately clear that all three things must be equal (to 5
). If however people with less experience in Python (but programming skills in other languages) see this, they might evaluate this to
if (a == b) == 5:
which would compare the boolean result of the first comparison with the integer 5, which is not what Python does and might lead to different results (consider for example with a=0, b=0
: a==b==0
is true while (a==b) == 0
is not!
The manual says:
There are eight comparison operations in Python. They all have the
same priority (which is higher than that of the Boolean operations).
Comparisons can be chained arbitrarily; for example, x < y <= z is
equivalent to x < y and y <= z, except that y is evaluated only once
(but in both cases z is not evaluated at all when x < y is found to be
false).
There might even be a difference, for example if evaulating b
in your example would have a side effect.
Regarding transitivity, you are right.
It depends. You could write your own custom __eq__
which allows you to compare yourself to ints and things:
class NonNegativeInt(object):
def __init__(self, value):
if value < 0:
raise Exception("Hey, what the...")
self.value = value
def __eq__(self, that):
if isinstance(that, int):
return self.value == that
elif isinstance(that, NonNegativeInt):
return self.value == that.value
else:
raise ArgumentError("Not an acceptible argument", "__eq__", that)
which would work different depending on comparing “b” to “a” and “b” to an “int.” Hence, a == b
could be false while a == 5 and b == 5
could be True.
As far as integers are concerned, there isn’t any difference, in terms of sheer performance, between the first two comparisons.
The third comparison is different, though; since a little more fiddling with the stack gets involved. Indeed, the code
import dis
def comparison_1(a, b):
if a == 5 and b == 5:
pass
def comparison_2(a, b):
if a == b and b == 5:
pass
def comparison_3(a, b):
if a == b == 5:
pass
print("*** First comparison ***")
dis.dis(comparison_1)
print("n*** Second comparison ***")
dis.dis(comparison_2)
print("n*** Third comparison ***")
dis.dis(comparison_3)
returns
*** First comparison ***
4 0 LOAD_FAST 0 (a)
3 LOAD_CONST 1 (5)
6 COMPARE_OP 2 (==)
9 POP_JUMP_IF_FALSE 27
12 LOAD_FAST 1 (b)
15 LOAD_CONST 1 (5)
18 COMPARE_OP 2 (==)
21 POP_JUMP_IF_FALSE 27
5 24 JUMP_FORWARD 0 (to 27)
>> 27 LOAD_CONST 0 (None)
30 RETURN_VALUE
*** Second comparison ***
8 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 COMPARE_OP 2 (==)
9 POP_JUMP_IF_FALSE 27
12 LOAD_FAST 1 (b)
15 LOAD_CONST 1 (5)
18 COMPARE_OP 2 (==)
21 POP_JUMP_IF_FALSE 27
9 24 JUMP_FORWARD 0 (to 27)
>> 27 LOAD_CONST 0 (None)
30 RETURN_VALUE
*** Third comparison ***
12 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 DUP_TOP
7 ROT_THREE
8 COMPARE_OP 2 (==)
11 JUMP_IF_FALSE_OR_POP 23
14 LOAD_CONST 1 (5)
17 COMPARE_OP 2 (==)
20 JUMP_FORWARD 2 (to 25)
>> 23 ROT_TWO
24 POP_TOP
>> 25 POP_JUMP_IF_FALSE 31
13 28 JUMP_FORWARD 0 (to 31)
>> 31 LOAD_CONST 0 (None)
34 RETURN_VALUE
If you have more variables to test, using all
might be slightly more readable:
if all(i==5 for i in [a,b,c,d]):
# do something
Suppose we want some block of code to be executed when both ‘a’ and ‘b’ are equal to say 5. Then we can write like:
if a == 5 and b == 5:
# Do something
But a few days ago, I just involuntarily wrote a similar condition check as:
if a == b and b == 5:
# Do something
which made me think, is there any difference between the two?
Also, there is one other way,
if a == b == 5:
# Do something
Is there a difference, a difference in terms of process of evaluation or execution or time taken? And also which one is the better or which is better to use?
Is it related to the concept of transitivity?
Since they are basically equivalent, you could also consider the way you read/think about the code:
if a == 5 and b == 5:
# do something
can be read as “if a
equals 5
and b
equals 5
, then do …”. You have to think/conclude, that then also a
will be equal to b
.
This is opposite to the next example:
if a == b and b == 5:
# do something
This reads as “if a
is equal to b
and b
equal to 5
” and you have to conclude that then also a
will be equal to 5
This is why I prefer the last example:
if a == b == 5:
# do something
If you are familiar with Python (thanks to Itzkata) it is immediately clear that all three things must be equal (to 5
). If however people with less experience in Python (but programming skills in other languages) see this, they might evaluate this to
if (a == b) == 5:
which would compare the boolean result of the first comparison with the integer 5, which is not what Python does and might lead to different results (consider for example with a=0, b=0
: a==b==0
is true while (a==b) == 0
is not!
The manual says:
There are eight comparison operations in Python. They all have the
same priority (which is higher than that of the Boolean operations).
Comparisons can be chained arbitrarily; for example, x < y <= z is
equivalent to x < y and y <= z, except that y is evaluated only once
(but in both cases z is not evaluated at all when x < y is found to be
false).
There might even be a difference, for example if evaulating b
in your example would have a side effect.
Regarding transitivity, you are right.
It depends. You could write your own custom __eq__
which allows you to compare yourself to ints and things:
class NonNegativeInt(object):
def __init__(self, value):
if value < 0:
raise Exception("Hey, what the...")
self.value = value
def __eq__(self, that):
if isinstance(that, int):
return self.value == that
elif isinstance(that, NonNegativeInt):
return self.value == that.value
else:
raise ArgumentError("Not an acceptible argument", "__eq__", that)
which would work different depending on comparing “b” to “a” and “b” to an “int.” Hence, a == b
could be false while a == 5 and b == 5
could be True.
As far as integers are concerned, there isn’t any difference, in terms of sheer performance, between the first two comparisons.
The third comparison is different, though; since a little more fiddling with the stack gets involved. Indeed, the code
import dis
def comparison_1(a, b):
if a == 5 and b == 5:
pass
def comparison_2(a, b):
if a == b and b == 5:
pass
def comparison_3(a, b):
if a == b == 5:
pass
print("*** First comparison ***")
dis.dis(comparison_1)
print("n*** Second comparison ***")
dis.dis(comparison_2)
print("n*** Third comparison ***")
dis.dis(comparison_3)
returns
*** First comparison ***
4 0 LOAD_FAST 0 (a)
3 LOAD_CONST 1 (5)
6 COMPARE_OP 2 (==)
9 POP_JUMP_IF_FALSE 27
12 LOAD_FAST 1 (b)
15 LOAD_CONST 1 (5)
18 COMPARE_OP 2 (==)
21 POP_JUMP_IF_FALSE 27
5 24 JUMP_FORWARD 0 (to 27)
>> 27 LOAD_CONST 0 (None)
30 RETURN_VALUE
*** Second comparison ***
8 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 COMPARE_OP 2 (==)
9 POP_JUMP_IF_FALSE 27
12 LOAD_FAST 1 (b)
15 LOAD_CONST 1 (5)
18 COMPARE_OP 2 (==)
21 POP_JUMP_IF_FALSE 27
9 24 JUMP_FORWARD 0 (to 27)
>> 27 LOAD_CONST 0 (None)
30 RETURN_VALUE
*** Third comparison ***
12 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 DUP_TOP
7 ROT_THREE
8 COMPARE_OP 2 (==)
11 JUMP_IF_FALSE_OR_POP 23
14 LOAD_CONST 1 (5)
17 COMPARE_OP 2 (==)
20 JUMP_FORWARD 2 (to 25)
>> 23 ROT_TWO
24 POP_TOP
>> 25 POP_JUMP_IF_FALSE 31
13 28 JUMP_FORWARD 0 (to 31)
>> 31 LOAD_CONST 0 (None)
34 RETURN_VALUE
If you have more variables to test, using all
might be slightly more readable:
if all(i==5 for i in [a,b,c,d]):
# do something