Numerical accuracy when converting from Single to float
Environment
- Pythonnet version: 3.0.0rc4
- Python version: 3.10.5
- Operating System: Windows 10 20H2
- .NET Runtime: 6.0.6
Details
-
Describe what you were trying to get done.
I'm sorry it's me again with some weird low-level issue. I use a DLL where a function returns a
System.Single. Its return value is0.01. Before pythonnet version 3.0 this got converted to a Pythonfloatalso of value0.01. With Pythonnet 3.0 I get a Pythonfloat0.009999999776482582instead. I can reproduce this issue by creatingSystem.Singleobjects. -
What commands did you run to trigger this issue? If you can provide a Minimal, Complete, and Verifiable example this will help us understand the issue.
import clr import System # Parse a string to avoid conversion from Python float to dotnet print(System.Single.Parse('1e-2')) # Create a pure dotnet object without conversion back to Python (I think .ToString() gets called here?) print(System.Single(0.01)) # Create a pure dotnet object without conversion back to Python and convert back to Python print(System.Single(0.01).MemberwiseClone())0.009999999776482582 0.01 0.009999999776482582With pythonnet 2.5.2 this will return
0.01 0.01 0.01
Always check your types )
The three expressions perhaps surprisingly give values of two different types: the 1st and 3rd are Python float (which in CPython is a float64, so same as System.Double), and the 2nd is System.Single (float32). And the values are actually equal. The reason you see different strings has to do with how precision of the number type affects rounding.
Sadly, it looks like the current behavior deserves its place in the famous JavaScript WAT video.
The cause for type to be different is that in most cases any System.Single value returned to Python from a call to .NET is automatically widened without loss of precision to Python float (again, which is == float64 == System.Double) similarly to how System.Int32 is widened to Python int - for convenience. The direct invocation of System.Single "constructor" is AFAIK the only exception because it is used to explicitly convert type to System.Single in order to choose a .NET overload when necessary.
So I guess that WAT-like behavior is by design, and we won't change it in 3.0.0
Though while playing with your code I noticed that in 3.0 (at least) float(System.Single(42)) fails, and so does any attempt to use int() or float() on instances of primitive .NET types, which looks like a bug, but it also might be intentional.
@filmor thoughts on the float(System.Single(42)) issue?
My experiments:
>>> parsed = System.Single.Parse('1e-2')
>>> from_py = System.Single(0.01)
>>> cloned = from_py.MemberwiseClone()
>>> parsed
0.009999999776482582
>>> from_py
<System.Single object at 0x000001E25534B580>
>>> cloned
0.009999999776482582
>>> from_py.ToString()
'0.01'
>>> float(from_py)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: float() argument must be a string or a real number, not 'Single'
>>> parsed == from_py
True
>>> float(from_py)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: float() argument must be a string or a real number, not 'Single'
>>> from_py2 = System.Single(0.02)
>>> parsed == from_py2
False
>>> System.Double(from_py)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: System.Single value cannot be converted to System.Double
>>> System.Array[int](10).Length
10
>>> type(System.Array[int](10).Length)
<class 'int'>
>>> float(System.Int32(42))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: float() argument must be a string or a real number, not 'Int32'
>>> int(System.Int32(42))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'Int32'
Thank you for that detailed explanation!
The reason you see different strings has to do with how precision of the number type affects rounding.
I guessed it's something related to this. That's why I chose that title with accuracy. Floats can be evil sometimes.
Sadly, it looks like the current behavior deserves its place in the famous JavaScript WAT video.
Thank you, I did not know the video. I laughed out loud and can't wait to send it to colleagues tomorrow. 😂
Though while playing with your code I noticed that in 3.0 (at least)
float(System.Single(42))fails, and so does any attempt to useint()orfloat()on instances of primitive .NET types, which looks like a bug, but it also might be intentional.
That's reassuring, I thought I was just too stupid to make it a Python datatype again and therefore came up with
System.Single(0.01).MemberwiseClone()
I guess float(Single(42)) used to work because we implicitly converted Single to Python's float. We currently don't implement __float__ or __int__ on any .NET types, that is what's missing here.
As a workaround I can use numpy for the rescue and its float32-datatype
import clr
import numpy as np
import System
s = System.Single(0.01).MemberwiseClone()
print(s)
n = np.float32(s)
print(n)
0.009999999776482582
0.01