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.