Binary Representation of Float in Python (Bits Not Hex)

Binary representation of float in Python (bits not hex)

You can do that with the struct package:

import struct
def binary(num):
return ''.join('{:0>8b}'.format(c) for c in struct.pack('!f', num))

That packs it as a network byte-ordered float, and then converts each of the resulting bytes into an 8-bit binary representation and concatenates them out:

>>> binary(1)
'00111111100000000000000000000000'

Edit:
There was a request to expand the explanation. I'll expand this using intermediate variables to comment each step.

def binary(num):
# Struct can provide us with the float packed into bytes. The '!' ensures that
# it's in network byte order (big-endian) and the 'f' says that it should be
# packed as a float. Alternatively, for double-precision, you could use 'd'.
packed = struct.pack('!f', num)
print 'Packed: %s' % repr(packed)

# For each character in the returned string, we'll turn it into its corresponding
# integer code point
#
# [62, 163, 215, 10] = [ord(c) for c in '>\xa3\xd7\n']
integers = [ord(c) for c in packed]
print 'Integers: %s' % integers

# For each integer, we'll convert it to its binary representation.
binaries = [bin(i) for i in integers]
print 'Binaries: %s' % binaries

# Now strip off the '0b' from each of these
stripped_binaries = [s.replace('0b', '') for s in binaries]
print 'Stripped: %s' % stripped_binaries

# Pad each byte's binary representation's with 0's to make sure it has all 8 bits:
#
# ['00111110', '10100011', '11010111', '00001010']
padded = [s.rjust(8, '0') for s in stripped_binaries]
print 'Padded: %s' % padded

# At this point, we have each of the bytes for the network byte ordered float
# in an array as binary strings. Now we just concatenate them to get the total
# representation of the float:
return ''.join(padded)

And the result for a few examples:

>>> binary(1)
Packed: '?\x80\x00\x00'
Integers: [63, 128, 0, 0]
Binaries: ['0b111111', '0b10000000', '0b0', '0b0']
Stripped: ['111111', '10000000', '0', '0']
Padded: ['00111111', '10000000', '00000000', '00000000']
'00111111100000000000000000000000'

>>> binary(0.32)
Packed: '>\xa3\xd7\n'
Integers: [62, 163, 215, 10]
Binaries: ['0b111110', '0b10100011', '0b11010111', '0b1010']
Stripped: ['111110', '10100011', '11010111', '1010']
Padded: ['00111110', '10100011', '11010111', '00001010']
'00111110101000111101011100001010'

Float representation of binary in Python (bits not hex)

Using struct.pack and struct.unpack:

>>> import struct
>>> n = '00111111100000000000000000000000'
>>> struct.unpack('f', struct.pack('i', int(n, 2)))[0]
1.0
  • int(.., 2) to convert the binary representation to int (base 2)
  • struct.pack('i', ..) to convert bytes (i: 32bit int)
  • struct.unpack('f', ...)[0] to convert bytes back to float.

For other struct format character, see Format charactes - struct module documentation.

Float to Binary and Binary to Float in Python

Copied from this answer and edited per suggestion from Mark Dickinson:

import struct

def float_to_bin(num):
return format(struct.unpack('!I', struct.pack('!f', num))[0], '032b')

def bin_to_float(binary):
return struct.unpack('!f',struct.pack('!I', int(binary, 2)))[0]

print float_to_bin(3.14) yields “01000000010010001111010111000011”.

print bin_to_float("11000000001011010111000010100100") yields “-2.71000003815”.

Float to binary

Next answer with a bit of theory.

Explanation below does not explain IEEE Floating Point standard only general ideas concerning representation of floating point numbers

Every float number is represented as a fractional part multiplied by an exponent multiplied by a sign. Additionally there is so called bias for exponent, which will be explained bellow.

So we have

  1. Sign bit
  2. Fractional part digits
  3. Exponent part digits

Example for base 2 with 8 bit fraction and 8 bit exponent

Bits in fraction part tell us which summands (numbers to be added) from sequence below are to be included in represented number value

2^-1 + 2^-2 + 2^-3 + 2^-4 + 2^-5 + 2^-6 + 2^-7 + 2^-8

So if you have say 01101101 in fractional part it gives

0*2^-1 + 1*2^-2 + 1*2^-3 + 0*2^-4 + 1*2^-5 + 1*2^-6 + 0*2^-7 + 1*2^-8 = 0.42578125

Now non-zero numbers that are representable that way fall between
2 ** -8 = 0.00390625 and 1 - 2**-8 = 0.99609375

Here the exponent part comes in. Exponent allows us to represent very big numbers by multiplying the fraction part by exponent. So if we have an 8bit exponent we can multiply the resulting fraction by numbers between 0 and 2^255.

So going back to example above let's take exponent of 11000011 = 195.

We have fractional part of 01101101 = 0.42578125 and exponent part 11000011 = 195. It gives us the number 0.42578125 * 2^195, this is really big number.

So far we can represent non-zero numbers between 2^-8 * 2^0 and (1-2^-8) * 2^255. This allows for very big numbers but not for very small numbers. In order to be able to represent small numbers we have to include so called bias in our exponent. It is a number that will be always subtracted from exponent in order to allow for representation of small numbers.

Let's take a bias of 127. Now all exponents are subtracted 127. So numbers that can be represented are between 2^-8 * 2^(0 - 127) and (1-2^-8) * 2^(255 - 127 = 128)

Example number is now 0.42578125 * 2^(195-127 = 68) which is still pretty big.

Example ends

In order to understand this better try to experiment with different bases and sizes for fractional and exponential part. At beginning don't try with odd bases because it only complicates things necessary.

Once you grasp how this representation works you should be able to write code to obtain representation of any number in any base, fractional/exponential part combination.

Converting float.hex() value to binary in Python

def float_to_binary(num):
exponent=0
shifted_num=num
while shifted_num != int(shifted_num):
shifted_num*=2
exponent+=1
if exponent==0:
return '{0:0b}'.format(int(shifted_num))
binary='{0:0{1}b}'.format(int(shifted_num),exponent+1)
integer_part=binary[:-exponent]
fractional_part=binary[-exponent:].rstrip('0')
return '{0}.{1}'.format(integer_part,fractional_part)

def floathex_to_binary(floathex):
num = float.fromhex(floathex)
return float_to_binary(num)

print(floathex_to_binary('0x1.a000000000000p+2'))
# 110.1

print(floathex_to_binary('0x1.b5c2000000000p+1'))
# 11.01101011100001

Explanation:

float.fromhex returns a float num. We'd like its binary representation.

{0:b}.format(...) returns binary representations of integers, but not floats.

But if we multiply the float by enough powers of 2, that is, shift the binary representation to the left enough places, we end up with an integer, shifted_num.

Once we have that integer, we are home free, because now we can use {0:b}.format(...).

We can re-insert the decimal point (err, binary point?) by using a bit of string slicing based on the number of places we had shifted to the left (exponent).

Technical point: The number of digits in the binary representation of shifted_num may be smaller than exponent. In that case, we need to pad the binary representation with more 0's on the left, so binary slicing with binary[:-exponent] won't be empty. We manage that with '{0:0{1}b}'.format(...). The 0{1} in the format string sets the width of the formated string to {1}, padded on the left with zeros. (The {1} gets replaced by the number exponent.)

How do I display the binary representation of a float or double?

C/C++ is easy.

union ufloat {
float f;
unsigned u;
};

ufloat u1;
u1.f = 0.3f;

Then you just output u1.u. You can adapt this implementation.

Doubles just as easy.

union udouble {
double d;
unsigned long u;
}

because doubles are 64 bit.

Java is a bit easier: use Float.floatToRawIntBits() combined with Integer.toBinaryString() and Double.doubleToRawLongBits combined with Long.toBinaryString().

How I can see the actual bit sequence of 0.1 in Python?

You could use the struct module to pack the floating-point value into a buffer. The "d" format string gives an 8-byte double.

import struct

x = float(0.1)
buf = struct.pack("d", x)
print(buf) # b'\x9a\x99\x99\x99\x99\x99\xb9?'

# Hex dump, for readability
print(" ".join("{:02X}".format(b) for b in buf)) # 9A 99 99 99 99 99 B9 3F

The buffer will reflect the native endianness of your system. You can also experiment with byte-order specifiers in the format string.

You can also re-interpret the same bytes as an integer. To do this, you can use the unsigned integer type of the same size (8 bytes). In this case, it would be the long long unsigned integer (format string "Q"):

# Interpret bytes as unsigned integer.
i, = struct.unpack("Q", buf)
print("{:0>16X}".format(i)) # 3FB999999999999A

Finally, if you want, you can interpret the buffer as a double, and confirm that the value survives a round-trip serialization:

x2, = struct.unpack("d", buf)
print(x2) # 0.1
print(x2 == x) # True

Viewing floating-point representation components

To view the individual components of the floating point number, you can examine parts of the integer equivalent using bitmasks.

import struct

x = -(16 + 4 + 2 + 1 + 0.5)
buf = struct.pack("d", x)
i, = struct.unpack("Q", buf)

Here, x has a value of -0b10111.1, or equivalently -0b1.01111 * 2^4.

There are two important notes about the IEEE 753 representation:

  • the exponent has a bias of 1023, so we expect a value of 1027 stored in the exponent.
  • the mantissa’s leading 1 is implicit, so we expect the mantissa bits to be 01111.

We can use the appropriate masks for the bit patterns shown in the question, then print in binary

print(format(i, '0>64b'))

SIGN_MASK = 0x8000000000000000
EXPN_MASK = 0x7FF0000000000000
MANT_MASK = 0x000FFFFFFFFFFFFF
print(format(i & SIGN_MASK, '0>1b')[:1])
print(format(i & EXPN_MASK, '0>11b')[:11])
print(format(i & MANT_MASK, '0>52b'))

Results:

1100000000110111100000000000000000000000000000000000000000000000
1
10000000011
0111100000000000000000000000000000000000000000000000


Related Topics



Leave a reply



Submit