目录
datetime中存在的部分问题
将不兼容的概念挤压到一个类中
UTC时区
固定偏移时区
IANA时区
解决方案
“naïve”的含义不一致
相关措施
不存在的日期时间
歧义性问题
解决歧义性破坏相等性
处理时区内的不一致相等性
datetime与date不能比较
datetime.timezone不是时区
解决方案
相关例子
加法和减法考虑夏令时:
Naïve永远是Naïve:
明确处理不存在的日期时间:
Python 的 datetime
模块在处理日期和时间时存在一些陷阱,本文将讨论其中的十个问题,并介绍一些相关库是如何解决这些问题的。
在Python中,datetime实例可以是“naïve”(无时区信息)或“aware”(有时区信息),而且它们不能混合使用。在任何复杂的代码库中,很难确保在不实际运行代码的情况下不会意外混用它们。结果就是你不得不编写冗余的运行时检查,或者希望所有开发人员都认真阅读文档字符串。
# 这里无法区分是否有时区信息
def plan_mission(launch_utc: datetime) -> None: ...
除了区分是否有时区之外,还存在一个问题,即在有时区类别内实际上有几种不同类型的datetime。例如,在处理模糊性时,UTC、固定偏移或IANA时区datetime的行为是非常不同的。
UTC时区的datetime是在协调世界时下定义的,没有夏令时调整。这意味着在处理时区转换和模糊性时,UTC datetime相对较为简单和明确。
具有固定偏移的datetime表示相对于UTC的固定时间偏移,例如东部标准时间(EST)或太平洋夏令时时间(PDT)。在这种情况下,模糊性可能会导致一些意料之外的结果,特别是在夏令时转换期间。
IANA时区(例如America/New_York)考虑了夏令时和其他时区变化,提供更准确的时区信息。在模糊性方面,IANA时区通常能够更好地处理,但仍需谨慎处理。
heliclockter(推荐)
该库为本地、时区和UTC datetime提供了分离的类。通过将不同类型的datetime分开,提高了代码的清晰度和可读性,减少了混淆的可能性。
DateType(推荐) DateType允许类型检查器区分“naïve”和“aware” datetime。通过引入DateType,可以更容易地进行静态类型检查,帮助开发人员在代码编写阶段捕获潜在的类型错误。
arrow 和 pendulum(不推荐) 这两个库仍然使用一个类来表示“naïve”和“aware” datetime。这种设计可能导致一些混淆和不一致性,尤其是在处理模糊性和不同时区时。
在 Python 的 datetime
上下文中,“naïve” 表示没有时区信息的日期时间对象。然而,对“naïve”日期时间的解释和处理可能不一致。 arrow
和 pendulum
等库在时区处理方面采用更明确的方法。
from datetime import datetime,UTC
import email
# 一个没有时区信息的 datetime
d = datetime(2024, 1, 1)
# ⚠️: 视为当地时间
d.timestamp()
d.astimezone(UTC)
# : 假设世界标准时间
d.utctimetuple()
email.utils.format_datetime(d)
datetime.utcnow()
# here: neither! (error)
d >= datetime.now(UTC)
# >>> TypeError: can't compare offset-naive and offset-aware datetimes
pendulum 和 arrow
尽管pendulum和arrow不鼓励使用“naïve” datetime,但它们仍然支持相同的不一致语义。
DateType 和 heliclockter
这两个库不支持“naïve” datetime的一致性语义问题。
在时区时钟向前调整时,会产生一个“间隙(gap)”。例如,如果时钟从凌晨2点调到凌晨3点,时区中就不存在2:30 AM这个时间点。然而,标准库在创建这样的不存在时间时并不会产生警告。一旦对这些对象进行操作,就可能遇到问题。
# 这个时间在这个日期不存在
d = datetime(2023, 3, 26, 2, 30, tzinfo=paris)
# 没有时间戳存在,所以它随意制造了一个
t = d.timestamp()
datetime.fromtimestamp(t) == d # False
当时区时钟被调回时,会产生一种歧义。例如,如果在凌晨3点将时钟调回一小时,那么凌晨2:30这个时间点存在两次:在调整前和调整后。为了解决这些歧义问题,引入了**[fold
**属性](PEP 495 – Local Time Disambiguation | peps.python.org)。
问题在于,对于**fold
**并没有客观的默认值:你是想选择“更早”的选项还是“更晚”将取决于具体的上下文。为了保持向后兼容性,标准库默认为0,这会导致默认假设你想要较早的发生时间而没有任何提示。
d = datetime(2023, 10, 29, 2, 30, tzinfo=paris)
尽管引入了**fold
**来消除模糊性,但由于向后兼容的原因,不同时区之间对于消除模糊性的时间进行比较时,始终会评估为False。
# 创建一个已经正确消除模糊性的时间点
d = datetime(2023, 10, 29, 2, 30, tzinfo=paris, fold=1)
# 转换为UTC时区
d_utc = d.astimezone(UTC)
# 比较时间戳
d_utc.timestamp() == d.timestamp() # True:同一时刻
# 但是,比较两个已消除模糊性的时间
d_utc == d # False
在与之前的问题相反的情况下,当比较两个具有完全相同**tzinfo
对象的datetime时,它们将通过它们的“壁挂时间(wall time)”进行比较。这在大多数情况下是相同的,除非涉及到fold
**的情况。
# 两个由于夏令时转换而相隔一小时的时间点
earlier = datetime(2023, 10, 29, 2, 30, tzinfo=paris, fold=0)
later = datetime(2023, 10, 29, 2, 30, tzinfo=paris, fold=1)
# 时间戳比较
earlier.timestamp() == later.timestamp() # False,符合预期
# 但是,比较两个在同一时区内的时间点
earlier == later # True!
如果你与相同的时区进行比较,但你从 dateutil.tz
而不是 ZoneInfo
获取它,你会得到不同的结果!
from dateutil import tz
later2 = later.replace(tzinfo=tz.gettz("Europe/Paris"))
earlier == later2 # False
datetime
与date
不能比较你可能会惊讶地发现**datetime
是date
的子类。这乍一看似乎并没有问题,但它导致了一些奇怪的行为。最明显的是,date
和datetime
之间不能进行比较**,违反了子类应该如何工作的基本假设。**datetime
继承自date
**现在被广泛认为是标准库中的一个设计缺陷。
# 如下方法,
def is_future(d: date) -> bool:
return d > date.today()
# 从`date`继承的一些方法没有意义
date.today() # datetime.date(2024, 2, 1)
datetime.today() # datetime.datetime(2024, 2, 1, 16, 22, 14, 661715)
is_future(datetime.today())
# 抛出异常:TypeError: can't compare datetime.datetime to datetime.date
datetime.timezone
不是时区**datetime.timezone
**只能表示固定偏移,并不能涵盖夏令时(DST)等转换。这可能对初学者来说是一个令人困惑的陷阱。
from datetime import timezone, datetime, timedelta
from zoneinfo import ZoneInfo
# 错误:它是一个仅在冬季有效的固定偏移!
paris_timezone = timezone(timedelta(hours=1), "CET")
# ✅ 正确的方式
paris_timezone = ZoneInfo("Europe/Paris")
arrow
和 pendulum
都通过将时区指定为字符串而不需要特殊的类实例来回避这个问题。
使用Python 库whenever处理
UTCDateTime
: 用于处理“UTC everywhere”情况的类。OffsetDateTime
: 用于简单的本地化,不考虑夏令时的类。ZonedDateTime
: 具有完整特性的IANA时区的类。LocalDateTime
: 本地系统时区的类。NaiveDateTime
: 不关联任何时区的类。from whenever import (
UTCDateTime,
OffsetDateTime,
ZonedDateTime,
LocalDateTime,
NaiveDateTime,
)
result = ZonedDateTime(2023, 1, 1, tz="Europe/Paris") + timedelta(days=1)
naive = NaiveDateTime(2023, 1, 1, 12, 30)
ZonedDateTime(2023, 10, 29, 2, tz="Europe/Paris")
ZonedDateTime(2023, 10, 29, 2, tz="Europe/Paris", disambiguate="raise")
# Ambiguous: