#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 миллион значений с плавающей запятой, есть вероятность, что чуть меньше половины будет отключено чуть ниже исходного значения микросекунды, а другая почти половина — чуть выше. Остальные значения могут быть точно представлены в виде двоичной дроби.