☰
Pendulum Interval 完全指南:理解两个 DateTime 之间的时间区间
2026/10/7 16:10:26 网站建设 项目流程
  • 后端

【免费下载链接】pendulum

Python datetimes made easy

项目地址:https://gitcode.com/gh_mirrors/pe/pendulum
点击查看免费下载

导读

Interval是 Pendulum 中表示"两个日期时间实例之间固定时间区间"的核心类型。当你用两个DateTime相减或调用diff()方法时,得到的并不是普通的timedelta,而是一个Interval——它继承自Duration,并额外保留生成它的起止实例,从而能提供按年、月、周、日等单位精确拆解的能力,同时对 DST(夏令时)切换天然感知。读完本文,你将掌握Interval的实例化方式、全部核心属性、range()区间迭代、in成员判断,以及其底层实现与Duration/timedelta的区别。

什么是 Interval

当你从一个DateTime实例中减去另一个DateTime,或使用diff()方法时,Pendulum 会返回一个Interval实例:

>>> import pendulum >>> start = pendulum.datetime(2000, 11, 20) >>> end = pendulum.datetime(2016, 11, 5) >>> interval = end - start >>> interval.years 15 >>> interval.months 11 >>> interval.in_years() 15 >>> interval.in_months() 191

Interval继承自 Duration 类,其额外优势在于"它知道是哪些实例生成了自己",因此可以访问更多方法和属性,比如按日历单位拆解出years、months,或者获取起止点start/end。在源码中,Interval被定义为class Interval(Duration, Generic[_T]),其中_T约束为date类型,表示它可以同时基于日期与日期时间构建(见 src/pendulum/interval.py)。

与 Duration 的关键差异:weeks 属性

由于Interval记录的是两个具体日期之间"按日历感知"的差值,它的weeks属性与Duration的计算口径不同:

# 注意:weeks 属性与 Duration 类的计算口径不同 >>> interval.weeks 2 # Duration 类中该值为 832 # 但 days 属性保持一致,以兼容 timedelta 类 >>> interval.days 5829

这里weeks的差异是刻意的设计:Interval.weeks基于精确日历差值(PreciseDiff)中的days字段除以 7 取整(abs(self._delta.days) // 7 * self._sign(self._delta.days),见 src/pendulum/interval.py),而Duration.weeks是基于总秒数换算。days属性则保持与timedelta完全一致,确保任何期待timedelta行为的代码都能无缝工作。

属性一览

兼容 timedelta 的属性

与Duration一样,Interval在属性层面完全兼容timedelta,可直接读取days、seconds、microseconds等标准字段。同时,它还会暴露total_seconds()、in_days()、in_hours()等换算方法。

DST 感知的自定义属性

Interval的自定义属性(如remaining_days、hours、minutes、remaining_seconds)会感知两次日期之间可能发生的 DST 切换,并自动调整。以跨夏令时的场景为例:

>>> import pendulum >>> start = pendulum.datetime(2017, 3, 7, tz='America/Toronto') >>> end = start.add(days=6) >>> interval = end - start # timedelta 属性(受 DST 影响:少了一个小时) >>> interval.days 5 >>> interval.seconds 82800 # interval 属性(按日历天数感知 DST) >>> interval.remaining_days 6 >>> interval.hours 0 >>> interval.remaining_seconds 0

在这个例子中,America/Toronto时区在 2017 年 3 月 12 日凌晨发生了 DST 切换(时钟拨快一小时),因此两个相隔 6 个日历日的时刻,其真实时间差只有 5 天 23 小时——timedelta视角的days=5, seconds=82800如实反映了这一点;而Interval的日历视角则认为跨过了 6 个自然日,remaining_days=6。这正是"固定时长"与"日历日数"两种口径的本质区别。

上述行为在测试 tests/interval/test_construct.py 中有完整断言:interval.days == 5、interval.seconds == 82800、interval.remaining_days == 6、interval.in_days() == 6、interval.in_hours() == 5 * 24 + 23。类似的 DST 场景还有欧洲巴黎时区(见同文件 tests/interval/test_construct.py):同一天 1:30 到次日 1:30,in_words()返回"1 day"而in_hours() == 23。

重要警告:算术运算返回 Duration

由于Interval的本质是两个日期时间之间的固定时长,大多数算术运算(加、减、乘、除、取模等)都会返回一个Duration而不是Interval:

>>> import pendulum >>> dt1 = pendulum.datetime(2016, 8, 7, 12, 34, 56) >>> dt2 = dt1.add(days=6, seconds=34) >>> interval = pendulum.interval(dt1, dt2) >>> interval * 2 Duration(weeks=1, days=5, minutes=1, seconds=8)

从源码可以清晰看到这一设计意图:Interval的__add__、__sub__、__mul__、__truediv__、__floordiv__、__mod__、__divmod__等方法全部先调用as_duration()将自身转换为Duration再执行运算(见 src/pendulum/interval.py)。例如interval * 2实际执行的是self.as_duration().__mul__(other)。as_duration()的实现仅保留总秒数:

def as_duration(self) -> Duration: """ Return the Interval as a Duration. """ return Duration(seconds=self.total_seconds())

实例化(Instantiation)

你可以使用pendulum.interval()辅助函数创建一个Interval实例:

>>> import pendulum >>> start = pendulum.datetime(2000, 1, 1) >>> end = pendulum.datetime(2000, 1, 31) >>> interval = pendulum.interval(start, end)

pendulum.interval()是包级工厂函数,签名与Interval构造函数一致:interval(start, end, absolute=False)(见 src/pendulum/init.py)。

倒置区间(inverted interval)

你也可以创建倒置的区间(起点晚于终点),此时相关属性会变成负值:

>>> interval = pendulum.interval(end, start) >>> interval.remaining_days -2

如果传入的日期是倒置的,但你希望区间始终为正,可以设置absolute=True关键字参数:

>>> interval = pendulum.interval(end, start, absolute=True) >>> interval.remaining_days 2

从源码看,absolute=True时__new__会直接交换start与end(if absolute and start > end: end, start = start, end,见 src/pendulum/interval.py),因此构造出的区间起点必然早于终点。

构造时的类型校验

Interval.__new__在构造时会做两类严格校验(见 src/pendulum/interval.py):

  • 如果起点和终点类型不一致(一个是datetime、另一个是date),抛出ValueError: "Both start and end of an Interval must have the same type";
  • 如果一个是带时区(aware)的datetime、另一个是不带时区(naive)的datetime,抛出TypeError: "can't compare offset-naive and offset-aware datetimes"。

测试 tests/interval/test_construct.py 还验证了:传入原生datetime对象时,interval.start/interval.end会被自动转换为pendulum.DateTime;传入倒置参数并开启absolute后,start与end会被正确交换。

区间迭代:range() 方法

如果要在区间上迭代,可以使用range()方法:

>>> import pendulum >>> start = pendulum.datetime(2000, 1, 1) >>> end = pendulum.datetime(2000, 1, 10) >>> interval = pendulum.interval(start, end) >>> for dt in interval.range('days'): >>> print(dt) '2000-01-01T00:00:00+00:00' '2000-01-02T00:00:00+00:00' '2000-01-03T00:00:00+00:00' '2000-01-04T00:00:00+00:00' '2000-01-05T00:00:00+00:00' '2000-01-06T00:00:00+00:00' '2000-01-07T00:00:00+00:00' '2000-01-08T00:00:00+00:00' '2000-01-09T00:00:00+00:00' '2000-01-10T00:00:00+00:00'

!!! noterange()支持的迭代单位为:years、months、weeks、days、hours、minutes、seconds和microseconds。

控制步长:amount 参数

可以为传入的单位指定一个amount,控制每次迭代的间隔长度:

>>> for dt in interval.range('days', 2): >>> print(dt) '2000-01-01T00:00:00+00:00' '2000-01-03T00:00:00+00:00' '2000-01-05T00:00:00+00:00' '2000-01-07T00:00:00+00:00' '2000-01-09T00:00:00+00:00'

range(unit, amount=1)的默认amount为 1。从源码看(src/pendulum/interval.py),迭代器以start为起点,反复调用self.start的add(或倒置区间时的subtract)方法叠加步长,直到越过end为止;非绝对模式下若区间倒置,则改用减法并反向比较。测试 tests/interval/test_range.py 验证了按天迭代正好覆盖起止两端(31 天);跨 DST 的迭代测试(同文件 tests/interval/test_range.py)则展示了迭代结果对时区偏移同样敏感。

直接迭代 Interval

你也可以直接对Interval实例进行迭代,此时单位固定为days:

>>> for dt in interval: >>> print(dt)

这得益于Interval.__iter__的实现:return self.range("days")(见 src/pendulum/interval.py)。测试 tests/interval/test_range.py 验证了直接迭代 31 天区间会产出 31 个pendulum.DateTime。

成员判断:in 关键字

可以用in关键字检查一个DateTime实例是否位于区间内部:

>>> dt = pendulum.datetime(2000, 1, 4) >>> dt in interval True

Interval.__contains__的实现是一个简单的区间比较:return self.start <= item <= self.end(见 src/pendulum/interval.py)。注意边界语义:起点与终点本身都包含在区间内。测试 tests/interval/test_range.py 验证了区间内的时刻返回True,而早于起点 1 小时的时刻返回False。

进阶:源码级原理

精确日历差值:PreciseDiff

Interval的核心数据来自precise_diff(_start, _end)返回的PreciseDiff结构(见 src/pendulum/interval.py),它同时保留了按日历单位(年、月、日)的精确拆解和按总秒数的拆解。years、months、hours、minutes等属性直接取自_delta,而remaining_days取的是_delta.days对 7 取模的结果——这就是它能跨 DST 正确反映"过了几个自然日"的原因。

precise_diff的底层实现位于 rust/src/python/types/precise_diff.rs,由 Rust 扩展(或纯 Python 回退实现src/pendulum/_helpers.py)提供,见 src/pendulum/helpers.py 的导入逻辑。

相等性、哈希与序列化

  • Interval与另一个Interval比较相等时,比较的是(start, end, absolute)三元组;与timedelta等其它对象比较时,则回退到as_duration()后的时长比较(见 src/pendulum/interval.py)。因此interval == timedelta(days=2)是合法的,测试 tests/interval/test_behavior.py 对此有断言。
  • __hash__基于(start, end, absolute),支持作为字典键或放入集合。
  • 通过__reduce_ex__/_getstate支持pickle序列化,且copy.deepcopy会深度复制起止实例(见 src/pendulum/interval.py),对应测试 tests/interval/test_behavior.py。

取反与取绝对值

Interval还支持一元运算:-interval会返回一个起止互换的新Interval(__neg__),abs(interval)会返回以absolute=True构造的新Interval(__abs__),二者都保留了类型本身。

人类可读输出:in_words()

Interval继承了Duration的in_words(locale=None, separator=" ")方法,可将区间翻译为当前语言环境下的自然语言描述(如法语的"6 jours 23 heures 58 minutes")。它按年、月、周、剩余天、时、分、秒的顺序组装,并支持通过pendulum.set_locale()切换语言;当所有单位都为零时会回退到"0 microseconds"之类的表达(见 src/pendulum/interval.py)。在 tests/interval/test_in_words.py 中有大量多语言断言语料。

总结

Interval是 Pendulum 中连接"日历感知"与"固定时长"两种时间观的桥梁:

  • 日历视角:years、months、remaining_days、weeks等属性精确反映两个日期之间跨过了多少自然年、月、日;
  • 时长视角:days、seconds、total_seconds()、in_*()系列方法与timedelta完全兼容,可无缝用于任何期望标准timedelta的代码;
  • DST 感知:自定义属性会自动补偿夏令时切换带来的"少一小时"或"多一小时"偏差;
  • 迭代与判断:range(unit, amount)、直接迭代、in成员判断让区间成为可消费的数据结构。

需要注意的是,任何算术运算(+、-、*、/、%等)都会将结果降级为Duration,因为运算结果不再是"两个具体时刻之间的区间",而只是一个时长。理解了这一设计取舍,你就能在日程排期、跨时区统计、日历跨度计算等场景中正确地选择Interval或Duration。

相关参考:完整的 API 定义见 src/pendulum/interval.py,interval()工厂函数见 src/pendulum/init.py,配套测试位于 tests/interval/ 目录(构造、行为、迭代、算术、哈希与多语言输出共六个测试文件)。

  • 后端

【免费下载链接】pendulum

Python datetimes made easy

项目地址:https://gitcode.com/gh_mirrors/pe/pendulum
点击查看免费下载

相关推荐

上一篇:RESTful API版本演进终极指南:10个向后兼容与平滑升级策略
下一篇:ni项目安全最佳实践:如何避免常见的安全隐患

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询