Overview
Continuation from the previous post↓
JavaScript-san: "We have this thing called NaN. It stands for Not-a-Number, but the type is actually number."
Midori-san: "Gahaha. That's tricky. We don't have anything like that in Python."
Python-san: "We do."
Here it comes. This is the kind of thing that makes playing with programming so much fun. It's time for another day of Python.
To my surprise, Python also has NaN
Seriously?!
print(float('NaN')) # nan
print(float('nan')) # nan
# In JavaScript, there's that amusing behavior: Not-a-Number is a number.
# In Python, it's Not-a-Number is a float.
print(type(float('NaN'))) # <class 'float'>
````
I had no idea you could pass strings like that into `float()`! It’s not relevant this time, but apparently you can also pass `'inf'` and such.
## Since it's a float, can it be negative too?
Remember this from before?
* [(2024-02-04) Python Minus Zero Bits](/en/python-minus-float)
> Midori-san: "Huh? No way, right? Zero isn't positive or negative."
> Python-san: "It is."
> Zero in float type can be negative.
So, if NaN is a float type, wouldn't it naturally follow that it can also be negative? Could it be that Not-a-Number is a float **and it can be negative**?
```python
# One well-known thing about float is the existence of negative zero.
# You can create it like this:
print(float('-0.0')) # -0.0
# Or like this:
print(0.0 * -1) # -0.0
print(0.0 / -1) # -0.0
# So can NaN be negative too?
# Nope.
print(float('-NaN')) # nan
print(float('NaN') * -1) # nan
print(float('NaN') / -1) # nan
Nope... or so I thought!
# A passing detective: "Hold on. Have you checked it at the binary level?"
import struct
print(struct.pack(">d", float('NaN'))) # b'\x7f\xf8\x00\x00\x00\x00\x00\x00'
print(struct.pack(">d", float('-NaN'))) # b'\xff\xf8\x00\x00\x00\x00\x00\x00'
# What the—?! The first bytes are \x7f and \xff?!
Something's different!
At the end of the minus-zero post, I wrote something like this↓
float
, or floating-point numbers, consist of the following structure:
- Sign (positive or negative)
- Mantissa
- Exponent
Since NaN is a float, we should be able to break it down into these parts. Let's try.
# As floats
nan = float('NaN')
minus_nan = float('-NaN')
# As bytes
nan_bytes = struct.pack(">d", nan)
minus_nan_bytes = struct.pack(">d", minus_nan)
# As integers
nan_int = int.from_bytes(nan_bytes, "big")
minus_nan_int = int.from_bytes(minus_nan_bytes, "big")
# As binary strings
nan_bits = f"{nan_int:064b}"
minus_nan_bits = f"{minus_nan_int:064b}"
# Finally, decompose float into sign, mantissa, and 10**exponent
print(f"NaN: {nan_bits[0]} {nan_bits[1:12]} {nan_bits[12:]}")
print(f"-NaN: {minus_nan_bits[0]} {minus_nan_bits[1:12]} {minus_nan_bits[12:]}")
# NaN: 0 11111111111 1000000000000000000000000000000000000000000000000000
# -NaN: 1 11111111111 1000000000000000000000000000000000000000000000000000
# Only the first part is different.
So yeah, NaN
and -NaN
are different! Just to confirm, let's also check with a regular float.
# Compare 1.234 and -1.234
sample = 1.234
minus_sample = -1.234
sample_bytes = struct.pack(">d", sample)
minus_sample_bytes = struct.pack(">d", minus_sample)
sample_int = int.from_bytes(sample_bytes, "big")
minus_sample_int = int.from_bytes(minus_sample_bytes, "big")
sample_bits = f"{sample_int:064b}"
minus_sample_bits = f"{minus_sample_int:064b}"
print(f"1.234: {sample_bits[0]} {sample_bits[1:12]} {sample_bits[12:]}")
print(f"-1.234: {minus_sample_bits[0]} {minus_sample_bits[1:12]} {minus_sample_bits[12:]}")
# 1.234: 0 01111111111 0011101111100111011011001000101101000011100101011000
# -1.234: 1 01111111111 0011101111100111011011001000101101000011100101011000
# The only difference is the first part, confirming it represents + and -.
Therefore, “NaN is a float, so it can have a minus sign!” Not-a-Number is a float and it can be negative!
The End
This is just too fun.