Reputation: 24691
By "comparable", I mean "able to mutually perform the comparison operations >
, <
, >=
, <=
, ==
, and !=
without raising a TypeError
". There are a number of different classes for which this property does hold:
1 < 2.5 # int and float
2 < decimal.Decimal(4) # int and Decimal
"alice" < "bob" # str and str
(1, 2) < (3, 4) # tuple and tuple
and for which it doesn't:
1 < "2" # int and str
1.5 < "2.5" # float and str
even when it seems like it really ought to:
datetime.date(2018, 9, 25) < datetime.datetime(2019, 1, 31) # date and datetime
[1, 2] < (3, 4) # list and tuple
As demonstrated in this similar question, you can obviously check this for two unknown-typed objects a
and b
by using the traditional python approach of "ask forgiveness, not permission" and using a try
/except
block:
try:
a < b
# do something
except TypeError:
# do something else
but catching exceptions is expensive, and I expect the second branch to be taken sufficiently frequently for that to matter, so I'd like to catch this in an if
/else
statement instead. How would I do that?
Upvotes: 1
Views: 120
Reputation: 107005
Since it is impossible to know beforehand whether a comparison operation can be performed on two specific types of operands until you actually perform such an operation, the closest thing you can do to achieving the desired behavior of avoiding having to catch a TypeError
is to cache the known combinations of the operator and the types of the left and right operands that have already caused a TypeError
before. You can do this by creating a class with such a cache and wrapper methods that do such a validation before proceeding with the comparisons:
from operator import gt, lt, ge, le
def validate_operation(op):
def wrapper(cls, a, b):
# the signature can also be just (type(a), type(b)) if you don't care about op
signature = op, type(a), type(b)
if signature not in cls.incomparables:
try:
return op(a, b)
except TypeError:
cls.incomparables.add(signature)
else:
print('Exception avoided for {}'.format(signature)) # for debug only
return wrapper
class compare:
incomparables = set()
for op in gt, lt, ge, le:
setattr(compare, op.__name__, classmethod(validate_operation(op)))
so that:
import datetime
print(compare.gt(1, 2.0))
print(compare.gt(1, "a"))
print(compare.gt(2, 'b'))
print(compare.lt(datetime.date(2018, 9, 25), datetime.datetime(2019, 1, 31)))
print(compare.lt(datetime.date(2019, 9, 25), datetime.datetime(2020, 1, 31)))
would output:
False
None
Exception avoided for (<built-in function gt>, <class 'int'>, <class 'str'>)
None
None
Exception avoided for (<built-in function lt>, <class 'datetime.date'>, <class 'datetime.datetime'>)
None
and so that you can use an if
statement instead of an exception handler to validate a comparison:
result = compare.gt(obj1, obj2)
if result is None:
# handle the fact that we cannot perform the > operation on obj1 and obj2
elsif result:
# obj1 is greater than obj2
else:
# obj1 is not greater than obj2
And here are some timing statistics:
from timeit import timeit
print(timeit('''try:
1 > 1
except TypeError:
pass''', globals=globals()))
print(timeit('''try:
1 > "a"
except TypeError:
pass''', globals=globals()))
print(timeit('compare.gt(1, "a")', globals=globals()))
This outputs, on my machine:
0.047088712933431365
0.7171912713398885
0.46406612257995117
As you can see, the cached comparison validation does save you around 1/3 of time when the comparison throws an exception, but is around 10 times slower when it doesn't, so this caching mechanism makes sense only if you anticipate that the vast majority of your comparisons are going to throw an exception.
Upvotes: 1
Reputation: 3301
What you could do is use isinstance
before the comparison, and deal with the exceptions yourself.
if(isinstance(date_1,datetime) != isinstance(date_2,datetime)):
#deal with the exception
Upvotes: 0