Sitelet https://github.com/python/cpython/issues/70867
Skip to content

Incorporating float.is_integer into Decimal #70867

Description

@rob-smallshire
BPO 26680
Nosy @tim-one, @rhettinger, @tiran, @serhiy-storchaka, @rob-smallshire, @abingham
PRs
  • bpo-26680: Incorporate is_integer in all built-in and standard library numeric types #6121
  • Revert "bpo-26680: Incorporate is_integer in all built-in and standard library numeric types" #22584
  • Files
  • is_integer_numeric_tower.patch: Patch to introduce is_integer to the numeric tower
  • is_integer_decimal.patch: Patch introducing is_integer to Decimal
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/rhettinger'
    closed_at = <Date 2021-05-06.20:54:14.940>
    created_at = <Date 2016-03-31.19:34:24.187>
    labels = ['type-feature', 'library', '3.11']
    title = 'Incorporating float.is_integer into Decimal'
    updated_at = <Date 2021-05-06.20:54:14.939>
    user = 'https://github.com/rob-smallshire'

    bugs.python.org fields:

    activity = <Date 2021-05-06.20:54:14.939>
    actor = 'rhettinger'
    assignee = 'rhettinger'
    closed = True
    closed_date = <Date 2021-05-06.20:54:14.940>
    closer = 'rhettinger'
    components = ['Library (Lib)']
    creation = <Date 2016-03-31.19:34:24.187>
    creator = 'robert_smallshire'
    dependencies = []
    files = ['42335', '42336']
    hgrepos = []
    issue_num = 26680
    keywords = ['patch']
    message_count = 67.0
    messages = ['262702', '262704', '262714', '262715', '262721', '262724', '262726', '262824', '262846', '262848', '262850', '262852', '262882', '262900', '262901', '262904', '262906', '262909', '313551', '313579', '313645', '313654', '313655', '313674', '313680', '313681', '313683', '313689', '313690', '313693', '313707', '313870', '313872', '313873', '313874', '313878', '313880', '313881', '313882', '313895', '313897', '313901', '313902', '313904', '313907', '350164', '350172', '350173', '350190', '377774', '377921', '377924', '377925', '377926', '377971', '377972', '377996', '377999', '378005', '378060', '378127', '378138', '378198', '378199', '378227', '378295', '393148']
    nosy_count = 6.0
    nosy_names = ['tim.peters', 'rhettinger', 'christian.heimes', 'serhiy.storchaka', 'robert_smallshire', 'Austin Bingham']
    pr_nums = ['6121', '22584']
    priority = None
    resolution = 'remind'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue26680'
    versions = ['Python 3.11']

    Activity

    1. rob-smallshire commented on Mar 31, 2016

      rob-smallshiremannequin
      MannequinAuthor

      When the useful float.is_integer method was added the opportunity was missed to incorporate this method into the numeric tower defined in numbers.py. This increased the API distance between different number types, making them less substitutable than previously, leading to what might be considered to be absurd behaviour:

        >>> a = 5.0
        >>> b = 5
        >>> a.is_integer()
        True
        >>> b.is_integer()
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        AttributeError: 'int' object has no attribute 'is_integer'

      The first attached patch modifies Python to:

      1. Implement int.is_integer() to always return True
      2. Add Real.is_integer() as an abstract method in numbers.py
      3. Provide a default implementation in Rational.is_integer() in numbers.py
      4. Adds tests for is_integer() for int and Fraction.
      5. Documentation changes commensurate with above.

      Although the Decimal type deliberately lies outside the numeric tower for reasons not relevant here, the principle of least surprise suggests that it too should support is_integer(). In fact, the implementation already contains just such a function, although it is not exposed to Python. The second patch implements is_integer() for both the pure Python and C implementations of Decimal, again with commensurate tests and documentation changes.

      I hope these changes can be implemented to reduce the degree of surprise encountered when working with different number types in Python.

    2. added
      type-featureA feature request or enhancement
      stdlibStandard Library Python modules in the Lib/ directory
      on Mar 31, 2016
    3. rob-smallshire commented on Mar 31, 2016

      rob-smallshiremannequin
      MannequinAuthor

      Adding the second patch file.

    4. rhettinger commented on Apr 1, 2016

      @rhettinger
      Contributor

      -0

      I question whether we ever needed a short-cut for x==int(x). Adding this to the numeric tower would cause it to propagate broadly including the int type. To me, this seems like feature creep resulting in language bloat.

      The decimal module has been around for a long time and no one has ever requested the feature. This suggests it would be just another unused method in a module that already has learnability and usability problems due to a fat API.

    5. rhettinger commented on Apr 1, 2016

      @rhettinger
      Contributor

      One other thought: the name is_integer() is inconsistent with the nomenclature in numbers.py. Had this been included at the outset, its name would have been is_integral().

    6. serhiy-storchaka commented on Apr 1, 2016

      @serhiy-storchaka
      Member

      Agree with Raymond.

      float.is_integer(x) is more efficient than x==int(x), but is this method used anywhere at all? It was added as a part of bpo-2224.

    7. skrah commented on Apr 1, 2016

      skrahmannequin
      Mannequin

      is_integer() is very important for writing new functions. libmpdec has
      it and it's used a lot inside mpdecimal.c.

      In this case though I assume Robert needs it for duck typing.

    8. rob-smallshire commented on Apr 1, 2016

      rob-smallshiremannequin
      MannequinAuthor

      As for whether the shortcut float.is_integer(x) was needed, it has different behavior to x==int(x) when x is either NaN or an infinity. We must even deal with two different exception types OverflowError or ValueError respectively for these two values on conversion to int. That float.is_integer() simply returns False for these values makes it more straightforward to use robustly. The same would go for Decimal, which has the same behavior with respect to NaNs and infinities as float.

      I agree that is_integral may have been a better name, although is_integer has the advantage that it avoids conflating numeric values with either of the types 'int' or 'Integral'.

      The motivation for my patches is to converge the interfaces of the various number types so that we can simply, and robustly, check for integer values (as opposed to integer types) without needing to be concerned about the concrete number type, so long as it is Real. Indeed, this is largely the point of having a numeric tower at all. I am more motivated by usability and concision and correctness than efficiency concerns: I believe that where
      possible we should allow one number type to be substituted for another, and in particular int for any other Real type where purely mathematical - rather than representational operations - are in play.

      Use of the existing float.is_integer is compromised by the fact that people have an entirely reasonably habit of passing integers (particularly literals) to functions which accept floats which then fail if they use float.is_integer.

      Adding this method would reduce the educational load as the various number types would be more similar, not less.

      I work in industrial fields where computational geometry, and hence rationals, floats, infinities and large integers are a day-to-day occurrence. Ultimately, I care more about consistency within the numeric tower types (Real, float, int, Rational, Integral, Fraction) than I do about Decimal, which is why I separated my changes to Decimal into a separate patch.

    9. skrah commented on Apr 3, 2016

      skrahmannequin
      Mannequin

      I've been thinking about this, and I'm +1 for the change now.

      These structural typing issues for numbers come up regularly
      (see also msg257088), and the functions are so simple and
      self-explanatory that API-complexity does not really increase.

      In general, I understand the argument that Python has become
      too complex in some areas -- recently I had to go back to
      Python-2.7 in order to understand a certain detail about
      .pyc files (in 2.7 it was immediately obvious).

      But here I see on such problems.

    10. rhettinger commented on Apr 4, 2016

      @rhettinger
      Contributor

      the functions are so simple and self-explanatory
      that API-complexity does not really increase.

      It increases complexity because it will show-up everywhere including places where it makes little sense.

      One place is int objects where its meaning and purpose will seem arcane to most Python programmers. For int objects, this is just total waste.

      With the fractions module, the method is also unnecessary because integral fractions all have a denominator of 1.

      With the decimal module, we were careful not to grow the API beyond what was in the spec or what was necessary to integrate it into Python (i.e. the usual magic methods). I think that was a good decision and would like to keep it that way.

      In general, the need for this method is almost nil (it would be a very, very rare Python programmer who would ever need this, and those that might what it are perfectly capable of writing a short function to handle their own requirements). In the OPs case, the motivation isn't inability to determine whether something is integral, it is more a desire for the test to be polymorphic with other types where the methods do not add any real value.

      The OPs notion of "absurd" behavior implies a rule that all float methods should be available for ints. That would suggest the is_integer, hex, fromhex, and as_integer_ratio would all need to propagate to the other types as well. I don't think we should start sliding down that slope.

      Another thought is that future updates to the decimal spec could make conflicting choices about what Decimal.is_integral() would return for Decimal('Infinity'). There could be a case to be made for true, for false, for NaN, or for setting one or more of the signal flags or traps.

      I'm glad that the OP separated out the request for Decimal given that his actual use cases involve everything except Decimal. The decimal class is intentionally not registered a Real (only as a Number) because it isn't interoperable with binary floats; hence, there has been no need to copy all the float methods into Decimal.

      AFAICT, this comes down to whether to push a float method into other types where we otherwise wouldn't do it, just to save the OP from one-line function:

      is_integral = lambda x:  isinstance(x, int) or isinstance(x, Fraction) and x.denominator == 1 or isinstance(x, float) and x.is_integer()
    11. rhettinger commented on Apr 4, 2016

      @rhettinger
      Contributor

      FWIW, I think the reasoning in http://bugs.python.org/issue1093 applies here as well. The need is insufficient to warrant inclusion in the numeric tower and propagation to types like int and Fraction.

    12. rob-smallshire commented on Apr 4, 2016

      rob-smallshiremannequin
      MannequinAuthor

      To be clear, I'm not arguing that is_integer is in the same category as hex and fromhex; is_integer is a mathematical property of the number, whereas hex and from hex are representational. Nobody expects interoperability of string representations of the different number types.

      Neither do I take issue with the general argument against enlarging the API. In fact, if float.is_integer did not exist, I would not be campaigning for it to be invented.

      What I do take issue with is already having a method on float which makes sense for all Real numbers, but then not supporting it for those other Real types. This just gets in the way of programming generally in terms of *numbers* rather than concrete types, especially when the is_integer method is being advocated as the *right way* to do such a test. This is can lead to other problems as people unthinkingly convert other number types to floats, with loss of information, just to get access to this convenience method.

      I'm (really) surprised that structural typing and polymorphism over numbers carries so little weight – a cursory look at StackOverflow* shows that there are already too many people being encouraged to answer the 'is integral' question with isinstance(x, int) when that is not what they mean or need.

      As a precedent we already have int.numerator, int.denominator, int.real and int.imag which are presumably using Raymond's argument are also 'total waste', but the reality is that the presence of these attributes causes no impediment to learning and makes many tasks easier with fewer special cases. When working with rationals, I frequently rely on the fact that ints implement the Rational interface. When working with complex numbers, I frequently rely on the fact that both int and float implement the Complex interface. For example. I have used use these attributes in the constructor for a fixed-point number type, and was thankful for their existence.

      I'm also happy to set the Decimal aspect of my proposal to one side as Decimal is explicitly outside the numeric tower.

      This isn't about me avoiding writing trivial one-line functions or "the OPs use case". I'm trying to help you make Python more predictable and easier to use. I'm far from the first person to be surprised by this.

      I'd be happy to trade adding is_integer to the numeric tower for a deprecation notice on float.is_integer - the outcome is the same - polymorphic number types with fewer special cases.

      <http://stackoverflow.com/questions/17170226/is-integer-not-working\>
      <http://stackoverflow.com/questions/6239967/determining-whether-an-value-is-a-whole-number-in-python/6239987#6239987\>
      <http://stackoverflow.com/questions/33002747/try-except-float-but-not-integer/33002796#33002796\>
      <http://stackoverflow.com/a/22053804/107907\>
      <http://stackoverflow.com/questions/36209324/trouble-using-is-integer-in-python\>

    13. skrah commented on Apr 4, 2016

      skrahmannequin
      Mannequin

      I agree that Robert's "absurdity" argument was unfortunate and could
      be reversed: Many people would consider an (10).is_integer() method
      absurd.

      I'm also only moderately interested in OOP or classification in general, but we *do* have a numeric tower modeled after Scheme,
      so here goes:

      scheme@(guile-user)> (integer? 487)
      $1 = #t
      scheme@(guile-user)> (integer? 1.2)
      $2 = #f
      scheme@(guile-user)> (integer? 1.0)
      $3 = #t
      scheme@(guile-user)> (integer? 1/7)
      $4 = #f
      scheme@(guile-user)> (integer? 100/10)
      $5 = #t
      scheme@(guile-user)>

      The ACL2 theorem prover has the same:

      ACL2 !>(integerp 100)
      T
      ACL2 !>(integerp 100/10)
      T
      ACL2 !>(integerp 100/7)
      NIL

      For me, these functions are something fundamental. I'd prefer
      them to be exposed in a functional manner like above, but we
      do have the numeric tower.

    14. 80 remaining items

    15. added
      3.11only security fixes
      and removed on May 4, 2021
    16. rhettinger commented on May 6, 2021

      @rhettinger
      Contributor

      This has gone stale and I've been unable to contact the OP. Marking as closed for now. Please reopen if this comes back to life again and I'll review the PR.

    17. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    3.11only security fixesstdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions