I came across a confusing problem when unit testing a module. The module is actually casting values and I want to compare this values.
There is a difference in comparison with ==
and is
(partly, I'm beware of the difference)
>>> 0.0 is 0.0
True # as expected
>>> float(0.0) is 0.0
True # as expected
As expected till now, but here is my "problem":
>>> float(0) is 0.0
False
>>> float(0) is float(0)
False
Why? At least the last one is really confusing to me. The internal representation of float(0)
and float(0.0)
should be equal. Comparison with ==
is working as expected.
If a
float
object is supplied tofloat()
, CPython* just returns it without making a new object.This can be seen in
PyNumber_Float
(which is eventually called fromfloat_new
) where the objecto
passed in is checked withPyFloat_CheckExact
; ifTrue
, it just increases its reference count and returns it:As a result, the
id
of the object stays the same. So the expressionreduces to:
But why does that equal
True
? Well,CPython
has some small optimizations.In this case, it uses the same object for the two occurrences of
0.0
in your command because they are part of the samecode
object (short disclaimer: they're on the same logical line); so theis
test will succeed.This can be further corroborated if you execute
float(0.0)
in separate lines (or, delimited by;
) and then check for identity:On the other hand, if an
int
(or astr
) is supplied, CPython will create a newfloat
object from it and return that. For this, it usesPyFloat_FromDouble
andPyFloat_FromString
respectively.The effect is that the returned objects differ in
id
s (which used to check identities withis
):*Note: All previous mentioned behavior applies for the implementation of python in
C
i.eCPython
. Other implementations might exhibit different behavior. In short, don't depend on it.This has to do with how
is
works. It checks for references instead of value. It returnsTrue
if either argument is assigned to the same object.In this case, they are different instances;
float(0)
andfloat(0)
have the same value==
, but are distinct entities as far as Python is concerned. CPython implementation also caches integers as singleton objects in this range -> [x | x ∈ ℤ ∧ -5 ≤ x ≤ 256 ]:In this example we can demonstrate the integer caching principle:
Now, if floats are passed to
float()
, the float literal is simply returned (short-circuited), as in the same reference is used, as there's no need to instantiate a new float from an existing float:This can be demonstrated further by using
int()
also:However, the results of
is
are also dependant on the scope it is being executed in (beyond the span of this question/explanation), please refer to user: @Jim's fantastic explanation on code objects. Even python's doc includes a section on this behavior: