Почему дата + timedelta - дата, а не дата?
В Python, в операции чисел смешанного типа, более узкий тип расширен больше, чем у другого, например int
+ float
→ float
:
In [57]: 3 + 0.1
Out[57]: 3.1
Но для datetime.date
имеем datetime.date
+ datetime.timedelta
→ datetime.date
, а не datetime.datetime
:
In [58]: datetime.date(2013, 1, 1) + datetime.timedelta(seconds=42)
Out[58]: datetime.date(2013, 1, 1)
Почему расширенное рассуждение применяется к числам, но не к date
/datetime
/timedelta
?
(Фон: я пишу процедуру чтения для формата файла, где одно поле - год, одно поле - в день года, одно поле - миллисекунды с полуночи. Конечно, простое и явное решение datetime.datetime(2013, 1, 1, 0, 0, 0) + datetime.timedelta(seconds=42)
, но можно было бы также обосновать, что следует переписать 3 + 0.1
как 3.0 + 0.1
)
Ответы
Ответ 1
Поведение задокументировано:
date2 перемещается вперед во времени, если timedelta.days > 0
, или назад, если timedelta.days < 0
. Затем date2 - date1 == timedelta.days
. timedelta.seconds
и timedelta.microseconds
игнорируются.
(Мой акцент. Это поведение осталось неизменным, поскольку
2.3 >
Я не смог найти никаких доказательств того, почему модуль разработан таким образом. Конечно, есть примеры использования, такие как ваши, где вы хотите представить момент времени, соответствующий полуночи в начале дня. В этих случаях раздражает необходимость конвертировать туда и обратно. Но есть и другие варианты использования, в которых вы хотите представить целый день (а не только некоторый момент времени в этот день), и в этом случае вы не хотите, чтобы случайно закончились частичные дни, когда вы добавляете timedeltas.
Крис Уизерс предложил изменить поведение в вопрос 3249, но Тим Петерс отметил, что:
вряд ли будет принято несовместимое изменение документально подтвержденного поведения, которое всегда срабатывало.
Если вам нужен объект, который ведет себя как datetime.date
, но где арифметические операции возвращают объекты datetime.datetime
, то его не должно быть сложно написать:
from datetime import date, datetime, time, timedelta
def _part_day(t):
"""Return True if t is a timedelta object that does not consist of
whole days.
"""
return isinstance(t, timedelta) and (t.seconds or t.microseconds)
class mydate(date):
"""Subclass of datetime.date where arithmetic operations with a
timedelta object return a datetime.datetime object unless the
timedelta object consists of whole days.
"""
def datetime(self):
"""Return datetime corresponding to the midnight at start of this
date.
"""
return datetime.combine(self, time())
def __add__(self, other):
if _part_day(other):
return self.datetime() + other
else:
return super().__add__(other)
__radd__ = __add__
def __sub__(self, other):
if _part_day(other):
return self.datetime() - other
else:
return super().__sub__(other)
(Это не проверено, но отсюда не должно быть трудно заставить его работать.)
Ответ 2
Объект timedelta
не хранит никакой информации о том, касается ли он только даты, а также раз. (Тот факт, что количество часов/минут/секунд/микросов равно 0, может быть просто совпадением!)
Следовательно, предположим, что у нас есть кто-то, кто просто хочет манипулировать датами, игнорируя время, она сделает что-то вроде my_new_date = my_old_date + timedelta(days=1)
. Она была бы очень удивлена и, возможно, раздражена, обнаружив, что my_new_date
теперь является объектом datetime
, а не объектом date
.
Ответ 3
Дата не является подклассом даты и времени; datetime - составной тип, объединяющий объект date
и time
в один. Вы не можете решить создать сложный тип из операций на date
здесь; компонент time
не является частью даты.
С другой стороны, числовая иерархия Python определяет целые числа как тип специализированного float (numbers.Integral
является косвенным подклассом numbers.Real
), и поскольку такие операции смешивания между целыми числами и поплавками приводят к получению базового типа. А float
не является составным типом, для десятичной части значения нет отдельного типа.
Если вы хотите создать составной тип из операций с датами, вы должны быть явными (и явное лучше, чем неявное). Добавьте компонент времени самостоятельно:
datetime.combine(yourdate, time.min) + yourdelta
где yourdate
можно было бы получить из анализа date.strptime('%Y %j')
вашего года и входа в день года.
Альтернативы (создающие объект datetime
, иногда основанный на значении timedelta.seconds
или всегда) требуют от программиста развернуть компонент даты еще раз, если это все, что они ожидали.
Ответ 4
Вероятная техническая причина заключается в том, что встроенные типы в Python не возвращают подклассы из своих операторов, как правило, т.е. date.__add__
не вернет datetime
. И для последовательного поведения требуется, чтобы date
и datetime
были взаимозаменяемыми (они не являются).
date + timedelta
поведение документировано и не изменится. Если вы хотите datetime
в результате; создайте datetime
с даты d
:
dt = datetime(d.year, d.month, d.day)
Технически, date.__add__
мог делегировать работу timedelta.__radd__
. timedelta
хранит days
отдельно и поэтому просто и эффективно выяснить, представляет ли оно целое число дней, т.е. мы могли бы, если бы хотели получить date
или datetime
из date + timedelta
(это не значит мы должны).
Проблема в том, что 1
и 1.0
в этом случае одинаковы timedelta(1)
, т.е. если мы должны позволить date + timedelta
возвращать datetime
, тогда он должен возвращать datetime
для всех значений, если мы рассмотрим только типы.
Был прецедент, когда int + int
возвращал либо int
, либо long
в зависимости от результата, т.е. операция с теми же типами может возвращать значения разных типов, зависящие только от входных значений. Хотя date
и datetime
не так много взаимозаменяемы, как int
и long
были.
date + timedelta
возврат date
для некоторых значений timedelta
и datetime
для других создаст путаницу, если мы не представим date(y,m,d) == datetime(y,m,d)
тоже (например, 1 == 1.0
) или date.today() < datetime.now()
из связанная проблема Python, упомянутая @Gareth Rees (например, 1 < 1.1
). datetime
, являющийся подклассом, предлагает этот маршрут, хотя я слышал аргумент о том, что было ошибкой сделать подкласс datetime
a date
.
Желаемое поведение реализовано в пакете dateutil
:
>>> from datetime import date
>>> from dateutil.relativedelta import relativedelta
>>> date.today() + relativedelta(days=1)
datetime.date(2016, 5, 12)
>>> date.today() + relativedelta(days=1, seconds=1)
datetime.datetime(2016, 5, 12, 0, 0, 1)
time() + timedelta
недействителен, поэтому combine()
мне не помогает.
combine()
работает datetime.combine(d, time()) + timedelta_obj
. Хотя вы можете написать это как: datetime(d.year, d.month, d.day) + timedelta_obj
.
Конечно, простым и явным решением является datetime(2013, 1, 1, 0, 0, 0) + timedelta(seconds=42)
, но можно также переписать 3 + 0.1
как 3.0 + 0.1
)
int
+ float
всегда float
:
>>> 3 + 1.0
4.0
>>> 3 + 1
4
в отличие от 1.0
и 1
; type(timedelta(1.0)) == type(timedelta(1))
(тот же тип в дополнение к тому, чтобы быть равным).
Я пишу процедуру чтения для формата файла, где одно поле - год, одно поле - в день года, одно поле - миллисекунды - с полуночи.
dt = datetime(year, 1, 1) + timedelta(days=day_of_year - 1, milliseconds=ms)
Ответ 5
Когда вы суммируете timedelta на дату, python будет искать атрибут days timedelta. Поэтому, если добавить 42 секунды, день будет 0, и это не повлияет на вашу дату.