8

Python seems to lacks a base class for "all numbers", e. g. int, float, complex, long (in Python2). This is unfortunate and a bit inconvenient to me.

I'm writing a function for matching data types onto each other (a tree matching / searching algorithm). For this I'd like to test the input values for being lists, dicts, strings, or "numbers". Each of the four cases is handled separately, but I do not want to distinguish between int, long, float, or complex (though the latter will probably not appear). This would be rather easy to achieve if all number types would be derived from a base type number, but unfortunately, they are not, AFAICS.

This enforcement of explicitness makes it obvious that the unusual complex is included. Which typically raises questions which I rather not want to think about. My design rather says "all number types" than that explicit list. I also do not want to explicitly list all possible number types coming from other libraries like numpy or similar.

First question, rather a theoretical one: Why didn't the designers make all number types inherit a common number base class? Is there a good reason for this, maybe a theory about it which lets it seem not recommended? Or would it make sense to propose this idea for later versions of Python?

Second question, the more practical one: Is there a recommended way of checking a value for being a number, maybe a library call I'm not aware of? The straight-forward version of using isinstance(x, (int, float, complex, long)) does look like a clutch to me, isn't compatible to Python3 which doesn't know a long type anymore, and doesn't include library-based number types like numpy.int32.

Alfe
  • 56,346
  • 20
  • 107
  • 159
  • Btw, `numpy` has this chain of inheritance for `int32`: `object` → `generic` → `number` → `integer` → `signedinteger` → `int32`. This is maybe a bit overkill, but shows that this probably isn't a bad idea in general. – Alfe Jun 26 '17 at 09:18
  • For convenience, you can hide that "is number" logic inside a method. The easiest way to do it that I see is to execute `isinstance` for each number type. You can even put the types in a tuple/list/whatever and iterate over these, checking each one. It *is* verbose, but only inside the method, and you'll be sure that your number is of one of the types that you define. – Meloman Jun 26 '17 at 09:28

2 Answers2

12

There is actually a base class for those types you listed.

If you're not looking at numpy types, a good starting point would be numbers.Complex:

>>> import numbers
>>> isinstance(1+9j, numbers.Complex)
True
>>> isinstance(1L, numbers.Complex)
True
>>> isinstance(1., numbers.Complex)
True
>>> isinstance(1, numbers.Complex)
True

It gets a bit messier when you start to include those from numpy, however, the numbers.Complex abstract base class already handles a good number of the mentioned cases.

Moses Koledoye
  • 77,341
  • 8
  • 133
  • 139
  • I didn't know this `numbers` module and am still wondering how it works. Why is `isinstance(4, numbers.Number)` returning `True` when `type(4).__bases__` lists only `object`?? I'll have a look at that PEP 3141 mentioned in the documentation of `numbers` … – Alfe Jun 26 '17 at 09:35
  • 3
    ABCs can implement [`__isinstancecheck__`](https://docs.python.org/2/reference/datamodel.html#class.__instancecheck__) that allows them to act as virtual base classes for e.g. builtin classes. They may not be listed in the mro. – Moses Koledoye Jun 26 '17 at 09:38
  • 1
    Ah. Thanks for this! I think you solved my issue completely. And, btw, the `numpy` data types also are handled correctly by `isinstance(x, numbers.Number)`. – Alfe Jun 26 '17 at 09:39
  • 2
    @Alfe Careful when you use that with numpy. AFAIK, it does not work for all versions of numpy. – Moses Koledoye Jun 26 '17 at 09:41
  • Remains just the question why the language designers did not include this in the built-in stuff. Any rationale on this? – Alfe Jun 26 '17 at 09:41
  • 1
    @Alfe Adding that in the built-in introduces a performance overhead which far outweighs its use: a seldom used `isinstance` check. – Moses Koledoye Jun 26 '17 at 09:45
2

Sorry, a bit late to the party. But this might be helpful for future readers of this page. I suggest you use Python's "duck typing" character and its EAFP ("Easier to Ask Forgiveness than Permission") philosophy: in other words, just try using the object in question as a number. Write something like this:

def isnumber(thing):
    try:
        thing + 0
        return True
    except TypeError:
        return False

It should work for any type of number, including user-defined classes.

  • Welcome Jason and thank you for your answer! Yes, just trying to use it as a number would be an idea. Unfortunately adding a zero to a value might work for other kinds of objects which aren't really numbers (after all the plus operator can be overloaded by any class by implementing `__add__()`). It also isn't very clean code to use adding to find out if something is a number, as it isn't very obvious. I also considered for quite some time if there aren't any valid numbers which don't allow adding 0 (e.g. inf, -inf, NaN); turned out that there aren't but that doesn't make this a good approach. – Alfe Apr 29 '21 at 00:37
  • @Alfe I don't agree. I believe my solution is the Pythonic way to do it: don't ask what something "is", but what it does. And although no approach in Python could be foolproof (anyone can write a class with perverse behaviour), my approach actually works in every case I am aware of. Whereas the `numbers` module does not. Try creating an instance like `x = decimal.Decimal('3.4')`, testing it with `isinstance(x, numbers.Complex)` (this will give the wrong result), and with `x + 0` (this will give the right result). – Jason Johnston May 03 '21 at 02:52
  • A `decimal.Decimal` instance isn't a `numbers.Complex` (using `isinstance`). It is, however, a `numbers.Number`. If any kind of number would be a complex number (because mathematically ℝ is a subclass of ℂ), then this distinction between `numbers.Number` and `numbers.Complex` wouldn't make any sense. So I take it that `numbers.Complex` rather says something about the used datatype, and that (`decimal.Decimal`) just isn't capable of holding complex numbers. So please explain why you think the `isinstance` answer is wrong. – Alfe May 03 '21 at 06:20
  • I think you've missed my point which is that the success of querying `numbers` depends on the accidental fact of whether its implementers have accounted specifically for the type of object you are interested in. Would you agree that 'decimal.Decimal' can hold real numbers? Try `isinstance(decimal.Decimal('3.4'), numbers.Real)`. The returned answer is `False`, which is surely incorrect, or at least irrelevant to how you can actually use that particular object. – Jason Johnston May 03 '21 at 08:56
  • I think I didn't miss your point, but you keep backing it with bad examples. Decimal is neither a real nor a complex. It only can hold some rational nurmbers, and in a very special way (in decimal which is kind of special in computers). So the designers of the numbers module _decided_ that it should not declare to be real or complex but just number. You may disagree with this decision, but it is obviously not a bug. Adding zero on the other hand lets it appear as if anything I can add a zero to _must_ be a number. That just isn't the case, given that we are in universe of diverse classes. – Alfe May 03 '21 at 23:51