Почему math.floor(x/y) != x // y для двух делимых на 100% чисел с плавающей запятой в Python?

Я читал о делении и целочисленном делении в Python, а также о различиях между делением в Python2 и Python3. По большей части все имеет смысл. Python 2 использует целочисленное деление только тогда, когда оба значения являются целыми числами. Python 3 всегда выполняет истинное деление. Python 2.2+ представил оператор // для целочисленного деления.

Примеры, которые предложили другие программисты, работают красиво и аккуратно, например:

>>> 1.0 // 2.0      # floors result, returns float
0.0
>>> -1 // 2         # negatives are still floored
-1

Как реализован //? Почему происходит следующее:

>>> import math
>>> x = 0.5 
>>> y = 0.1
>>> x / y
5.0
>>> math.floor(x/y)
5.0
>>> x // y
4.0

Разве не должен x // y = math.floor(x/y)? Эти результаты были получены на python2.7, но, поскольку x и y оба являются числами с плавающей запятой, результаты должны быть одинаковыми на python3+. Если есть какая-то ошибка с плавающей запятой, где x/y на самом деле 4.999999999999999, а math.floor(4.999999999999999) == 4.0, разве это не будет отражено в x/y?

Однако следующие аналогичные случаи не затрагиваются:

>>> (.5*10) // (.1*10)
5.0
>>> .1 // .1
1.0

person None    schedule 20.08.2015    source источник


Ответы (4)


Я не нашел другие ответы удовлетворительными. Конечно, .1 не имеет конечного двоичного расширения, поэтому мы предполагаем, что виновата ошибка представления. Но одна только эта догадка на самом деле не объясняет, почему math.floor(.5/.1) дает 5.0, а .5 // .1 дает 4.0.

Суть в том, что a // b фактически выполняет floor((a - (a % b))/b), а не просто floor(a/b).

.5 / .1 равно точно 5,0

Прежде всего, обратите внимание, что результат .5 / .1 равен точно 5.0 в Python. Это так, хотя .1 не может быть точно представлено. Возьмем, например, этот код:

from decimal import Decimal

num = Decimal(.5)
den = Decimal(.1)
res = Decimal(.5/.1)

print('num: ', num)
print('den: ', den)
print('res: ', res)

И соответствующий вывод:

num:  0.5
den:  0.1000000000000000055511151231257827021181583404541015625
res:  5

Это показывает, что .5 может быть представлено конечным бинарным расширением, а .1 — нет. Но это также показывает, что, несмотря на это, результат .5 / .1 равен в точности 5.0. Это связано с тем, что деление с плавающей запятой приводит к потере точности, и величина, на которую den отличается от .1, теряется в процессе.

Вот почему math.floor(.5 / .1) работает, как и следовало ожидать: поскольку .5 / .1 является 5.0, запись math.floor(.5 / .1) аналогична записи math.floor(5.0).

Так почему же .5 // .1 не дает 5?

Можно предположить, что .5 // .1 — это сокращение от floor(.5 / .1), но это не так. Как оказалось, семантика отличается. И это несмотря на то, что PEP говорит:

Деление по этажам будет реализовано во всех числовых типах Python и будет иметь семантику

    a // b == floor(a/b)

Как оказалось, семантика .5 // .1 фактически эквивалентна:

floor((.5 - mod(.5, .1)) / .1)

где mod — остаток с плавающей запятой от .5 / .1, округленный до нуля. Это станет ясно из прочтения исходного кода Python.

Здесь возникает проблема из-за того, что .1 не может быть точно представлено двоичным расширением. Остаток с плавающей запятой от .5 / .1 не равен нулю:

>>> .5 % .1
0.09999999999999998

и логично, что это не так. Поскольку двоичное представление .1 немного больше фактического десятичного числа .1, наибольшее целое число alpha, такое что alpha * .1 <= .5 (в нашей математике с конечной точностью), равно alpha = 4. Таким образом, mod(.5, .1) не равно нулю и примерно равно .1. Следовательно, floor((.5 - mod(.5, .1)) / .1) становится floor((.5 - .1) / .1), становится floor(.4 / .1), что равно 4.

И именно поэтому .5 // .1 == 4.

Почему // делает это?

Поведение a // b может показаться странным, но есть причина его расхождения с math.floor(a/b). В своем блоге об истории Python, Гвидо пишет:

Операция целочисленного деления (//) и родственная ей операция по модулю (%), идут вместе и удовлетворяют хорошему математическому соотношению (все переменные являются целыми числами):

a/b = q with remainder r

такой, что

b*q + r = a and 0 <= r < b

(при условии, что a и b >= 0).

Теперь Гвидо предполагает, что все переменные являются целыми числами, но эта связь сохраняется, если a и b являются числами с плавающей запятой, if q = a // b. Если q = math.floor(a/b), отношение не будет в общем случае. И поэтому // может быть предпочтительнее, потому что он удовлетворяет этому прекрасному математическому соотношению.

person jme    schedule 20.08.2015
comment
Считаете ли вы это ошибкой интерпретатора, учитывая противоречие с PEP? - person zwol; 20.08.2015
comment
@zwol Вместо ошибки в интерпретаторе я бы сказал, что это ошибка в PEP :). Есть причины предпочесть a // b math.floor(a, b), и я не вижу, где документация подтверждает это, а утверждение PEP о семантике // кажется явно неверным. Я добавил рекламу об этом в ответ, если вам интересно. - person jme; 20.08.2015
comment
.5 // .1 не эквивалентно floor((.5 - mod(.5, .1)) / .1); когда вы читали источник, вы пропустили часть if (div - floordiv > 0.5) floordiv += 1.0; сразу после части floordiv = floor(div);. Это означает, что он округляется, а не настил. - person user2357112 supports Monica; 21.08.2015
comment
Также важно отметить, что 4.0 является правильно округленным результатом, то есть, если вы взяли действительные числа, представленные двумя входными данными с плавающей запятой .5 и .1 (а не действительные числа .5 и .1 ), применил идеальную математическую функцию f(x, y) = floor(x/y) без промежуточного округления, а затем выбрал в качестве возвращаемого значения число с плавающей запятой, наиболее близкое к результату реального числа, 4.0 - это то, что вы получите . Я не анализировал, дает ли реализация floordiv правильно округленные результаты чаще, чем применение деления с плавающей запятой и пола с плавающей запятой... - person user2357112 supports Monica; 21.08.2015
comment
последовательно, и в любом случае отношения между // и %, вероятно, важнее сохранить. - person user2357112 supports Monica; 21.08.2015
comment
@user2357112 user2357112, когда вы читали исходный код, вы пропустили ... Да, я пропустил это, потому что в конкретном случае .5 / .1 ветвь округления if не выполняется, но вы правы, что обычно результат округляется вместо этого полов. Я обновлю свой ответ, чтобы уточнить. Спасибо! - person jme; 22.08.2015

Это потому что

>>> .1
0.10000000000000001

.1 не может быть точно представлено в двоичном формате

Вы также можете видеть, что

>>> .5 / 0.10000000000000001
5.0
person njzk2    schedule 20.08.2015
comment
Я так и думал, но мой интерпретатор, кажется, скрывает это: >>> 0.1 выводит 0.1 - person None; 20.08.2015
comment
@J.Money: попробуйте (0.1).as_integer_ratio() или format(0.1, '1.30f') - person DSM; 20.08.2015
comment
Спасибо @DSM, оба они иллюстрируют это. - person None; 20.08.2015
comment
Это не должен быть принятый ответ. Я предполагаю, что технически это отвечает на вопрос ОП, но объяснения нет. Пожалуйста, обновите свой ответ ссылкой, например следующей, чтобы сделать его более полным: h-schmidt .net/FloatConverter/IEEE754.html, grouper.ieee.org/groups/754, en.wikipedia.org/wiki/IEEE_floating_point - person searchengine27; 20.08.2015
comment
@searchengine27 .1 cannot be precisely represented in binary разве это недостаточно объясняет? (кроме того, другие ответы дали разные уровни детализации, и user5248483 опубликовал ссылку на учебник python 3 по поплавкам, который отлично объясняет все тонкости) - person njzk2; 20.08.2015
comment
даже не близко. Где формулы для поплавков? объяснения того, как вещи на самом деле представлены? Сказать, что это не работает, не равносильно тому, чтобы сказать, как это работает. Ссылка на IEEE 754 может быть началом, но вы даже не попытались сделать это. - person searchengine27; 20.08.2015
comment
Сказать, что кто-то другой опубликовал правильный ответ, не делает ваш ответ правильным. Это делает их ответ правильным и делает более очевидным, что ваш ответ неполный. Послушай, ты можешь либо сидеть здесь и плакать об этом в комментариях, потому что я призвал тебя к этому, и ты явно обижаешься на это, либо взять то, что я тебе дал, и просто исправить свой ответ. РЕДАКТИРОВАТЬ: кроме того, его ответ также не является полным, потому что это не проблема, специфичная для Python. Это влияет на любой процессор base 2 (что я не знаю о процессоре, отличном от base2) - person searchengine27; 20.08.2015
comment
@searchengine27, пожалуйста, проголосуйте за ответ, который, по вашему мнению, лучше всего отвечает на вопрос (если нет, рассмотрите возможность предоставления ответа самостоятельно). принятый ответ является исключительно выбором ОП, независимо от вашего мнения по этому вопросу, так это работает (почему и для объяснения я призываю вас перейти к мете для этого) - person njzk2; 20.08.2015
comment
@ searchengine27 Я не слежу. Учебник python 3 float кратко объясняет арифметику с плавающей запятой в целом, а затем, в частности, в случае python. - person njzk2; 20.08.2015
comment
Этот ответ неверен, см. ответ с самым высоким рейтингом. - person BlueRaja - Danny Pflughoeft; 21.08.2015
comment
‐1 -- Этот ответ совершенно неполный, неудовлетворительный и, как таковой, бесполезный. - person Bakuriu; 21.08.2015

Проблема в том, что Python будет округлять вывод, как описано здесь. Поскольку 0.1 не может быть точно представлено в двоичном виде, результатом будет что-то вроде 4.999999999999999722444243844000. Естественно, это становится 5.0, когда формат не используется.

person user5248483    schedule 20.08.2015
comment
Отличный ресурс. Это был тот документ, который я искал. - person None; 20.08.2015
comment
Боюсь, это неправильно. .5 / .1 равно 5.0 точно. См.: (.5/.1).as_integer_ratio(), что дает (5,1). - person jme; 20.08.2015

Боюсь, это неправильно. .5/.1 это точно 5.0. См.: (.5/.1).as_integer_ratio(), что дает (5,1).

Да, 5 можно представить как 5/1, это правда. Но чтобы увидеть долю фактического результата, который Python дает из-за неточного представления, следуйте инструкциям.

Во-первых, импорт:

from decimal import *
from fractions import Fraction

Переменные для удобства использования:

// as_integer_ratio() returns a tuple
xa = Decimal((.5).as_integer_ratio()[0])
xb = Decimal((.5).as_integer_ratio()[1])
ya = Decimal((.1).as_integer_ratio()[0])
yb = Decimal((.1).as_integer_ratio()[1])

Выдает следующие значения:

xa = 1
xb = 2
ya = 3602879701896397
yb = 36028797018963968

Естественно, 1/2 == 5 и 3602879701896397 / 36028797018963968 == 0.1000000000000000055511151231.

Так что же происходит, когда мы разделяем?

>>> print (xa/xb)/(ya/yb)
4.999999999999999722444243845

Но когда нам нужно целочисленное отношение...

>>> print float((xa/xb)/(ya/yb)).as_integer_ratio()
(5, 1)

Как было сказано ранее, 5 это, конечно, 5/1. Вот тут и появляется Fraction:

>>> print Fraction((xa/xb)/(ya/yb))
999999999999999944488848769/200000000000000000000000000

И wolfram alpha подтверждает, что это действительно 4.999999999999999722444243845.


Почему бы вам просто не сделать Fraction(.5/.1) или Fraction(Decimal(.5)/Decimal(.1))?

Последний даст нам тот же результат 5/1. Вместо этого первый даст нам 1249999999999999930611060961/250000000000000000000000000. Это приводит к 4.999999999999999722444243844, похожему, но не такому же результату.

person user5249304    schedule 20.08.2015
comment
Здесь нужно быть осторожным: xa, xb, ya и yb являются объектами Decimal, а не Python float, поэтому показ того, что (xa/xb)/(ya/yb) является 4.999..., просто демонстрирует арифметику произвольной точности, которую обеспечивает класс Decimal. Если вы напишете Decimal(.5 / .1), вы увидите, что .5 / .1 точно равно 5.0 -- хотя .1 показывает ошибку представления, .5 / .1 нет, потому что ошибка теряется при делении. - person jme; 21.08.2015