- 后端
【免费下载链接】pendulum
Python datetimes made easy
导读
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() 191Interval继承自 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 TrueInterval.__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
相关推荐
深度解析LightRAG:构建高效知识图谱增强检索系统的架构实践
深度解析LightRAG:构建高效知识图谱增强检索系统的架构实践 在当今信息爆炸的时代,传统检索系统面临语义理解不足、上下文关联缺失等挑战。LightRAG作为
人工智能RAG大模型知识图谱本地部署Pendulum Interval类终极指南:掌握时间区间操作的5个核心技巧
Pendulum Interval类终极指南:掌握时间区间操作的5个核心技巧 Pendulum是一个强大的Python日期时间库,它的Interval类让时间区
后端Pendulum时间戳转换终极指南:从Unix时间到DateTime的完整教程
Pendulum时间戳转换终极指南:从Unix时间到DateTime的完整教程 在处理时间数据时,Unix时间戳转换是Python开发者经常遇到的挑战。Pend
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考