9

Есть задача преобразования float значений в значения типа int (одно float значение в одно 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 в моей задаче преобразовывать не надо.


Попытки решения

Изначально на ум пришла сериализация с помощью pickle.dumps, а затем получение int из этих байт, но это совсем не рациональное решение и оно совершенно не удовлетворяет требованиям.

Были попытки реализовать это через байтовые представления чисел (ответ Qwertiy♦), в таком варианте не сохраняется сумма.

  • Можно использовать Fraction: fractions.Fraction(0.1+0.2) => Fraction(1351079888211149, 4503599627370496) ` – vp_arth Oct 26 '20 at 15:38
  • 1
    @vp_arth Да я ему предлагал. Говорит два инта не хочу, хочу один! )) – CrazyElf Oct 26 '20 at 15:38
  • @vp_arth Ок. Можно. А как потом эти числитель и знаменатель хранить в одном int значении? И как их потом друг от друга отделять после сложения, например? – Mikhail Murugov Oct 26 '20 at 15:43
  • Тогда так: ((0.1+0.2)*1e32).as_integer_ratio()[0] / 1e32 == (0.1+0.2) 32 взял «от балды», не в курсе, сколько там точности) – vp_arth Oct 26 '20 at 15:43
  • @vp_arth В питоне числа с плавающей точкой являются double (64 бит), вроде как. Идею Вашу понял, сейчас попробую потестировать. – Mikhail Murugov Oct 26 '20 at 15:51
  • @vp_arth Сумма сохраняется с точностью до sys.float_info.epsilon (не точно, но уже хоть что-то), но порядок не сохраняется :/ Например, для чисел 1.043380078e-314, 2.0142355855e-314 (в преобразовании e32 заменил на e64). – Mikhail Murugov Oct 26 '20 at 16:21
  • Лучше на (1<<64). Ну да, если вам весь диапазон нужен, такой фокус не пройдёт, знаменатель слишком большой) – vp_arth Oct 26 '20 at 16:45
  • Если выполнять нужное условие по сравнению, целые числа должны содержать более 600 разрядов) Думаю, тут нужно не выдумывать велосипед, а настраивать Decimal: float(Decimal.from_float(1.23e-312)). Хотя, вам нужен именно int... – vp_arth Oct 26 '20 at 17:00
  • @vp_arth >если вам весь диапазон нужен, такой фокус не пройдёт, знаменатель слишком большой) - А для какого диапазона применимо Ваше решение? – Mikhail Murugov Oct 27 '20 at 03:44
  • Ну, это же вы сами можете выбрать. Например, для сдвига в (1<<128) вы получите диапазон около 1e-20..1e+260. Больше сдвиг - доступны более мелкие, недоступны(обращаются в бесконечность) более крупные. – vp_arth Oct 27 '20 at 04:55
  • @vp_arth Оформите ответом? С подробным описанием, как и почему это работает – Mikhail Murugov Oct 27 '20 at 13:50
  • Оформил. Но на самом деле, 2й ответ @Qwertiy — фактически то же самое, что бы он ни имел в виду под «постепенным умножением» =) – vp_arth Oct 27 '20 at 14:30

6 Answers6

2
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. Только умножать надо по мере перевода, а не дробное число - иначе произойдёт переполнение.

Qwertiy
  • 123,725
  • Что значит "по мере перевода"? Не могли бы Вы, пожалуйста, приложить пример? – Mikhail Murugov Oct 20 '20 at 05:01
  • @МихаилМуругов, должно быть какое-то сходство с этим: https://ru.stackoverflow.com/a/734615/178988. – Qwertiy Oct 20 '20 at 08:30
1

Надо просто двоичное представление 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)

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

Qwertiy
  • 123,725
  • Спасибо! Именно это-то подобное я и считал рациональным решением, но не знал, как в питоне из float получить его битовое представление. Осталось лишь удостовериться, что сохраняются операции. – Mikhail Murugov Oct 19 '20 at 12:19
  • Данное преобразование не сохраняет сумму :( lhs = random.getrandbits(30); rhs = random.getrandbits(30); assert int_to_float(lhs) + int_to_float(rhs) == int_to_float(lhs + rhs). – Mikhail Murugov Oct 19 '20 at 13:54
  • @МихаилМуругов, а с какой стати оно должно её сохранять? – Qwertiy Oct 19 '20 at 16:08
1

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

from fractions import Fraction

print(Fraction(0.123456789))

8895999182988127/72057594037927936

print(float(Fraction(8895999182988127, 72057594037927936)))

0.123456789

CrazyElf
  • 71,194
  • 1
    Тут не "я хочу", а надо :) Вместо библиотеки тогда уж можно использовать просто float.as_integer_ratio. – Mikhail Murugov Oct 19 '20 at 13:48
1

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

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.

vp_arth
  • 27,179
  • А как это работает? Почему при умножении на степень двойки знаменатель становится 1? – Mikhail Murugov Oct 27 '20 at 15:15
  • Потому что все числа с плавающей запятой рациональны. Состав коэффициента(степень двойки, десятки да хоть тройки) не принципиален. Главное — быть достаточно большим, чтобы исчерпать(перенести в целую часть) все значащие цифры. – vp_arth Oct 27 '20 at 15:23
0

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

хорошо то, что в программировании не бывает иррациональных чисел :), а значит любой 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
Zhihar
  • 37,513
  • Первый способ очень близок к правде, но хранить надо одним целым числом. Если в одном числе "хранить" 2 числа для дроби, то как их потом друг от друга отделять? И не будет ли потом проблемой их друг от друга отделить после сложения? По второму способу не понимаю, что значит первые n знаков. Первые n знаков где? – Mikhail Murugov Oct 19 '20 at 11:54
  • Что с NaN и бесконечностями? – Qwertiy Oct 19 '20 at 12:01
  • @Qwertiy NaN и бесконечностей в моём кейсе нет. – Mikhail Murugov Oct 19 '20 at 12:05
0

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, представляет в виде битов целого числа. По его аналогии есть решение

Barmaley
  • 81,300