【Python】 datetime陷阱及相关库的解决方案

目录

datetime中存在的部分问题

将不兼容的概念挤压到一个类中

UTC时区

固定偏移时区

IANA时区

解决方案

“naïve”的含义不一致

相关措施

不存在的日期时间

歧义性问题

解决歧义性破坏相等性

处理时区内的不一致相等性

datetime与date不能比较

datetime.timezone不是时区

解决方案

相关例子

加法和减法考虑夏令时:

Naïve永远是Naïve:

明确处理不存在的日期时间:


Python 的 datetime 模块在处理日期和时间时存在一些陷阱,本文将讨论其中的十个问题,并介绍一些相关库是如何解决这些问题的。

datetime中存在的部分问题

将不兼容的概念挤压到一个类中

在Python中,datetime实例可以是“naïve”(无时区信息)或“aware”(有时区信息),而且它们不能混合使用。在任何复杂的代码库中,很难确保在不实际运行代码的情况下不会意外混用它们。结果就是你不得不编写冗余的运行时检查,或者希望所有开发人员都认真阅读文档字符串。

# 这里无法区分是否有时区信息
def plan_mission(launch_utc: datetime) -> None: ...

除了区分是否有时区之外,还存在一个问题,即在有时区类别内实际上有几种不同类型的datetime。例如,在处理模糊性时,UTC、固定偏移或IANA时区datetime的行为是非常不同的。

UTC时区

UTC时区的datetime是在协调世界时下定义的,没有夏令时调整。这意味着在处理时区转换和模糊性时,UTC datetime相对较为简单和明确。

固定偏移时区

具有固定偏移的datetime表示相对于UTC的固定时间偏移,例如东部标准时间(EST)或太平洋夏令时时间(PDT)。在这种情况下,模糊性可能会导致一些意料之外的结果,特别是在夏令时转换期间。

IANA时区

IANA时区(例如America/New_York)考虑了夏令时和其他时区变化,提供更准确的时区信息。在模糊性方面,IANA时区通常能够更好地处理,但仍需谨慎处理。

解决方案

  • heliclockter(推荐)

    该库为本地、时区和UTC datetime提供了分离的类。通过将不同类型的datetime分开,提高了代码的清晰度和可读性,减少了混淆的可能性。

  • DateType(推荐) DateType允许类型检查器区分“naïve”和“aware” datetime。通过引入DateType,可以更容易地进行静态类型检查,帮助开发人员在代码编写阶段捕获潜在的类型错误。

  • arrow 和 pendulum(不推荐) 这两个库仍然使用一个类来表示“naïve”和“aware” datetime。这种设计可能导致一些混淆和不一致性,尤其是在处理模糊性和不同时区时。

“naïve”的含义不一致

在 Python 的 datetime 上下文中,“naïve” 表示没有时区信息的日期时间对象。然而,对“naïve”日期时间的解释和处理可能不一致。 arrowpendulum 等库在时区处理方面采用更明确的方法。

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

datetimedate不能比较

你可能会惊讶地发现**datetimedate的子类。这乍一看似乎并没有问题,但它导致了一些奇怪的行为。最明显的是,datedatetime之间不能进行比较**,违反了子类应该如何工作的基本假设。**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")

arrowpendulum 都通过将时区指定为字符串而不需要特殊的类实例来回避这个问题。

解决方案

使用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)

Naïve永远是Naïve:

  • 不关联时区的日期时间始终保持为不关联状态。
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:

你可能感兴趣的:(语言,工具,python,后端,开发语言)