Распространение NaN посредством вычислений

Обычно NaN (не число) распространяется через вычисления, поэтому мне не нужно проверять NaN на каждом шаге. Это работает почти всегда, но видимо есть исключения. Например:

>>> nan = float('nan')
>>> pow(nan, 0)
1.0

Я нашел следующий комментарий по этому поводу:

Распространение тихих NaN посредством арифметических операций позволяет обнаруживать ошибки в конце последовательности операций без тщательного тестирования на промежуточных этапах. Однако обратите внимание, что в зависимости от языка и функции, значения NaN могут быть удалены без уведомления в выражениях, которые дадут постоянный результат для всех других значений с плавающей запятой, например. NaN^0, которое может быть определено как 1, поэтому, как правило, требуется более поздняя проверка для установленного флага INVALID, чтобы обнаружить все случаи, когда вводятся NaN.

Чтобы удовлетворить тех, кто хочет более строгой интерпретации того, как должна действовать функция мощности, стандарт 2008 г. определяет две дополнительные функции мощности; pown(x, n), где показатель степени должен быть целым числом, и powr(x, y), который возвращает NaN всякий раз, когда параметр является NaN, иначе возведение в степень даст неопределенный вид.

Есть ли способ проверить упомянутый выше флаг INVALID через Python? В качестве альтернативы, есть ли другой подход для выявления случаев, когда NaN не распространяется?

Мотивация: я решил использовать NaN для отсутствующих данных. В моем приложении отсутствующие входные данные должны приводить к отсутствующему результату. Он отлично работает, за исключением того, что я описал.


person max    schedule 05.04.2012    source источник


Ответы (4)


Я понимаю, что прошел месяц с тех пор, как об этом спросили, но я столкнулся с похожей проблемой (т.е. pow(float('nan'), 1) выдает исключение в некоторых реализациях Python, например Jython 2.52b2), и я обнаружил, что приведенные выше ответы были не совсем тем, что я искал.

Использование типа MissingData, предложенного 6502, кажется правильным, но мне нужен был конкретный пример. Я попробовал класс NullType Итана Фурмана, но обнаружил, что он не работает с какими-либо арифметическими операциями, поскольку не приводит к принудительному принуждению типов данных (см. ниже), и мне также не понравилось, что он явно называет каждую арифметическую функцию, которая была переопределена.

Начиная с примера Итана и настройки кода, я нашел здесь , я прибыл в классе ниже. Хотя класс сильно прокомментирован, вы можете видеть, что на самом деле в нем всего несколько строк функционального кода.

Ключевые моменты: 1. Используйте coerce(), чтобы вернуть два объекта NoData для смешанных арифметических операций (например, NoData + float) и две строки для строковых операций (например, concat) операции. 2. Используйте getattr() для возврата вызываемого объекта NoData() для доступа ко всем другим атрибутам/методам 3. Используйте call() для реализации всех других методов объекта NoData(): возвращая объект NoData()

Вот несколько примеров его использования.

>>> nd = NoData()
>>> nd + 5
NoData()
>>> pow(nd, 1)
NoData()
>>> math.pow(NoData(), 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: nb_float should return float object
>>> nd > 5
NoData()
>>> if nd > 5:
...     print "Yes"
... else:
...     print "No"
... 
No
>>> "The answer is " + nd
'The answer is NoData()'
>>> "The answer is %f" % (nd)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: float argument required, not instance
>>> "The answer is %s" % (nd)
'The answer is '
>>> nd.f = 5
>>> nd.f
NoData()
>>> nd.f()
NoData()

Я заметил, что использование pow с NoData() вызывает оператор ** и, следовательно, работает с NoData, но использование math.pow не работает, поскольку сначала он пытается преобразовать объект NoData() в число с плавающей запятой. Я доволен использованием нематематического pow - надеюсь, 6502 и т. д. использовали math.pow, когда у них были проблемы с pow в их комментариях выше.

Другая проблема, которую я не могу придумать, - это использование с оператором формата (%f)... В этом случае не вызываются никакие методы NoData, оператор просто терпит неудачу, если вы не предоставляете число с плавающей запятой. Во всяком случае, вот сам класс.

class NoData():
"""NoData object - any interaction returns NoData()"""
def __str__(self):
    #I want '' returned as it represents no data in my output (e.g. csv) files
    return ''        

def __unicode__(self):
    return ''

def __repr__(self):
    return 'NoData()'

def __coerce__(self, other_object):
    if isinstance(other_object, str) or isinstance(other_object, unicode):
        #Return string objects when coerced with another string object.
        #This ensures that e.g. concatenation operations produce strings.
        return repr(self), other_object  
    else:
        #Otherwise return two NoData objects - these will then be passed to the appropriate
        #operator method for NoData, which should then return a NoData object
        return self, self

def __nonzero__(self):
    #__nonzero__ is the operation that is called whenever, e.g. "if NoData:" occurs
    #i.e. as all operations involving NoData return NoData, whenever a 
    #NoData object propagates to a test in branch statement.       
    return False        

def __hash__(self):
    #prevent NoData() from being used as a key for a dict or used in a set
    raise TypeError("Unhashable type: " + self.repr())

def __setattr__(self, name, value):
    #This is overridden to prevent any attributes from being created on NoData when e.g. "NoData().f = x" is called
    return None       

def __call__(self, *args, **kwargs):
    #if a NoData object is called (i.e. used as a method), return a NoData object
    return self    

def __getattr__(self,name):
    #For all other attribute accesses or method accesses, return a NoData object.
    #Remember that the NoData object can be called (__call__), so if a method is called, 
    #a NoData object is first returned and then called.  This works for operators,
    #so e.g. NoData() + 5 will:
    # - call NoData().__coerce__, which returns a (NoData, NoData) tuple
    # - call __getattr__, which returns a NoData object
    # - call the returned NoData object with args (self, NoData)
    # - this call (i.e. __call__) returns a NoData object   

    #For attribute accesses NoData will be returned, and that's it.

    #print name #(uncomment this line for debugging purposes i.e. to see that attribute was accessed/method was called)
    return self
person jcdude    schedule 03.05.2012
comment
Я имел в виду Jython 2.5.2b2, а не 2.52b2 - person jcdude; 03.05.2012

Зачем использовать NaN, у которого уже есть другая семантика, вместо использования экземпляра класса MissingData, определенного вами?

Определение операций над экземплярами MissingData для распространения должно быть простым...

person 6502    schedule 05.04.2012
comment
Не могу поверить, что я не подумал об этом. Теперь с ABC будет даже не так сложно определить все арифметические операции, верно? - person max; 05.04.2012
comment
Или, как я предложил в своем только что отредактированном ответе, даже не выполняйте никаких операций над классом MissingData. Просто позвольте Python вызвать любое исключение, когда вы попытаетесь использовать один из этих объектов в расчете, поймать его и указать значение по умолчанию. - person kindall; 05.04.2012
comment
На самом деле мне нужны операции над MissingValue, потому что при каждом промежуточном вычислении нужно было бы перехватывать исключение, а это слишком много работы. Гораздо лучше просто позволить MissingValue распространяться, а затем сделать так, чтобы MissingValue заполнил результирующий набор данных. - person max; 05.04.2012
comment
Да, я предполагал, что вычисления происходят в блоке или могут быть легко организованы для этого. - person kindall; 06.04.2012
comment
К сожалению, похоже, что функция pow() на самом деле не вызывает специальный метод __pow__() класса (только x ** y вызовет x.__pow__()). Так что вы, вероятно, все еще будете переписывать это, и abs(), и большое количество других встроенных числовых функций. - person kindall; 06.04.2012

Если это просто pow() вызывает у вас головную боль, вы можете легко переопределить его, чтобы возвращать NaN при любых обстоятельствах.

def pow(x, y):
    return x ** y if x == x else float("NaN")

Если NaN можно использовать в качестве показателя степени, вы также захотите проверить это; это вызывает исключение ValueError, за исключением случаев, когда основание равно 1 (очевидно, согласно теории, что 1 в любой степени, даже если это не число, равно 1).

(И, конечно, pow() на самом деле принимает три операнда, третий необязательный, это упущение я оставлю в качестве упражнения...)

К сожалению, оператор ** ведет себя так же, и его невозможно переопределить для встроенных числовых типов. Возможность поймать это состоит в том, чтобы написать подкласс float, который реализует __pow__() и __rpow__(), и использовать этот класс для ваших значений NaN.

Python, похоже, не предоставляет доступ к каким-либо флагам, установленным расчетами; даже если бы это было так, вам придется проверять это после каждой отдельной операции.

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

person kindall    schedule 05.04.2012
comment
Я не понимаю, как это работает. NaN != NaN так что ваше if всегда будет верным. - person Duncan; 05.04.2012
comment
Просто замените x != NaN на x == x. - person max; 05.04.2012
comment
И я не уверен; может быть, pow единственный, может быть, нет... Я думаю, что использовать NaN для отсутствующих данных, как бы аккуратно это ни звучало, не очень практично... :( - person max; 05.04.2012
comment
Хороший звонок, забыл о таком поведении NaN. - person kindall; 05.04.2012
comment
Это не работает - вероятно, потому что x!=NaN всегда будет оцениваться как True. (nan != nan по стандарту IEEE). nan действительно распространяется до тех пор, пока показатель степени не равен 0... по-видимому, библиотека использует подход, согласно которому x**0=1 независимо от того, что такое x... Способ, которым я обычно проверяю nan, использует numpy.isnan(x ). - person mgilson; 05.04.2012
comment
Не забывайте, что pow принимает необязательный третий аргумент! - person Gareth Rees; 05.04.2012

Чтобы ответить на ваш вопрос: нет, нет возможности проверить флаги с помощью обычных поплавков. Однако вы можете использовать класс Decimal, который обеспечивает гораздо больший контроль . . . но немного медленнее.

Другой вариант — использовать класс EmptyData или Null, например этот:

class NullType(object):
    "Null object -- any interaction returns Null"
    def _null(self, *args, **kwargs):
        return self
    __eq__ = __ne__ = __ge__ = __gt__ = __le__ = __lt__ = _null
    __add__ = __iadd__ = __radd__ = _null
    __sub__ = __isub__ = __rsub__ = _null
    __mul__ = __imul__ = __rmul__ = _null
    __div__ = __idiv__ = __rdiv__ = _null
    __mod__ = __imod__ = __rmod__ = _null
    __pow__ = __ipow__ = __rpow__ = _null
    __and__ = __iand__ = __rand__ = _null
    __xor__ = __ixor__ = __rxor__ = _null
    __or__ = __ior__ = __ror__ = _null
    __divmod__ = __rdivmod__ = _null
    __truediv__ = __itruediv__ = __rtruediv__ = _null
    __floordiv__ = __ifloordiv__ = __rfloordiv__ = _null
    __lshift__ = __ilshift__ = __rlshift__ = _null
    __rshift__ = __irshift__ = __rrshift__ = _null
    __neg__ = __pos__ = __abs__ = __invert__ = _null
    __call__ = __getattr__ = _null

    def __divmod__(self, other):
        return self, self
    __rdivmod__ = __divmod__

    if sys.version_info[:2] >= (2, 6):
        __hash__ = None
    else:
        def __hash__(yo):
            raise TypeError("unhashable type: 'Null'")

    def __new__(cls):
        return cls.null
    def __nonzero__(yo):
        return False
    def __repr__(yo):
        return '<null>'
    def __setattr__(yo, name, value):
        return None
    def __setitem___(yo, index, value):
        return None
    def __str__(yo):
        return ''
NullType.null = object.__new__(NullType)
Null = NullType()

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

person Ethan Furman    schedule 05.04.2012