Ошибка Python 3 datetime.fromtimestamp на 1 микросекунду

#python #datetime #python-3.x #unix-timestamp #python-3.4

#python #datetime #python-3.x #unix-временная метка #python-3.4

Вопрос:

Я хочу сохранить даты с разрешением в микросекунду в виде временных меток. Но, похоже, что модуль Python 3 datetime потерял одну микросекунду при их загрузке. Чтобы проверить это, давайте создадим скрипт:

test_datetime.py:

 from random import randint
from datetime import datetime

now = datetime.now()

for n in range(1000):
    d = datetime(year=now.year, month=now.month, day=now.day,
            hour=now.hour, minute=now.minute, second=now.second,
            microsecond=randint(0,999999))

    ts = d.timestamp()
    d2 = datetime.fromtimestamp(ts)

    assert d == d2, 'failed in pass {}: {} != {}'.format(n, d, d2)
  

python3 test_datetime.py всегда сбой на одну микросекунду:

 Traceback (most recent call last):
  File "test_datetime.py", line 14, in <module>
    assert d == d2, 'failed in pass {}: {} != {}'.format(n, d, d2)
AssertionError: failed in pass 4: 2014-07-02 11:51:46.984716 != 2014-07-02 11:51:46.984715
  

Следует ли принимать такое поведение? Разве мы не должны полагаться на datetime.fromtimestamp, если нам нужно разрешение в микросекундах?

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

1. Немного более простой метод генерации нового значения с новым значением микросекунды: d = now.replace(microsecond=randint(0,999999)) .

2. @MartijnPieters: да, спасибо. Я просто хотел быть очень четким в вопросе.

3.Я бы нашел now.replace(microsend=randint(0,99999)) более ясным, поскольку тогда вам не нужно анализировать остальные 6 аргументов ключевого слова, чтобы увидеть, что вы делаете в этой строке.

Ответ №1:

Значения временных меток являются значениями с плавающей запятой. Значения с плавающей запятой являются приближениями, и поэтому применяются ошибки округления.

Например, значение с плавающей точкой 1404313854.442585 не является точным. Это действительно:

 >>> dt = datetime(2014, 7, 2, 16, 10, 54, 442585)
>>> dt.timestamp()
1404313854.442585
>>> format(dt.timestamp(), '.20f')
'1404313854.44258499145507812500'
  

Это ужасно близко к 442585, но не совсем. Это чуть ниже 442585, поэтому, когда вы берете только десятичную часть, умножаете это на 1 миллион, затем берете только целочисленную часть, остаток 0,991455078125 игнорируется, и в итоге получается 442584.

Таким образом, при последующем преобразовании значения с плавающей запятой обратно в datetime объект ошибки округления на 1 микросекунду являются нормальными.

Если вам требуется точность, не полагайтесь на float ; возможно, вместо этого сохраните значение микросекунды как отдельное целое число, затем используйте dt.fromtimestamp(seconds).replace(microsecond=microseconds) .

Вы можете найти уведомление об отклонении для PEP-410 (используйте десятичное число.Десятичный тип для временных меток), поучительный в этом контексте. PEP затронул проблему точности с временными метками, представленными в виде чисел с плавающей точкой.

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

1. Не указывает ли это на недостаток fromtimestamp в том, что он усекает вместо округления? Учитывая, что отметка времени, как ожидается , будет немного отклонена. Это предполагает обходной путь с помощью fromtimestamp(ts 0.0000005) .

2. @MarkRansom: Этот патч ввел параметр; до этого округление было неявным в других операциях. Согласно комментариям и проблеме, вводящей его , старое поведение округления было сохранено здесь, с явным округлением, введенным для проблем в другом месте.

3. @MarkRansom: Ах, нашел это. Смотрите эту проблему ; округление было изменено, потому что это более распространенное поведение в других местах и позволяет избежать округления метки времени в будущем .

4. @MarkRansom: например, лучше, чтобы половина временных меток отставала на одну микросекунду, чем другая половина была на одну микросекунду в будущем.

5. Я не уверен, что согласен с рассуждениями в этой проблеме. Есть важный принцип, которого я хотел бы придерживаться: когда есть дополнительные функции, цикл туда и обратно всегда должен давать идентичные результаты, если это возможно. И в этом случае это возможно . Даже простое добавление nextafter вместо округления решило бы проблему.

Ответ №2:

Временная метка — это время POSIX, которое по существу концептуализируется как целое число секунд с произвольной «эпохи». datetime.fromtimestamp() возвращает «локальную дату и время, соответствующие временной метке POSIX, например, возвращаемые time.time() «, в документации которой говорится, что «Возвращает [s] время в секундах с момента начала эпохи в виде числа с плавающей запятой. Обратите внимание, что, хотя время всегда возвращается в виде числа с плавающей запятой, не все системы предоставляют время с большей точностью, чем 1 секунда. «

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

РЕДАКТИРОВАТЬ: Следующий код проверяет, какие значения микросекунды недопустимы для произвольного datetime при запуске программы.

 from datetime import datetime
baset = datetime.now()

dodgy = []
for i in range(1000000):
    d = baset.replace(microsecond=i)
    ts = d.timestamp()
    if d != datetime.fromtimestamp(ts):
        dodgy.append(i)
print(len(dodgy))
  

Я получил 499 968 «сомнительных» раз, но я их не проверял.

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

1. datetime на самом деле не используется time.time() , хотя микросекунды поддерживаются независимо от платформы. Однако это проблема с числами с плавающей запятой.

2. Я бы согласился, что это проблема с плавающей запятой, и не понял, что все платформы теперь обеспечивают точность в микросекундах — просто они сообщают о таймере с наилучшим разрешением как число с плавающей запятой.

3. 499 968 «сомнительных» раз : почти половина. И это звучит правильно, потому что код C округляется в меньшую сторону (обратите внимание на _PyTime_ROUND_DOWN константу там) при преобразовании. Учитывая 1 миллион значений с плавающей запятой, есть вероятность, что чуть меньше половины будет отключено чуть ниже исходного значения микросекунды, а другая почти половина — чуть выше. Остальные значения могут быть точно представлены в виде двоичной дроби.