Как представить float в виде integer без потери информации?

Есть задача преобразования float значений в значения типа int без потери информации, т.е., чтобы потом можно было выполнить обратное преобразование. Как это можно реализовать? На уме только сериализация с помощью pickle.dumps, а затем получать int из этих байт, но мне кажется не совсем рациональным решением. И вряд ли оно будет удовлетворять требованиям ниже.

Очень важно, чтобы при таком преобразовании сохранялся порядок и сумма, т.е. для любых a, b: float должно выполняться:

to_float(to_int(a) + to_int(b)) == a + b

to_int(a) < to_int(b) при a < b и т.д.

Нашёл на англоязычном SO ответ, где побитово представляется структура float значений, но не представляю, как её можно применить для своей задачи.

Примечание: NaN и inf в моей задаче преобразовывать не надо.


Ответы (6 шт):

Автор решения: Zhihar

первый способ

хорошо то, что в программировании не бывает иррациональных чисел :), а значит любой float - рациональный

поэтому его можно представить в виде дроби - т.е. 2х чисел int

учитывая, что в питоне целочисленные значения не ограничены длинной, то в одном числе может быть 2 числа

например 0,5 - это [5][10],

можно реализовать такое хранение

второй способ

float очень легко переводятся в fixed - числа с фиксированной точкой - например если считать что первые n знаков - целая часть, а вторые m знаков - дробная часть, то

сложение и вычитание чисел будет ровно таким же как и у float или int

сравнение чисел будет ровно таким же как и у float или int

умножение будет чуть другим -

(a * b) >> m

перевод float в fixed:

fixed_value = 2**m * float_value

обратно соответственно

float_value = fixed_value / 2**m
→ Ссылка
Автор решения: Qwertiy

Надо просто двоичное представление float'а переинтерпретировать в целое число.

На основе https://stackoverflow.com/a/14431225/4928642

import struct

def floatBits(f):
  s = struct.pack('>f', f)
  return struct.unpack('>l', s)[0]

На основе https://stackoverflow.com/a/58362843/4928642

import ctypes

def floatBits(x):
  f = ctypes.c_float(x)
  return ctypes.c_int.from_address(ctypes.addressof(f)).value

Данное преобразование не сохраняет сумму :(

lhs = random.getrandbits(30)
rhs = random.getrandbits(30)
assert int_to_float(lhs) + int_to_float(rhs) == int_to_float(lhs + rhs)

И не должно. И вообще не получится сделать так, чтобы сохраняло.

→ Ссылка
Автор решения: Barmaley

2 метода:

Из float в long

def floatToRawLongBits(f):
    s = struct.pack('=f', f)
    return struct.unpack('=l', s)[0]

Обратно из long в float

def longToRawFloatBits(l):
    s = struct.pack('=l', l)
    return struct.unpack('=f', s)[0]

Просьба сильно не бить, ногами, пока не силен в Python

В Java есть метод Float.floatToRawIntBits(), который битовое определение IEEE 754, представляет в виде битов целого числа. По его аналогии есть решение

→ Ссылка
Автор решения: CrazyElf

А зря вы не хотите в двух целых числах хранить. Для этого в Python даже специальная библиотека есть. Которая может представить любое float число в виде точной дроби и работать с такими числами можно совершенно естественным образом:

from fractions import Fraction

print(Fraction(0.123456789))
# 8895999182988127/72057594037927936

print(float(Fraction(8895999182988127, 72057594037927936)))
# 0.123456789
→ Ссылка
Автор решения: Qwertiy
to_float(to_int(a) + to_int(b)) == a + b
to_int(a) < to_int(b) при a < b и т.д.

Я вспомнил, что в питоне int'ы бесконечные. У double есть 11 бит на степень, значит при умножении на 2**(2**12) получится целое число, которое можно положить в int. Только умножать надо по мере перевода, а не дробное число - иначе произойдёт переполнение.

→ Ссылка
Автор решения: vp_arth

Самое простое — просто умножать и делить на достаточно большой коэффициент, чтобы покрыть необходимый диапазон значений.

ratio = (1 << 156)

def to_int(f):
    """ Если точности недостаточно, может вернуть 0 или бросить OverflowError при конвертации получившейся при умножении бесконечности
    """
    return int(f * ratio)

def to_float(d):
    return d / ratio


def to_int_2(f):
    r = (f * ratio).as_integer_ratio()
    assert r[1] == 1, 'Not enough precision'
    return r[0]

Немного утверждений:

# На некоторых ratio мы таки будем получать получать ровно 0.3 при сложении, так что тут поведение нестабильно    
assert to_float(to_int(0.1) + to_int(0.2)) == 0.1 + 0.2

assert to_float(to_int(-1.03e-30) + to_int(-2.04e-31)) == (-1.234e-30)
assert to_float(to_int(-1.03e+130) + to_int(-2.04e+129)) == (-1.234e+130)


assert to_int(1e-300) == 0
try:
    print(to_int(1e+300))
except OverflowError as e:
    assert str(e) == 'cannot convert float infinity to integer'

# to_int_2
assert to_float(to_int_2(0.1) + to_int_2(0.2)) == 0.1 + 0.2
assert to_float(to_int_2(-1.03e-30) + to_int_2(-2.04e-31)) == (-1.234e-30)
assert to_float(to_int_2(-1.03e+130) + to_int_2(-2.04e+129)) == (-1.234e+130)
try:
    to_int_2(1e-300) == 0
except AssertionError as e:
    assert str(e) == 'Not enough precision'
try:
    print(to_int_2(1e+300))
except OverflowError as e:
    assert str(e) == 'cannot convert Infinity to integer ratio'

Сейчас прихожу к выводу, что as_integer_ratio не нужен, достаточно проверки res == 0 and f != 0 в to_int.

→ Ссылка