Green Cloak Guy
Green Cloak Guy

Reputation: 24691

Check if two objects are comparable to each other, without relying on raised errors

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

Answers (2)

blhsing
blhsing

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

alex067
alex067

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

Related Questions