Expose PyFloat_AsDouble to Python

Here's the short version: * "Convert float-like object to a float" is a useful operation * For a C extension, there's an obvious way to spell that operation: use `PyFloat_AsDouble` * In pure Python, there's no Obvious Way To Do It * Proposal: expose the equivalent of `PyFloat_FromDouble(PyFloat_AsDouble(obj))` to Python * Question: where should it go? Longer version: Various parts of CPython that expect a float will accept anything "float-like"; examples include most of the `math` module functions, the `struct` module, `ctypes`, `statistics`, old-style string formatting with `"%f"` and the like, anything that uses `PyArg_ParseTuple` with the `'d'` format converter, anything that uses argument clinic and declares an input as `double`, and more. This is a Good Thing: it means that as a Python user, you can make your own types compatible with these operations simply by giving them a `__float__` method. Now suppose that as a Python user and 3rd party library writer, I want to write my own function that similarly accepts anything float-like, following the same rules that core Python uses. If I'm writing a C extension module, this is trivially easy: I use `PyFloat_FromDouble`, which is part of the stable ABI. However, if I'm writing in pure Python, it's surprisingly awkward to clearly express the equivalent behaviour, and I'd like to have an obvious way to do it. Some of the non-obvious ways: 1. Use the `float` constructor. This _ought_ to be the one obvious way to do it: it'll certainly accept anything float-like and convert it to a float. But it will also accept instances of `str`, `bytes`, `bytearray`, and anything supporting the buffer protocol, so if you want to exclude those (for example, because passing a non-number to your numeric code is likely to be a coding error, and you'd like to catch it as such) you need to do a LBYL check. 2. Call the object's `__float__` method. But this is fraught with peril, too: for a proper equivalent, you need to be careful to look up `__float__` on the type, not the object itself. And then a new version of Python changes `PyFloat_AsDouble` to also accept objects with `__index__`, and suddenly your version no longer matches what Python does. (This happened.) 3. Leverage the `math` module's existing ability to do this. For example, I can do `x_as_float = math.copysign(x, x)`. This works, giving me _exactly_ the semantics that I want. But I think it would be stretch to call this an *obvious* way to do it. 4. Write your own small C extension containing a single function that looks like `return PyFloat_FromDouble(PyFloat_AsDouble(obj))` (with extra error checking, fast path for exact floats, etc.). So my modest proposal is: expose the conversion represented by `PyFloat_AsDouble` to Python somewhere: either on the `float` type itself, or somewhere in the standard library. My question for everyone on this list is: _if_ it were to be added, where should it go? (There's also the "what should it be called" question, of course.) I have a proof-of-concept PR [1] that exposes this in the `operator` module as `operator.as_float`. See also the discussion on the tracker [2]. [1] https://github.com/python/cpython/pull/20481 [2] https://bugs.python.org/issue40801

29.05.20 12:48, Mark Dickinson пише:
2. Call the object's `__float__` method. But this is fraught with peril, too: for a proper equivalent, you need to be careful to look up `__float__` on the type, not the object itself. And then a new version of Python changes `PyFloat_AsDouble` to also accept objects with `__index__`, and suddenly your version no longer matches what Python does. (This happened.)
There is yet one problem with calling the object's `__float__` method. It looks up `__float__` as object's attribute instead of type's attribute, so it is possible to override it for individual object.
So my modest proposal is: expose the conversion represented by `PyFloat_AsDouble` to Python somewhere: either on the `float` type itself, or somewhere in the standard library. My question for everyone on this list is: _if_ it were to be added, where should it go? (There's also the "what should it be called" question, of course.)
I prefer it to be an alternative float constructor. We can also add the corresponding constructor for complex, and add constructors which accept only str, bytes or bytes-like object (i.e. parse a text representation of the number). See also previous discussion about alternative constructors. [1] [1] https://mail.python.org/archives/list/python-ideas@python.org/thread/5JKQMIC...

29.05.20 14:22, Serhiy Storchaka пише:
I prefer it to be an alternative float constructor. We can also add the corresponding constructor for complex, and add constructors which accept only str, bytes or bytes-like object (i.e. parse a text representation of the number). See also previous discussion about alternative constructors. [1]
I can explain. It is not just style preference. The problem is that we have more than one numeric type. We have operator.index, and we can add operator.as_float and operator.as_complex. But what about Decimal and Fraction? And third-party numeric types? It would be impractical to make the operator module depending on the decimal and fractions modules.

On Fri, 29 May 2020 09:48:53 -0000 "Mark Dickinson" <mdickinson@enthought.com> wrote:
I have a proof-of-concept PR [1] that exposes this in the `operator` module as `operator.as_float`. See also the discussion on the tracker [2].
`operator.as_float` sounds good to me. There's the `operator.index` precedent. Regards Antoine.
participants (3)
-
Antoine Pitrou
-
Mark Dickinson
-
Serhiy Storchaka