- 后端
【免费下载链接】pendulum
Python datetimes made easy
本指南围绕 Pendulum 的差值计算体系展开,深入讲解diff()如何返回表示两个时刻总时长的Interval对象、如何通过in_*系列方法按指定单位(年/月/周/天/时/分/秒)表达截断后的差值,以及diff_for_humans()如何输出 "1 day ago"、"3 weeks from now" 等人类可读的文案并支持多语言本地化。读完本文,你将掌握 Pendulum 中日期差值计算的完整 API、正负号与绝对值的控制方式、截断(不四舍五入)的边界行为,以及本地化定制的方法,并能直接迁移到评论时间、内容时效等真实业务场景。
一、diff():两个时间点之间的总时长
diff()是 Pendulum 计算两个时刻差值的入口方法。它返回一个 Interval 实例,该实例表示两个DateTime之间的总时长。例如:
>>> import pendulum >>> dt_ottawa = pendulum.datetime(2000, 1, 1, tz='America/Toronto') >>> dt_vancouver = pendulum.datetime(2000, 1, 1, tz='America/Vancouver') >>> dt_ottawa.diff(dt_vancouver).in_hours() 3两个城市同刻跨时区比较,diff()返回的是真实的 3 小时时差,而不是简单的本地时间相减。
1.1 参数签名与语义
从源码看,DateTime.diff()的定义位于 src/pendulum/datetime.py:
def diff(self, dt: datetime.datetime | None = None, abs: bool = True) -> Interval[datetime.datetime]: if dt is None: dt = self.now(self.tz) return Interval(self, dt, absolute=abs)两个参数的完整语义如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
dt | None | 要与之比较的DateTime实例;传入None时使用now()(并沿用当前实例的时区self.tz) |
abs | True | 是否返回绝对值。True时始终返回正数;False时返回带正负号的相对值,若传入的日期早于当前实例则带-号 |
1.2 正负号:absolute 参数的实战效果
abs参数决定了差值的符号方向,文档示例清楚地展示了这一点:
>>> dt_ottawa.diff(dt_vancouver).in_hours() 3 >>> dt_ottawa.diff(dt_vancouver, False).in_hours() 3 >>> dt_vancouver.diff(dt_ottawa, False).in_hours() -3当abs=True(默认)时,无论谁先谁后,结果都是正数 3;当abs=False时,以调用者为基准,dt_ottawa.diff(dt_vancouver, False)表示"渥太华相对温哥华"(Ottawa 更早,但二者相减符号随内部方向),而dt_vancouver.diff(dt_ottawa, False)返回-3,明确表达出"温哥华在渥太华之前 3 小时"这一方向性信息。
从 src/pendulum/interval.py 的实现可以看到,Interval.__new__在absolute=True且start > end时会自动交换起止点(end, start = start, end),从而保证绝对值的语义;同时Interval.__init__中通过self._invert = start > end记录方向,供后续格式化使用。
1.3 返回值类型:Interval
diff()返回的不是普通的timedelta,而是Interval。Interval继承自Duration(见 src/pendulum/interval.py),并额外携带start、end两个端点属性,支持in_words()、range()、迭代与成员判断(in运算符)等高级能力。关于 Interval 的更多用法可参考 Interval 文档。
二、in_* 方法:以指定单位表达总差值
diff()返回的 Interval 可以通过一系列in_*方法,把总时长折算为任意单位。这些方法永远返回"以指定时间单位表达的完整总差值",所有值都是截断(truncate)而非四舍五入(round)。
2.1 可用方法一览
Interval(及父类 Duration)提供的单位方法定义在 src/pendulum/duration.py:
def in_weeks(self) -> int: return int(self.total_weeks()) def in_days(self) -> int: return int(self.total_days()) def in_hours(self) -> int: return int(self.total_hours()) def in_minutes(self) -> int: return int(self.total_minutes()) def in_seconds(self) -> int: return int(self.total_seconds())Interval 还在 src/pendulum/interval.py 中补充了in_years()(返回整年数)、in_months()(years * 12 + months的完整月数)、in_weeks()与in_days()等语义。所有方法都以int(...)截断小数部分,因此 59 秒的差值用in_minutes()表达就是 0,而 60 秒就是 1。
2.2 截断语义的文档示例
文档用一组精确的例子验证了"截断而非四舍五入"的规则:
>>> dt = pendulum.datetime(2012, 1, 31, 0) >>> dt.diff(dt.add(months=1)).in_days() 29 >>> dt.diff(dt.subtract(months=1), False).in_days() -31 >>> dt = pendulum.datetime(2012, 4, 30, 0) >>> dt.diff(dt.add(months=1)).in_days() 30 >>> dt.diff(dt.add(weeks=1)).in_days() 7 >>> dt = pendulum.datetime(2012, 1, 1, 0) >>> dt.diff(dt.add(seconds=59)).in_minutes() 0 >>> dt.diff(dt.add(seconds=60)).in_minutes() 1 >>> dt.diff(dt.add(seconds=119)).in_minutes() 1 >>> dt.diff(dt.add(seconds=120)).in_minutes() 2这些示例蕴含两个关键事实:
- 月份长度按真实日历计算:1 月 31 日加一个月到达 2 月 29 日(2012 为闰年),差值 29 天;4 月 30 日加一个月到达 5 月 30 日,差值 30 天。
diff()的月差基于精确的日历日期差,而非固定 30 天。 - 严格向下截断:119 秒 = 1 分 59 秒,
in_minutes()返回 1;120 秒 = 2 分钟,返回 2。任何小于整单位的部分都被丢弃。
2.3 底层精确差值计算
Interval 内部的精确差值由precise_diff()完成(src/pendulum/interval.py 中self._delta: PreciseDiff = precise_diff(_start, _end))。precise_diff在 src/pendulum/helpers.py 中按环境动态加载:当PENDULUM_EXTENSIONS环境变量为"1"(默认)且平台为 64 位时,优先使用 Rust 扩展实现(pendulum._pendulum.precise_diff),否则回退到 Python 实现(pendulum._helpers.precise_diff)。这也是 Interval 能精确拆解出years / months / weeks / days / hours / minutes / seconds / remaining_*等结构化字段的原因。
2.4 测试用例佐证
仓库测试 tests/datetime/test_diff.py 覆盖了所有单位的正负、跨年、跨时区场景,例如:
dt.diff(dt.subtract(years=1), False).in_years() == -1(相对值带符号)dt.diff(dt.add(days=1).add(hours=13)).in_days() == 1(截断)dt.diff(dt.add(seconds=1.9)).in_seconds() == 1(小数秒截断)dt_ottawa.diff(dt_vancouver).in_seconds() == 3 * 60 * 60(跨时区精确换算)
三、diff_for_humans():人性化差值文案
diff_for_humans()在差值数值后附加一个短语,把"天数"翻译成自然语言表达。它根据比较对象是否为 now以及方向(过去/未来),共有 4 种输出模式:
| 比较场景 | 输出模式 | 示例 |
|---|---|---|
| 过去的值 vs 默认 now | ago | 1 hour ago、5 months ago |
| 未来的值 vs 默认 now | from now | 1 hour from now、5 months from now |
| 过去的值 vs 另一个值 | before | 1 hour before、5 months before |
| 未来的值 vs 另一个值 | after | 1 hour after、5 months after |
3.1 基本用法示例
>>> import pendulum # 最典型的场景:评论时间对比当前 now() >>> pendulum.now().subtract(days=1).diff_for_humans() '1 day ago' >>> pendulum.now().diff_for_humans(pendulum.now().subtract(years=1)) '1 year after' >>> dt = pendulum.datetime(2011, 8, 1) >>> dt.diff_for_humans(dt.add(months=1)) '1 month before' >>> dt.diff_for_humans(dt.subtract(months=1)) '1 month after' >>> pendulum.now().add(seconds=5).diff_for_humans() '5 seconds from now' >>> pendulum.now().subtract(days=24).diff_for_humans() '3 weeks ago'注意最后一行:24 天被智能地折叠为"3 weeks"而非"24 days"——这体现了diff_for_humans()的近似取整策略(见下文 4.2 节)。
3.2 absolute 参数:去掉修饰语
传入True作为第 2 个参数,可以移除ago、from now、before、after等修饰语,只保留纯数值:
>>> pendulum.now().subtract(days=24).diff_for_humans(absolute=True) '3 weeks'3.3 源码调用链
DateTime.diff_for_humans()的实现位于 src/pendulum/datetime.py:
def diff_for_humans(self, other=None, absolute=False, locale=None) -> str: is_now = other is None if is_now: other = self.now() diff = self.diff(other) return pendulum.format_diff(diff, is_now, absolute, locale)核心逻辑委托给pendulum.format_diff(),最终由DifferenceFormatter(src/pendulum/formatting/difference_formatter.py)完成文案生成。is_now标志决定使用ago / from now还是before / after分支;方向由 Interval 的invert属性(差值为负时为真)决定使用未来(future)还是过去(past)的翻译键。
Date与Time类型同样提供diff()与diff_for_humans(),实现分别位于 src/pendulum/date.py 与 src/pendulum/time.py,未传参时默认与today()/ 当前时间比较。
四、本地化:全局与单次调用
4.1 两种设置方式
差值文案的本地化支持两种方式:全局设置(在调用diff_for_humans()之前调用pendulum.set_locale('fr'))或单次调用传参(通过locale关键字参数)。后者优先级更高,且不会影响全局状态:
>>> import pendulum >>> pendulum.set_locale('de') >>> pendulum.now().add(years=1).diff_for_humans() 'in 1 Jahr' >>> pendulum.now().add(years=1).diff_for_humans(locale='fr') 'dans 1 an'示例中全局 locale 为德语(de),但单次调用指定locale='fr'后输出法语,二者互不干扰。完整的本地化机制与可用语言列表可参考 Localization 文档;仓库中 src/pendulum/locales 目录下按语言(de、fr、zh、ja等)组织 locale 数据,DifferenceFormatter通过Locale.load(locale)加载对应语言包,并利用 CLDR(Unicode 通用语言环境数据仓库)中的translations.relative与translations.units数据进行翻译与复数规则匹配。
4.2 近似取整规则(从源码看文案逻辑)
diff_for_humans()的输出不是简单的单位换算,而是遵循DifferenceFormatter.format()中定义的阈值近似规则(src/pendulum/formatting/difference_formatter.py):
| 阈值常量 | 值 | 含义 |
|---|---|---|
DAYS_THRESHOLD_FOR_HALF_WEEK | 3 | 剩余天数 > 3 天时,周数 +1(约半周) |
DAYS_THRESHOLD_FOR_HALF_MONTH | 15 | 剩余天数 > 15 天时,月数 +1 |
MONTHS_THRESHOLD_FOR_HALF_YEAR | 6 | 剩余月数 > 6 个月时,年数 +1 |
HOURS_IN_NEARLY_A_DAY | 22 | 小时 ≥ 22 时,天数 +1(接近一天) |
DAYS_IN_NEARLY_A_MONTH | 27 | 折算天数 ≥ 27 时,月数 +1(接近一月) |
MONTHS_IN_NEARLY_A_YEAR | 11 | 11 个月 + 足够天数时进位为 1 年 |
FEW_SECONDS_MAX | 10 | 剩余秒数 ≤ 10 时优先使用 "a few seconds" 等自定义单位 |
这解释了"24 天 → 3 weeks":24 天 = 3 周 + 3 天剩余,未超过半周阈值(3 天),因此保持 3 周。格式化同时会依据 locale 的复数规则(locale.plural(count))选择单复数形式,例如英语的 "1 day ago" 与 "2 days ago"。
五、实际应用与注意事项
5.1 典型业务场景
- 评论/帖子时间戳:
pendulum.now().subtract(days=1).diff_for_humans()输出 "1 day ago",是社交产品最典型的用法; - 事件方向提示:
dt.diff_for_humans(other)输出 "before / after" 指明先后关系; - 无修饰的纯时长:
diff_for_humans(absolute=True)用于表格、标签等不强调方向的界面; - 精确计算:需要精确时长时使用
diff().in_*(),它严格截断,适合倒计时、统计等场景。
5.2 边界与陷阱
- 时区参与计算:
diff()对带时区的实例执行的是真实时刻差,跨时区比较会自动换算(如本文开篇渥太华与温哥华的 3 小时示例);Interval.__new__还会对 offset-naive 与 offset-aware 混用的情况抛出TypeError(见 src/pendulum/interval.py)。 - 截断非取整:
in_minutes()对 119 秒返回 1、in_days()对 1 天 13 小时返回 1,这是文档与测试共同确认的行为,做倒计时逻辑时需自行处理余数。 - 月份差异依赖日历:
add(months=1)后的实际天数随起止月份变化(1 月末加 1 月是 29 天,4 月末加 1 月是 30 天),不要假设固定 30 天/月。 - 人性化文案是近似值:
diff_for_humans()的输出经过阈值四舍五入式的折叠(如 24 天显示为 3 周),适合展示而不适合精确计算。
仓库中 tests/datetime/test_diff.py 与 tests/date/test_diff.py 提供了大量可直接对照的断言用例,可作为理解截断、符号与文案规则的"活文档"。
六、小结
Pendulum 的差值体系由三层构成:diff()负责产出携带端点与方向的Interval;in_*系列负责把总时长严格截断到目标单位;diff_for_humans()负责把差值折叠为符合人类阅读习惯、且支持复数规则与多语言本地化的自然语言文案。掌握abs(正负号)、absolute(去修饰语)与locale(本地化)三个开关,以及"截断不取整""近似折叠"两条核心规则,即可在真实业务中正确、优雅地处理所有时间差需求。
- 后端
【免费下载链接】pendulum
Python datetimes made easy
相关推荐
Pendulum时间差计算终极指南:从秒到年的精确时间间隔管理
Pendulum时间差计算终极指南:从秒到年的精确时间间隔管理 Pendulum是Python中一个强大的日期时间处理库,专门为解决标准datetime模块在处
后端PlayIntegrityFix社区贡献指南:如何参与项目开发和问题解决
PlayIntegrityFix社区贡献指南:如何参与项目开发和问题解决 PlayIntegrityFix是一个开源的Android Magisk模块,专门用于
Perfetto Data Explorer Interval Intersect 节点:多源时间区间交集计算实战指南
Perfetto Data Explorer Interval Intersect 节点:多源时间区间交集计算实战指南 Interval Intersect 是
可观测性后端开发工具前端数据可视化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考