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
  • 4,944
  • 1
  • 27
  • 51
  • 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. – ha9u63ar 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
  • Perhaps that is what you meant. This question boils down to unit testing when you are in a dynamically typed language. But if your questions are like "is `int(x)` the same as `x`?" and "can `x` be compared to `float`, `fraction.Decimal`, and `fraction.Fraction`?" then you could cover these cases. Someone can always contrive a class that meets the conditions, but then behaves non-integery in some other way, but if that way wasn't important to testing, it shouldn't matter to the application. – ely Nov 15 '14 at 16:49

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
  • 70,012
  • 31
  • 140
  • 215
  • 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
  • The issue with Python 3 is pretty unrelated and doesn't really need to be documented here. The issue of converting some code from Python 2 to Python 3 would be a separate question outside of the scope of this question. Also, it seems like a bad idea to let those other exception types to pass. It super unrelated to the idea of checking for an integer, and seems very pedantic and nit-picky to even mention it here at all. Using a plain `except:` is perfectly fine and common in Python. – ely Nov 15 '14 at 18:33
  • [Why is “except: pass” a bad programming practice?](http://stackoverflow.com/q/21553327/3821804) – GingerPlusPlus Nov 15 '14 at 19:57
  • You don't seem to understand. I am not using `pass` but instead returning `False`. The logic of my program is *correct* to return `False` on any exception. – ely Nov 15 '14 at 20:00
  • 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
  • Hey, design it however you'd like. If you have magic users who can press Ctrl-C that fast, I'd like to meet them. Because the function here is not sleeping or delaying, the probability that a CTRL-C signal is received at that exact moment is so close to zero that it deserves no brain cycles to think about. – ely Nov 15 '14 at 20:13
  • I think your understanding of what will happen with a `KeyboardInterrupt` is also incorrect. For example, I put this code in a file called `except_pass.py` -- `while True: try: pass except: pass` (with indentation). If I run it from the console with `python except_pass.py`, then CTRL-C still causes the program to exit with a `KeyboardInterrupt`. And you can always fall back to CTRL-\ if you really have sleeping code where this is a true concern. – ely Nov 15 '14 at 20:19
  • About your last comment, why handled exception exits the loop? – GingerPlusPlus Nov 15 '14 at 20:23
  • 1
    Because it's almost literally impossible to time CTRL-C in such a way that the signal is handled during the execution of the `try` block. The only time this will happen is if the `try` block is executing very expensive code, or is intentionally sleeping or waiting on a system resource. If it is doing an easy calculation, it would be exponentially unlikely for the CTRL-C press to be handled during the `try` block. For this problem, where the body of `try` should be simple integer checks, this will be the case. If you replaced the first `pass` with say `time.sleep(1)` instead, then CTRL-C fails. – ely Nov 15 '14 at 20:27
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
  • 54,508
  • 14
  • 106
  • 164
  • 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
  • 4,944
  • 1
  • 27
  • 51
-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,654
  • 6
  • 31
  • 43