Как Python сравнивает объекты «int» с «плавающими» объектами?

В документации о числовых типах указано следующее:

Python полностью поддерживает смешанную арифметику: когда двоичный арифметический оператор имеет операнды разных числовых типов, операнд с «более узким» типом расширяется до другого, где целое уже, чем с плавающей запятой, которое уже, чем сложное. При сравнении чисел смешанного типа используется одно и то же правило.

Это подтверждается следующим поведением:

>>> int.__eq__(1, 1.0)
NotImplemented
>>> float.__eq__(1.0, 1)
True

Однако для больших целых чисел, похоже, происходит что-то еще, поскольку они не будут сравниваться равными, если они не будут явно преобразованы в float:

>>> n = 3**64
>>> float(n) == n
False
>>> float(n) == float(n)
True

С другой стороны, для степеней двойки это не кажется проблемой:

>>> n = 2**512
>>> float(n) == n
True

Поскольку в документации подразумевается, что int "расширяется" (я полагаю, преобразовано/приведено?) до float, я ожидаю, что float(n) == n и float(n) == float(n) будут похожи, но приведенный выше пример с n = 3**64 предполагает другое. Итак, какие правила использует Python для сравнения int с float (или вообще смешанных числовых типов)?


Протестировано с CPython 3.7.3 от Anaconda и PyPy 7.3.0 (Python 3.6.9).


person a_guest    schedule 31.01.2020    source источник
comment
Вероятно, вы просто достигли предела точности с плавающей запятой, а float(3**64) не совсем равно int(3**64).   -  person deceze♦    schedule 31.01.2020
comment
float(2**512), с другой стороны, может быть представлено точно, несмотря на то, что он намного больше, потому что это степень числа 2. Мантиссе требуется только 1 бит для полной точности, и экспоненте нужно только 9.   -  person chepner    schedule 31.01.2020
comment
Я понимаю все это поведение, но в документации указано, что более узкий тип, в данном случае int, будет расширен до другого, в данном случае float. Таким образом, не должно быть проблем с потерей точности из-за кругового отключения; Я ожидаю, что float(n) == n будет внутренне обработан так, что r.h.s. преобразуется в float, т. е. становится похожим на float(n) == float(n).   -  person a_guest    schedule 31.01.2020
comment
Если я правильно помню, Python или, по крайней мере, некоторые его реализации прилагают все усилия, чтобы точно сравнивать значения. Когда 3**64 сравнивается с float(n), оно не преобразуется в float. Скорее точное математическое значение 3**64 сравнивается с точным математическим значением float(n). Поскольку они различаются, результат равен False. Когда вы конвертируете 3**64 в float, оно преобразуется в значение, представляемое в типе float, что приводит к некоторой ошибке. В документации неудачно сказано, что float «шире», чем int. Как правило, он не может представлять все значения int  -  person Eric Postpischil    schedule 31.01.2020
comment
github.com/python/cpython/blob/master/Objects/, по-видимому, отвечает за сравнение чисел с плавающей запятой и целого числа. Он, безусловно, делает больше, чем просто преобразует int в число с плавающей запятой и сравнивает его, но мои способности чтения C недостаточно сильны, чтобы понять, что именно он делает.   -  person Kevin    schedule 31.01.2020
comment
… и поэтому неясно, соответствует ли реализация, которая, по сути, преобразуется в действительно более широкий формат, который может представлять все значения int и все значения float, этой документации.   -  person Eric Postpischil    schedule 31.01.2020
comment
Я понимаю все это поведение, но в документации указано, что более узкий тип, в данном случае int, будет расширен до другого, в данном случае float. Python имеет целые числа с бесконечной точностью, поэтому int абсолютно не уже, чем float.   -  person Masklinn    schedule 31.01.2020
comment
@Masklinn Согласно приведенному фрагменту документации, это…   -  person deceze♦    schedule 31.01.2020
comment
@Masklinn В документации также указано, что ... где целое число уже, чем с плавающей запятой, что уже, чем сложное.. Можно не согласиться с тем, что это имеет место на более общем уровне, но в рамках их определения широкого и узкого это является (или, по крайней мере, должно быть) последовательным.   -  person a_guest    schedule 31.01.2020
comment
@Kevin Если мой анализ верен, он должен следовать этой ветке но дальше не совсем понятно, что происходит. Мне нужно больше времени, чтобы проанализировать это более подробно.   -  person a_guest    schedule 31.01.2020
comment
@Masklinn Согласно цитируемому фрагменту документации, это ... Я думаю, формулировка вводит в заблуждение, хотя это приводит к забавному результату: factorial(512) + 1.0 жалуется, что int слишком велик.   -  person Masklinn    schedule 31.01.2020
comment
@EricPostpischil Когда вы говорите точное математическое значение, вы имеете в виду, что оно сравнивает соответствующее значение с основанием 10 и пытается извлечь его из объекта float?   -  person a_guest    schedule 31.01.2020
comment
@Masklinn Поскольку для Python 3 единственным ограничением для int является память вашего компьютера, а float привязан к 64 битам, я думаю, что значение терминов «широкий» и «узкий» не следует воспринимать слишком буквально, а скорее следует рассматривать в рамках собственное определение документации для них, которое является целым числом, уже, чем с плавающей запятой, которое уже, чем сложное. Таким образом, после этого определения int действительно должно быть преобразовано в float или, по крайней мере, результаты должны быть такими же, как если бы это было так, но фактическое поведение кажется другим.   -  person a_guest    schedule 31.01.2020
comment
@a_guest: числа с основанием 10 не имеют значения. Тот факт, что двенадцать — это «12» в десятичной системе счисления и «14» в восьмеричной — это всего лишь вопрос представления; десятичное число 12 равно восьмеричному 14. Результат 3**64 равен 3433683820292512484657849089281. Результат float(3**64) равен 3433683820292512441173561835520. Они не равны, поэтому 3**64 == float(3**64) ложно.   -  person Eric Postpischil    schedule 31.01.2020
comment
@EricPostpischil То есть вы имеете в виду, что в этих случаях поведение похоже на то, как если бы float было преобразовано в int (а не наоборот)? Глядя на исходный код, преобразование float кажется произойдет, если int использует меньше или равно 48 битам. Действительно, проверка n = 3**33 и n = 3**34, похоже, связана с круговым обходом int(float(n)) == n.   -  person a_guest    schedule 31.01.2020
comment
@a_guest: преобразование целого числа в число с плавающей запятой может привести к потере информации, поскольку в типе с плавающей запятой недостаточно битов для представления целого числа, как вы видели с float(3**64). Преобразование из числа с плавающей запятой в целое число может привести к потере информации, поскольку целое число не представляет собой дробные части. Насколько мне известно, код сравнения, на который ссылается другой комментарий выше, эффективно сравнивает точные математические значения. Для этого не нужно делать полное преобразование. Если n — целое число, а ffloat, не являющееся NaN, то каждое из них представляет определенное число,…   -  person Eric Postpischil    schedule 31.01.2020
comment
… и n == f оценивается как True тогда и только тогда, когда они представляют одно и то же число.   -  person Eric Postpischil    schedule 31.01.2020
comment
@EricPostpischil Итак, если я правильно понимаю, точное математическое значение 3**34 (то есть 16677181699666569) равно просто 16677181699666569.0, хотя это не может быть точно представлено в float, поскольку float(3**34) равно 16677181699666568.0, что также является его точным математическим значением. Преобразование int(float(3**34)), следовательно, дает 16677181699666568, то есть 3**34 - 1. Однако, выполняя побитовый анализ задействованных чисел, можно получить доступ к этим точным значениям без необходимости преобразования в какой-либо тип данных. Это то, что происходит?   -  person a_guest    schedule 31.01.2020
comment
@a_guest: Да. Анализ может быть побитовым или смешанным. Например, проверьте, имеет ли число с плавающей запятой дробь, проверив его биты. Если да, то оно не равно целому числу. Проверьте, не выходит ли оно за целочисленные границы. Если да, то оно не равно целому числу. В противном случае точно преобразуйте его в целое число, а затем сравните.   -  person Eric Postpischil    schedule 31.01.2020
comment
@a_guest: Точное математическое значение 3**34 равно 16677181699666569. 16677181699666569 и 16677181699666569.0 — это числа, которые также представляют это число. (Числа — это строки символов, представляющие числа. На самом деле они не являются числами, так же как слово «лиса» не является лисой.)   -  person Eric Postpischil    schedule 31.01.2020
comment
немного связано для тех, кто интересуется как? больше, чем что? stackoverflow.com/questions/58734034/   -  person aka.nice    schedule 31.01.2020


Ответы (1)


спецификация языка сравнения значений содержит следующий абзац:

Числа встроенных числовых типов (Числовые типы — int, float, complex) и стандартных библиотечных типов fractions.Fraction и decimal.Decimal можно сравнивать внутри и между их типами с тем ограничением, что комплексные числа не поддерживают сравнение по порядку. В пределах задействованных типов они сравниваются математически (алгоритмически) корректно без потери точности.

Это означает, что при сравнении двух числовых типов сравниваются фактические (математические) числа, представленные этими объектами. Например, число 16677181699666569.0 (которое равно 3**34) представляет число 16677181699666569, и хотя в формате "float -пробел" нет никакой разницы между этим числом и 16677181699666568.0 (3**34 - 1) они представляют разные числа. Из-за ограниченной точности с плавающей запятой в 64-битной архитектуре значение float(3**34) будет храниться как 16677181699666568 и, следовательно, представляет собой число, отличное от целочисленного числа 16677181699666569. По этой причине у нас есть float(3**34) != 3**34, который выполняет сравнение без потери точности.

Это свойство важно для обеспечения транзитивности отношение эквивалентности числовых типов. Если бы сравнение int и float дало такие же результаты, как если бы объект int был преобразован в объект float, то транзитивное отношение было бы недействительным:

>>> class Float(float):
...     def __eq__(self, other):
...         return super().__eq__(float(other))
... 
>>> a = 3**34 - 1
>>> b = Float(3**34)
>>> c = 3**34
>>> a == b
True
>>> b == c
True
>>> a == c  # transitivity demands that this holds true
False

С другой стороны, реализация float.__eq__, учитывающая представленные математические числа, не нарушает этого требования:

>>> a = 3**34 - 1
>>> b = float(3**34)
>>> c = 3**34
>>> a == b
True
>>> b == c
False
>>> a == c
False

В результате отсутствия транзитивности порядок следующего списка не будет изменен при сортировке (поскольку все последовательные числа кажутся равными):

>>> class Float(float):
...     def __lt__(self, other):
...         return super().__lt__(float(other))
...     def __eq__(self, other):
...         return super().__eq__(float(other))
... 
>>> numbers = [3**34, Float(3**34), 3**34 - 1]
>>> sorted(numbers) == numbers
True

С другой стороны, при использовании float порядок меняется на обратный:

>>> numbers = [3**34, float(3**34), 3**34 - 1]
>>> sorted(numbers) == numbers[::-1]
True
person a_guest    schedule 02.02.2020