NaN все еще может немного удивить

    image

    Сначала, я подумал, что это очередной вопрос из тех, которые могут задаваться на собеседовании. Наверное, если как следует пораскинуть мозгами, то можно догадаться до того, каким будет результат. Откинувшись на спинку кресла, начал размышлять, включать логику, вспоминать что-нибудь, на что можно опереться в рассуждениях. Но тщетно! Вдруг стало совершенно очевидно, что найти ответ не удается. Но почему? В чем нужно разбираться, чтобы он был найден? В математике? В языке программирования?

    Ответ должен быть NaN. Но почему я не уверен в этом? Всю дорогу была уверенность в том, что любые выражения, содержащие NaN, вернут NaN. Ну разве что только если поделить NaN на ноль — в этом случае будет вызвано исключение ZeroDivisionError. Сто процентов NaN!

    Ввожу выражение в ячейку блокнота:

    >>> 1**nan + 1**nan
    2.0

    В самом деле? Постойте:

    >>> arange(5)**nan
    array([nan,  1., nan, nan, nan])

    То есть, по какой-то причине, единица в степени NaN — это единица, а вот ноль и все остальные числа в степени NaN — это NaN. Где логика? В чем дело?

    Так, давайте еще раз:

    >>> 0**nan, 1**nan
    (nan, 1.0)

    Может быть я просто из-за отсутствия какой-то практической надобности в глубоких познаниях о NaN, просто о чем-то не подозревал? А может я знал, но забыл? А может еще хуже — я не знал и забыл?

    Заходим на Википедию. Там данный вопрос тоже обозначен как проблема, но почему все именно так устроено, никак не объясняется. Зато узнал что:

    >>> hypot(inf, nan)
    inf

    Хотя, в то же время:

    >>> sqrt(inf**2 + nan**2)
    nan

    Что, согласитесь, тоже немного странно.

    Ладно, с Википедии отправляемся в C99 на 182 страницу и наконец-то получаем логическое объяснение, почему pow(x, 0) возвращает 1 для любых x, даже для x равного NaN:

    >>> power(nan, 0)
    1.0

    Если функция $f(x)$ возводится в степень $g(x)$ и при этом $g(x)$ стремится к 0, то в результате получится 1, вне зависимости от того, какое значение имеет $f(x)$.

    image

    А если результат не зависит от числового значения функции $f(x)$, то 1 — является подходящим результатом, даже для NaN. Однако это по-прежнему не объясняет, почему 1 в степени NaN равна 1.

    Отыскиваем еще один C99 и на 461 странице не видим никаких объяснений, просто требование того, что pow(+1, y) должно возвращать 1 для всех y, даже равных NaN. Все.

    С другой стороны, объяснение, почему pow(NaN, 0)=1 является более предпочтительным, чем pow(NaN, 0)=NaN все-таки наталкивает на мысль о том, что NaN не стоит воспринимать буквально, как Not-a-Number. Допустим, в результате каких- то вычислений мы получили число, превышающее размер памяти, выделенный под данный тип чисел, например:

    >>> a = pi*10e307
    >>> a
    inf

    В результате мы получили inf, что именно это за число мы не знаем, но все же это какое-то число. Затем мы снова что-то вычислили и снова получили слишком большое число:
    >>> b = e*10e307
    >>> b
    inf

    Разность a и b вернет NaN:

    >>> c = a - b
    >>> c
    nan

    Единственная причина, по которой мы можем считать c не числом, заключается в том, что мы использовали недостаточно точные вычисления. Однако, в c под NaN все же скрывается какое-то значение. О том, что это за значение, мы не знаем. Но все же это число, а раз это число, то нет ничего удивительного в том, что pow(1, NaN)=1.

    Почему же тогда pow(0, NaN)=NaN? Дело в том, что если возвести 0 в любую степень, то мы действительно получим ноль. Кроме одного единственного случая — когда степень равна 0:

    >>> 0**0
    1

    Из-за чего в выражении pow(0, NaN) появляется неопределенность с конкретным значением NaN. Конечно, вероятность того, что под NaN может скрываться 0 — исчезающе мала и можно было бы принять, что pow(0, NaN)=0. Но все же лучше перестраховаться, мало ли к чему это может привести. Возможно, так и рассуждали, когда создавались стандарты.

    Даже не знаю, что еще сказать… если вы заранее знали ответ, то скорее всего вам можно позавидовать, ведь сферы, где могут пригодиться такие познания, наверняка, переполнены интересными задачами. А может и наоборот. Напишите об этом в комментариях.

    P.S. Поскольку NaN относится к числам с плавающей точкой, оно может быть ключом словаря:

    >>> d = {0.1: 'a', nan: 'b'}
    >>> d[nan]
    'b'

    Имеет ли смысл использовать такое на практике? Думаю, что лучше не стоит.

    Комментарии 29

      +4
      Все эти странности идут из стандарта IEE 754. Собрались люди и сделали стандарт так, что какую бы чушь ты не считал, чтобы получил адекватный результат. (Чушь — адекватный результат, хм, ладно....)
      Глядя на это все иногда возникает чувство, что тут немного перемудрили…
        0

        Там, насколько я помню, не комитет по стандартизации придумывал, а Intel продавила свою реализацию.

        +2
        Заведомо там вначале условие, что если 1, то и возвращается 1 неглядя на остальные аргументы.
          0
          Может не надо было называть число «not a number», если это все-таки число, хоть и не определенное.
            0

            Но это в самом деле не число. Оно не обладает свойствами действительных чисел.

            0

            Вообще-то 0**0 неопределено и может быть честным NaN
            https://en.wikipedia.org/wiki/Zero_to_the_power_of_zero#Treatment_on_computers

              0
              Да. Но как я понимаю, здесь ставится вопрос «Что предпочтительнее в данных обстоятельствах?»
                0

                Это зависит от задачи. Скажем, при определении кривой Безье соглашение о том, что 0**0=1 упрощает формулу.


                Где-то тут на Хабре была статья о том, что в некоторых языках программирования определено три разных функции возведения в степень, отличающиеся только поведением в точке 0**0.

                +3
                Спасибо, очень понравилось ваше мнемоническое правило, что NaN — это на самом деле число, просто неизвестное. Могу добавить, что, поскольку log(-1) = NaN, это число ещё и необязательно вещественное (в данном случае комплексное).
                  –1

                  Еще один прикол NaN — он не равен ничему, даже самому себе


                  >>> nan != nan
                  True
                    0
                    Ну это-то логично. Если под NaN может быть любое число, то вероятность совпадения исчезающе мала. А вот если-бы вы результат функции конторая выдала вам nan сравнивали сам с собой-тогда уже другой вопрос.
                      +1
                      Только вот это не всегда так. Например, GLSL не на любых GPU вернёт true при проверке NaN != NaN. Только isnan() вернёт правильный результат.
                        +1

                        В большинстве реализаций это так, но существуют исключения, которые отходят отходят от стандарта. В PostgreSQL NaN == NaN, и NaN больше чем любое число (больше, чем Infinity)

                        0

                        Это было введено в стандарт для того, чтобы не пришлось добавлять в языки операцию проверки на NaN.
                        В ранних языках стандартным способом проверки x на NaN было условие x != x.

                          0

                          Я нашел ответ на SO от бывшего члена комитета IEEE754.


                          More importantly, there was no isnan( ) predicate at the time that NaN was formalized in the 8087 arithmetic; it was necessary to provide programmers with a convenient and efficient means of detecting NaN values that didn’t depend on programming languages providing something like isnan( ) which could take many years.
                          https://stackoverflow.com/questions/1565164/what-is-the-rationale-for-all-comparisons-returning-false-for-ieee754-nan-values/1573715#1573715
                          +2
                          Если функция возводится в степень и при этом стремится к 0, то в результате получится 1, вне зависимости от того, какое значение имеет функция.

                          Кто-то не знаком с определением числа e, например. lim_{x -> inf} (1 + 1/x) ^ (1/x) = e != 1.

                            +2
                            Поправочка. В написанном вами пределе всё-таки получится 1^0 = 1. Чтобы e получить, надо считать 1^∞, т.е. (1 + 1/x)^x при x→∞. Но если степень стремится к нулю, то не единица может получаться в особенностях вида 0^0 и ∞^0. Например, x^{1 / ln x} → e при x→0.
                              +1

                              Да, ошибся. Забыл, как выглядит замечательный предел.

                            +1

                            Я подумал и угадал верный результат. При этом я даже едва знаком с питоном. По-моему, всё предельно логично.

                              +1
                              Я подумал и угадал верный результат.

                              С учетом всех ваших статей на Хабре — вам можно верить на слово.
                              По-моему, всё предельно логично.

                              Верно. Возможно раньше я бы тоже ответил правильно, но уже пол года активно использую Pandas, а там NaN начинает восприниматься буквально, как пропущенные (или отсутствующие) данные. Перестаешь воспринимать NaN как число.
                                +1

                                Интересно, а это поведение тоже выглядит предельно логично?


                                >>> 0*nan
                                nan
                                  0

                                  Да, потому что за NaN в предложенной логике может скрываться Inf.

                                    0
                                    Ну кстати, если так рассуждать, ломается логика случая, с которого мы начали. Единица в степени бесконечность — это неопределённость.
                                    0
                                    Да. Ноль в положительной степени — ноль, в отрицательной — бесконечность.
                                    +1

                                    Я угадал верный результат только потому что раз есть вопрос (и целая статья на эту тему), значит не очень очевидное поведение. Логическо только 2 подошла. Но, стоит признаться, я думал что приоритет у вычислительного рака NaN выше и все функции, которые на него натыкаются, так же должны возвращать NaN без исключений.


                                    Более того, в некоторых языках (не будем тыкать в js пальцем) за NaN может скрываться не какое-то число, которое не влезло в дабл или что-то в этом духе, а строка или вообще объект. В нем выражение Math.pow(1, "give me a power!") или даже с смайликом в качестве степени — возможно. Единица в степени эмоджи, равная единице, выглядит странно.


                                    Справедливости ради, в JS все наоборот, как обычно:


                                    1 ** NaN // NaN
                                    
                                    "Give me a power" ** 0 // 1
                                    new Object() ** 0 // 1
                                    0

                                    Я как-то писал очень похожую статью:
                                    https://m.habr.com/ru/post/454352/

                                      0
                                      А я ее как-то читал :)

                                      Прежде чем писать статью, вбил «NaN» в поиск Хабра, был уверен, что про это уже кто-нибудь писал. Надо сказать, в результате поиска очень много классных статей.
                                      +1
                                      Имеет ли смысл использовать такое на практике? Думаю, что лучше не стоит.

                                      Правильно думаете. NaN — это семейство значений с плавающей точкой, у которых экспонента состоит полностью из 1, а мантисса не состоит полностью из нулей. Поэтому nan как литерал не обязан быть по хэшу равен nan, полученному в ходе вычислений.


                                      По теме статьи — вопрос дискуссионный, что считать наиболее правильным подходом. Как по мне, надо бы различать два случая:


                                      • 1.0**nan должно быть nan, т.к. 1.0 обозначает лишь некоторое число между 1-eps/2 и 1+eps. В частности, (1 + 10**(-10))**nannan, но (float32(1 + 10**(-10)))**nan == 1 выглядит несколько абсурдно
                                      • 1**nan (1 — целое число) вполне можно взять равным 1.0, т.к. целочисленный литерал тут обозначает точную единицу без какой-либо погрешности
                                        0
                                        Во многих процессорах операция с nan может приводить к программному исключению. Правда эта функция может быть и выключена. Так что программное исключение тоже может быть результатом вычисления приведенного примера:)

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое