2

I have some functions doing math stuff which needs to take integer agrmuents.

I know that I can force using int by using condition isinstance(x, int)
or more strict type(x) == int, but IMO it isn't pythonic.
I think my Python code shouldn't reject 2.0 just because it's float.

What's the best way to check if value is logically integer?

By logically integer I mean value of any type, which represents integer.

It should be able to be used in arithmetic operations like int, but I don't have to check it,
because I belive that in Python any set of conditions can get fooled.

For example,
True, -2, 2.0, Decimal(2), Fraction(4, 2) are logically integers,
when '2' and 2.5 are not.

At the moment I use int(x) == x, but I'm not sure if it's the best solution.
I know I can use float(x).is_integer(). I also saw x % 1 == 0.

GingerPlusPlus
  • 5,336
  • 1
  • 29
  • 52
  • 1
    You could extend my answer here: http://stackoverflow.com/a/26892252/3001761. Alternatively, there's nothing wrong with `int(x) == x`. – jonrsharpe Nov 15 '14 at 13:20
  • 2
    you have a simple solution - use it. – ha9u63a7 Nov 15 '14 at 13:22
  • 1
    Note that just using `int(x) == x` is not necessarily enough. A custom class could implement `__int__` (the protocol called when `int` is called) but fail to have comparison operators that work with other numeric types like `float`. – ely Nov 15 '14 at 16:29
  • There's no way to determine that a class implements all of the operations you're going to need, other than to use the object as you need to, and deal with the exceptions. – Ned Batchelder Nov 15 '14 at 16:40
  • @NedBatchelder If you know all of the operations in advance, you can use them in my metaclass solution. If you don't know the operations that define the boundary of "integerness" (for your application) ahead of time, then I agree with you. – ely Nov 15 '14 at 16:41
  • @EMS you can tell that the methods are implemented. You can't tell that they will succeed, or that they do what you intend. – Ned Batchelder Nov 15 '14 at 16:42
  • I don't understand. You can place them in `try..except` and return `False` if they fail. If they succeed, then you can check the result to see if it matches the expectation. I guess, like unit testing, you can't test every possible input/output. But probably the limit of "integerness" is not defined that way, and checking for a few cases will be sufficient. – ely Nov 15 '14 at 16:44

4 Answers4

1

One way to solve this is by using a metaclass to define your custom implementation of __instancecheck__, then define a concrete class having the metaclass, and use isinstance with your concrete class.

This has the downside of pulling in metaclass machinery, which is often extraneous.

But it has the upside of cleanly encapsulating whatever properties you desire to use for what you mean by "logically integer" for your application.

Here's some code to show this approach:

class Integral(type):
    def __instancecheck__(self, other):
        try:
            cond1 = int(other) == other
            cond2 = (other >= 1.0) or (other < 1.0)
            # ... plus whatever other properties you want to check
            return all([cond1, cond2,])
        except:
            return False

class IntLike:
    __metaclass__ = Integral

print isinstance(-1, IntLike)
print isinstance('1', IntLike)
print isinstance(27.2, IntLike)
print isinstance(27.0, IntLike)
print isinstance(fractions.Decimal(2), IntLike)
print isinstance(fractions.Fraction(4, 2), IntLike)

It prints:

True
False
False
True
True
True

Note that it is important to get rid of the idea that the mathematical concept of being logically integer should apply to your program. Unless you bring in some proof-checking machinery, you won't get that. For example, you mention properties like certain functions being available, and specifically sqrt -- but this won't be available for negative integers unless you implement custom behavior to check for complex results.

It will be application-specific. For example, someone else might pick up this code and modify it so that '1' does register as IntLike, and perhaps for the sake of their application it will be correct.

This is why I like the metaclass approach here. It lets you explicitly denote each condition that you are imposing, and store them in one location. Then using the regular isinstance machinery makes it very clear to code readers what you are trying to do.

Lastly, note that no given conditions will always be perfect. For example, the class below could be used to 'fool' the int(x) == x trick:

class MyInt(object):
    def __init__(self, value):
        self.value = value

    def __int__(self):
        return int(self.value)

    def __add__(self, other):
        return self.value + other

    #... define all needed int operators

    def __eq__(self, other):
        if isinstance(other, float):
            raise TypeError('How dare you compare me to a float!')
        return self.value == other

    # ..etc

Then you get behavior like this:

In [90]: mi = MyInt(3)

In [91]: mi + 4
Out[91]: 7

In [92]: mi == 3
Out[92]: True

In [93]: int(mi) == mi
Out[93]: True

In [94]: mi == 3.0
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-93-827bea4a197f> in <module>()
----> 1 mi == 3.0

<ipython-input-89-66fec92fab7d> in __eq__(self, other)
     13     def __eq__(self, other):
     14         if isinstance(other, float):
---> 15             raise TypeError('How dare you compare me to a float!')
     16         return self.value == other
     17 

TypeError: How dare you compare me to a float!

and whether or not isinstance(mi, IntLike) returns True will be totally dependent on how you have implemented the comparison operators for MyInt and also whatever extra checks you have made in Integral's __instancecheck__.

ely
  • 74,674
  • 34
  • 147
  • 228
  • In Python 3 should be `class IntLike(metaclass = Integral): pass`. Also, AFAIK, `except:` isn't the best approach, better is `except Exception:`, because it lets exceptions like `KeyboardInterrupt` and `SystemExit` pass. – GingerPlusPlus Nov 15 '14 at 17:59
  • [Why is “except: pass” a bad programming practice?](http://stackoverflow.com/q/21553327/3821804) – GingerPlusPlus Nov 15 '14 at 19:57
  • I wouldn't design the logic this way - if user press Ctrl+C during the function execution, `False` is returned and program continues execution. – GingerPlusPlus Nov 15 '14 at 20:09
  • About your last comment, why handled exception exits the loop? – GingerPlusPlus Nov 15 '14 at 20:23
1

Normally one would check against the Integral ABC (Abstract Base Class), but floating point values are not normally meant to be treated as integers no matter their value. If you want that, take note of their is_integer property:

(1324.34).is_integer()
#>>> False

(1324.00).is_integer()
#>>> True

and the code is then just:

from numbers import Integral

def is_sort_of_integer(value):
    if isinstance(value, Integral):
        return True

    try:
        return value.is_integer()

    except AttributeError:
        return False

If you also want to deal with Decimals, Fractions and so on, which don't have an is_integer method, the best option would probably be just:

from numbers import Number, Integral

def is_sort_of_integer(value):
    if isinstance(value, Integral):
        return True

    if isinstance(value, Number):
        try:
            return not value % 1
        except TypeError:
            return False

    return False

The Integral check shouldn't be needed in this case, but it's probably best to keep it.

Veedrac
  • 58,273
  • 15
  • 112
  • 169
  • Thanks for mentioning `numbers.Integral`. I understood that when I need `int`, I can requie the argument to be `Integral`. None of other conditions mentioned in answers rejected for example `3786428937490273548972435971204632547289375498324893265.3`, which is too big float to be decremented. – GingerPlusPlus Nov 19 '14 at 07:48
0

There are some cases, which none of int(x) == x, x.isinteger() & x % 1 == 0 can handle the way I would like to.

Example:

>>> big_float = 9999999999999999.1

The big_float is big enough to ignore substracting some small number from it (AFAIK, it's called underflow):

>>> big_float -1 == big_float
True

Then

>>> def fib(n):
...     current, prev = 1, 0
...     while n > 0:
...         current, prev, n = current+prev, current, n-1
...     return prev
...
>>> fib(big_float) #unwanted infinite loop

There are some cases, which none of int(x) == x, x.isinteger() & x % 1 == 0 can handle the way I would like to.

>>> int(big_float) == big_float
True
>>> big_float.is_integer()
True
>>> big_float % 1 == 0
True

Solution

We can check if big_float in the same way as int(big_float):

>>> int(big_float) -1 == big_float -1
False

Of course, this method works also for more trivial cases, like this:

>>> x = 2.1
>>> int(x) -1 == x -1
False

Of course, you don't have to substract 1, you can use whatever mathematical operation you need.

Note that this condition may throw exception:

>>> x = '2'
>>> int(x) -1 == x -1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'str' and 'int'
GingerPlusPlus
  • 5,336
  • 1
  • 29
  • 52
-2

Simple, try to convert it to an integer. If int() works, it is "logically an integer", otherwise it is not.

try:
    int(thing)
    is_integer = True
except ValueError:
    is_integer = False

But, typically, rather than do it like this you would just use int(thing) in the code you needed this for, and just catch the error if it ends up not being an integer and handle that case appropriately.

ironfroggy
  • 7,991
  • 7
  • 33
  • 44