Inspired by and created because of python3 utility: convert safely to int and driven partly by the 'refactor' in my answer.
The aforementioned question and my answer drove me to go and do a thing while bored at work - I created a new object class called StrictInt
that behaves much like the function created by the original poster of that question/thread, but as an object class instead.
This object class is a subclass of int
(which is the superclass. It's designed to work just like an int
, but when you cast a str
, int
, or float
to the StrictInt
object, it will only convert the value if it is indeed an integer.
Because I'm crazy, you'll also need the typing
module, but most Python 3 installs have that, if not, grab it from pip3
.
This is the code used for the StrictInt class, and any required imports, in the strictint.py
file:
from typing import Union, Any
class StrictInt(int):
def __new__(cls, value, *args, **kwargs):
# type: (Union[int, float, str], Any, Any) -> int
if not isinstance(value, (int, StrictInt, float, str)):
t = str(type(value)).replace("<class '", "").replace("'>", "")
raise TypeError("Cannot convert type '{type}' to strict integer".format(type=t))
try:
f = float(value)
except ValueError:
f = None
if not f:
raise ValueError("Cannot convert a non-number to a strict integer.")
if not f.is_integer():
raise ValueError("Cannot convert value due to non-integer parts.")
return super(StrictInt, cls).__new__(cls, int(f))
There is a set of unit tests I've been using as well (tests.py
):
from strictint import StrictInt
import unittest
class TestStrictInt(unittest.TestCase):
def test_float_conversion(self):
# Non-integer parts present in a float, should raise ValueError
self.assertRaises(ValueError, StrictInt, 3.14159)
# Float that is equal to an int should be equal.
self.assertEqual(3.0, StrictInt(3.0))
def test_ints(self):
# int(3) should equal StrictInt(3).
self.assertEqual(3, StrictInt(3))
def test_nonnumeric_string(self):
# Not a number at all.
self.assertRaises(ValueError, StrictInt, "I Am A Teapot")
# Number with an invalid character in it, so Not a Number.
self.assertRaises(ValueError, StrictInt, " 3.14159")
# Has numeric content, but not a valid number due to dots.
self.assertRaises(ValueError, StrictInt, "3.14.156")
def test_numeric_string(self):
# int('3') should equal StrictInt('3')
self.assertEqual(int('3'), StrictInt('3'))
# int(float('3.0')) is equal to int(3.0), and should equal StrictInt('3.0')
self.assertEqual(int(float('3.0')), StrictInt('3.0'))
# String with a number that has a decimal part should raise ValueError
self.assertRaises(ValueError, StrictInt, '3.14159')
if __name__ == '__main__':
unittest.main(warnings='ignore')
Any and all improvement suggestions are welcome. This works pretty well and fairly quickly from what I've tested, but I value everyone's opinions.
3 Answers 3
Converting everything via a
float
means that you get the wrong result whenever the input cannot be represented exactly as a double-precision floating-point number. For example, this is surely not acceptable:>>> StrictInt(10**23) 99999999999999991611392
There's an
OverflowError
when the input is too large to be represented as afloat
:>>> StrictInt(10**400) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "cr195375.py", line 13, in __new__ f = float(value) OverflowError: int too large to convert to float
The strategy of enumerating the allowable types
(int, StrictInt, float, str)
means that many plausible use cases are disallowed, for example withfractions.Fraction
:>>> from fractions import Fraction >>> StrictInt(Fraction(10, 1)) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "cr195375.py", line 10, in __new__ raise TypeError("Cannot convert type '{type}' to strict integer".format(type=t)) TypeError: Cannot convert type 'fractions.Fraction' to strict integer
and similarly with
decimal.Decimal
.The Pythonic approach is to use duck typing — that is, instead of testing whether input belongs to a fixed collection of types, you call the appropriate methods on the input. Here we want to determine whether the input has a non-zero fractional part, and otherwise to use the integer part. Hence we need Python's built-in
divmod
function:quotient, remainder = divmod(value, 1) if remainder: raise ValueError("could not convert value due to non-zero " f"fractional part: {val!r}")
There is no point taking
*args
and**kwargs
if you are not going to use them.
Putting this together:
class StrictInt(int):
"Subclass of int that refuses to coerce non-integer values."
def __new__(cls, value):
if isinstance(value, str):
for converter in (int, float, complex):
try:
value = converter(value)
break
except ValueError:
pass
else:
raise ValueError(f"invalid literal for {cls.__name__}(): "
f"{value!r}")
if value.imag:
raise ValueError("could not convert value due to non-zero "
f"imaginary part: {value!r}")
quotient, remainder = divmod(value.real, 1)
if remainder:
raise ValueError("could not convert value due to non-zero "
f"fractional part: {value!r}")
return super(StrictInt, cls).__new__(cls, int(quotient))
This handles a wider ranges of input types:
>>> StrictInt(Fraction(10, 2))
5
>>> StrictInt(Decimal('11.00000'))
11
>>> StrictInt(5+0j)
5
and it copes with integers that are too large to be represented as floats:
>>> StrictInt(10**400) == 10**400
True
-
\$\begingroup\$ Do we also have a Python 3.5 compatible version of the value error strings? f-prefix isn't supported in there and is one of the Python languages I have a CI running for this whenever I upload changes to the private repo. Would that just be the long-string with a
.format()
at the end? \$\endgroup\$Thomas Ward– Thomas Ward2018年05月29日 13:28:30 +00:00Commented May 29, 2018 at 13:28 -
\$\begingroup\$ Yes, if you don't have f-strings you can use
format
instead. \$\endgroup\$Gareth Rees– Gareth Rees2018年05月29日 13:33:41 +00:00Commented May 29, 2018 at 13:33 -
\$\begingroup\$ Thanks. Just for thoroughness, the
value!r
string wasn't supported for some reason in the.format()
method. As such, I went digging in PEP 498 and found thatvalue!r
is equivalent torepr(value)
, so I had to use this in place ofvalue!r
. Maybe my Python is being stupid/weird, but meh. \$\endgroup\$Thomas Ward– Thomas Ward2018年05月29日 13:50:17 +00:00Commented May 29, 2018 at 13:50 -
1\$\begingroup\$ You can write
"{value!r}".format(value=value)
, referring to the parameter by name, or"{0!r}".format(value)
, referring to the parameter by numbered position, or just"{!r}".format(value)
, since positions are implicitly numbered starting at 0. See the Format String Syntax documentation. \$\endgroup\$Gareth Rees– Gareth Rees2018年05月29日 13:55:18 +00:00Commented May 29, 2018 at 13:55
naming
I try to avoid 1-letter variable names, unless it are x, y, z
for coordinates, or i
during an iteration, so I would rename f
to float_value
or something, but that is a matter of taste.
short_circuit
if value
is an int
or StrictInt
already, you can return early. This way, you also
failing test-cases
there are a few cases that fail the current implementation that are integers
StrictInt(0)
StrictInt(0.0)
StrictInt(3 + 0j)
StrictInt('3 + 0j')
fixing the first is as easy as changing if not f:
to if f is None
Complex
adding support for complex is rather easy, and eliminates the need to call float
class StrictInt(int):
def __new__(cls, value, *args, **kwargs):
# type: (Union[int, float, str], Any, Any) -> int
if isinstance(value, (int, StrictInt)):
return super(StrictInt, cls).__new__(cls, value)
if isinstance(value, str):
value = value.replace(' ', '')
elif not isinstance(value, (float, complex)):
type_str = str(type(value)).replace("<class '", "").replace("'>", "")
raise TypeError("Cannot convert type '{type}' to strict integer".format(type=type_str))
try:
complex_value = complex(value)
except ValueError:
raise ValueError("Cannot convert a non-number to a strict integer.")
if complex_value.imag:
raise ValueError('Cannot convert complex number with imaginary part')
float_value = complex_value.real
if not float_value.is_integer():
raise ValueError("Cannot convert value due to non-integer parts.")
return super(StrictInt, cls).__new__(cls, int(float_value))
Python 2
If you want to include python 2, unicode
should also be accepted as type
-
\$\begingroup\$ Regarding Python 2: I wrote it to be Py3 compliant. Otherwise I have to throw in the platform module and do additional logical testing to protect against a Py3 crash when it sees 'unicode' as a type test. \$\endgroup\$Thomas Ward– Thomas Ward2018年05月29日 11:33:34 +00:00Commented May 29, 2018 at 11:33
-
\$\begingroup\$ Also, the string
'3 + 0j'
isn't actually a number, it's a string with spaces. Are you saying this needs to be eval'd before we handle whether it can be converted? \$\endgroup\$Thomas Ward– Thomas Ward2018年05月29日 13:21:13 +00:00Commented May 29, 2018 at 13:21 -
\$\begingroup\$ This is handled fairly well in the other answer, but thank you for your review :) \$\endgroup\$Thomas Ward– Thomas Ward2018年05月29日 14:36:38 +00:00Commented May 29, 2018 at 14:36
-
\$\begingroup\$ I have no problem admitting the other answer is better \$\endgroup\$Maarten Fabré– Maarten Fabré2018年05月29日 14:48:10 +00:00Commented May 29, 2018 at 14:48
Some thoughts in no particular order.
First of all, I'm not sure of the benefit of making it a class. Given that it is a class, it should override __repr__
so it is clear when someone is working with a "StrictInt object" rather than an int. Also, StrictInt need not be given in the list of types passed to isinstance, since it's a subclass of int - and of Number - but others have pointed out flaws in the type-checking approach anyway.
Support for the Decimal and Fraction types may be useful, and in fact you could support any type that implements the Number
ABC. Gareth Rees's answer covers this reasonably well. The numeric side can be simplified a great deal, though - other than strings, the only types you are contemplating supporting are all numeric types. Numeric types are required to ensure they have correct comparison (and hash value) to other numeric types, and it's unlikely they'd get it wrong for int of all things, so you can leverage this by simply checking if the original input is equal to the integer.
if isinstance(value, str):
...logic for string conversion
else:
v = int(value)
if v != value: raise...
Note that the int() conversion will not allow complex types, so you do have some additional work to do if you want to support "complex but imaginary is zero".
Handling strings totally correctly is actually more ambitious than it sounds - others have already pointed out some of the problems with the float approach. The fractions.Fraction constructor is available for handling full decimal notation (so 1.00, 1.2e3, 1e23, 1e999*, will all be integers with their correct values, and 1.0000000000000001, 1.234e2 will not be), and you can simply check that the result has a denominator of 1.
*However, if you're handling untrusted input, you should be aware that unlimited exponents present a potential denial of service attack, and use a different approach, possibly duplicating some of the logic from the fractions module.
-
\$\begingroup\$ I disagree on the
__repr__
override, if only because a StrictInt is still an integer, but with specific rules about how you can cast things as StrictInt. While it is its own type of object, it's still anint
, so if you doprint(StrictInt(12.0))
orrepr(StrictInt(12.0))
you should get the same output as if you doprint(int(12.0))
orrepr(int(12.0))
, which is12
. \$\endgroup\$Thomas Ward– Thomas Ward2018年05月30日 15:31:21 +00:00Commented May 30, 2018 at 15:31 -
\$\begingroup\$ @ThomasWard While the first case is reasonable (and hence why I didn't suggest overriding
__str__
), I expect repr to provide enough information to determine the actual type of an object. A bool is still an int too, but repr(True) isn't 1. But without more information about why you think it's useful for it to be a type, I can't comment further. \$\endgroup\$Random832– Random8322018年05月30日 15:49:15 +00:00Commented May 30, 2018 at 15:49 -
\$\begingroup\$ As I said, I was bored, and needed something to do; this just happened to pop into my mind. (about 40% of my projects end up being the brainspawn of an idle mind) \$\endgroup\$Thomas Ward– Thomas Ward2018年05月30日 16:14:01 +00:00Commented May 30, 2018 at 16:14
Explore related questions
See similar questions with these tags.