☰
Pendulum Duration 深度指南:超越 timedelta 的时间差对象
2026/10/7 16:24:15 网站建设 项目流程
  • 后端

【免费下载链接】pendulum

Python datetimes made easy

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

导读

Duration是 Pendulum 中用于表达时间差(time difference)的核心类,它继承自 Python 原生datetime.timedelta,但在此基础上实现了"直观归一化"(intuitive normalization)、年/月单位支持、丰富的属性分解与多语言文字化输出等能力。本文以 docs/docs/duration.md 为主线,结合 src/pendulum/duration.py 源码与 tests/duration/ 测试用例,完整讲解 Duration 的创建、属性语义、单位换算方法与本地化输出,帮助你在实际项目中安全、精准地使用 Pendulum 处理时间差。

一、Duration 是什么:继承 timedelta 的增强时间差类

在 Python 标准库中,两个datetime对象相减会得到一个timedelta,它只有days、seconds、microseconds三个字段,且会自动进行进位归一化。Pendulum 的Duration类直接继承自timedelta(见 duration.py),却在行为上做了重要改进。

1.1 直观归一化:不再自动进位

原生timedelta会把负的时间差统一表示为"负天数 + 正秒数"的形式,阅读起来很不直观:

>>> import pendulum >>> import datetime >>> d1 = datetime.datetime(2012, 1, 1, 1, 2, 3, tzinfo=datetime.UTC) >>> d2 = datetime.datetime(2011, 12, 31, 22, 2, 3, tzinfo=datetime.UTC) >>> delta = d2 - d1 >>> delta.days -1 >>> delta.seconds 75600

而 Pendulum 会保留每个单位的符号,保持"各分量直接相减"的直觉语义:

>>> d1 = pendulum.datetime(2012, 1, 1, 1, 2, 3) >>> d2 = pendulum.datetime(2011, 12, 31, 22, 2, 3) >>> delta = d2 - d1 >>> delta.days 0 >>> delta.hours -3

从源码看,这一逻辑在Duration.__new__中完成:先把所有输入折算为秒,再分别拆出_microseconds、_seconds、_days、_remaining_days、_weeks等内部字段(duration.py),负值通过符号位m保持各分量方向一致。

二、实例化:pendulum.duration()帮助函数

创建Duration实例最直接的方式是使用pendulum.duration()帮助函数(定义于 src/pendulum/init.py):

>>> import pendulum >>> it = pendulum.duration(days=1177, seconds=7284, microseconds=1234)

2.1 完整的构造参数

Duration.__new__与duration()帮助函数接受的参数完全一致,所有参数默认值为0,且顺序无关,全部以关键字形式传入:

参数含义默认值
days天数0
seconds秒数0
microseconds微秒数0
milliseconds毫秒数0
minutes分钟数0
hours小时数0
weeks周数0
years年数(必须为整数)0
months月数(必须为整数)0

其中milliseconds会被换算并合并进微秒:源码在记录_signature时使用microseconds + milliseconds * 1000(duration.py),与原生timedelta的换算规则一致。

2.2 支持 years 与 months:timedelta 不具备的能力

原生timedelta无法直接表达"2 年 3 个月",而Duration可以:

>>> import pendulum >>> it = pendulum.duration(years=2, months=3)

注意两个限制(均有源码与测试佐证):

  • years与months必须是整数,传入浮点数会抛出ValueError("Float year and months are not supported")(duration.py),对应测试见 test_construct.py;
  • 由于timedelta内部没有年/月字段,Duration采用固定近似:1 年 = 365 天,1 个月 = 30 天(见 duration.py 中的days + years * 365 + months * 30)。

因此,为了维持与原生类的兼容,days、total_seconds()等原生属性/方法在包含年/月时会使用上述近似值:

>>> it.days 820 >>> it.total_seconds() 70848000.0

该近似行为同样被测试锁定,例如duration(years=2).days == 730、duration(months=3).days == 90(test_construct.py)。

三、属性体系:细粒度分解每个时间单位

原生timedelta只有days、seconds、microseconds三个属性,Duration则提供了一套完整的分解属性。以下沿用文档示例:

>>> import pendulum >>> it = pendulum.duration( ... years=2, months=3, ... days=1177, seconds=7284, microseconds=1234 ... ) >>> it.years 2 >>> it.months 3

3.1 天与周的分解

# Weeks 基于总天数计算,不计入 years 和 months >>> it.weeks 168 # days 与 timedelta 语义一致,表示时长内的总天数; # 若指定了 years 和/或 months,则使用近似值 >>> it.days 1997 # 不足一整周的天数 >>> it.remaining_days 1

对应源码中的拆分逻辑为_weeks = abs(_days) // 7、_remaining_days = abs(_days) % 7(duration.py),即days是"年×365 + 月×30 + 实际天数"的累计总量,而weeks/remaining_days只基于实际传入的天数部分拆分。

3.2 时、分、秒与微秒

>>> # 各单位的剩余值 >>> it.hours 2 >>> it.minutes 1 # seconds 与 days 一样属于特殊情形: # 为兼容 timedelta,默认属性返回剩余秒数的整体值 >>> it.seconds 7284 # 若想获取未计入小时和分钟的秒数 >>> it.remaining_seconds 24 >>> it.microseconds 1234

从实现看,hours与minutes由_seconds逐级取模而来(abs(seconds) // 3600 % 24、abs(seconds) // 60 % 60,见 duration.py),并通过缓存字段_h、_i避免重复计算;remaining_seconds则是abs(seconds) % 60(duration.py)。上述属性都对应了测试 test_construct.py 中的断言。

四、单位换算:total_xxx()与in_xxx()

当需要把时长整体换算成某个单位时,Duration提供两组方法。

4.1total_xxx():返回浮点数

与原生total_seconds()返回浮点数的语义一致,每个total_xxx()都返回该单位下的完整时长(保留小数):

>>> it.total_weeks() 168.15490079569113 >>> it.total_days() 1177.0843055698379 >>> it.total_hours() 28250.02333367611 >>> it.total_minutes() 1695001.4000205665 >>> it.total_seconds() 101700084.001234

源码实现非常直白:全部基于total_seconds()除以对应常量折算,常量定义于 constants.py(SECONDS_PER_MINUTE、SECONDS_PER_HOUR、SECONDS_PER_DAY),例如total_hours()即self.total_seconds() / SECONDS_PER_HOUR(duration.py)。

4.2in_xxx():返回截断整数

in_xxx()系列则对total_xxx()的结果做int()截断,返回整数:

>>> it.in_weeks() 168 >>> it.in_days() 1997 >>> it.in_hours() 28250 >>> it.in_minutes() 1695001 >>> it.in_seconds() 101700084

实现上即int(self.total_weeks())等一行转换(duration.py),相关断言见 tests/duration/test_in_methods.py。

五、文字化输出:in_words()与多语言支持

in_words()是Duration最实用的方法之一,它决定时长被打印时的呈现形式,并且内置多语言本地化支持。其签名与核心逻辑在 duration.py:

def in_words(self, locale: str | None = None, separator: str = " ") -> str
  • locale:指定语言环境,默认使用pendulum.get_locale()的当前 locale;
  • separator:各单位之间的分隔符,默认单个空格。
>>> import pendulum >>> pendulum.set_locale('fr') >>> it = pendulum.duration(days=1177, seconds=7284, microseconds=1234) >>> it.in_words() '168 semaines 1 jour 2 heures 1 minute 24 secondes' >>> print(it) '168 semaines 1 jour 2 heures 1 minute 24 secondes' >>> it.in_words(locale='de') '168 Wochen 1 Tag 2 Stunden 1 Minute 24 Sekunden'

5.1 实现要点

  • 输出单位顺序固定为:year → month → week → day(remaining_days)→ hour → minute → second(remaining_seconds),对应 duration.py 的intervals列表;
  • 复数处理:通过loaded_locale.plural(count)选择单复数词形,再用translation.format(interval_count)填充数字;
  • __str__复用:Duration.__str__直接返回self.in_words()(duration.py),所以print(it)与it.in_words()结果一致;
  • 零值兜底:当所有单位均为 0 时,若存在微秒则输出"0.12 seconds"这类亚秒形式,否则输出"0 microseconds"(对应测试 test_in_words.py);
  • __repr__则输出结构化的构造参数形式,如Duration(years=2, months=3, weeks=168, days=1, hours=2, minutes=1, seconds=25)(duration.py,测试见 test_in_words.py)。

5.2 分隔符与负值

>>> pendulum.duration(days=1177, seconds=7284, microseconds=1000000) >>> pi.in_words(separator=", ") '168 weeks, 1 day, 2 hours, 1 minute, 25 seconds' >>> pendulum.duration(days=-1).in_words() '-1 day'

多语言词形取自 src/pendulum/locales/ 下各语言包的units翻译表,测试覆盖了法文等场景(test_in_words.py)。

六、进阶能力:算术运算、转换与类常量

6.1 算术运算

Duration重载了原生timedelta的算术协议(duration.py):

  • 加减:__add__/__sub__支持与任意timedelta(含Duration)运算,结果基于total_seconds()重新构造;
  • 乘法:__mul__/__rmul__支持整数与浮点数;整数乘法会保留 years/months 并按比例放大,浮点乘法内部用_divide_and_round做银行家舍入;
  • 除法:__truediv__、__floordiv__、__mod__、__divmod__同样可用,除以timedelta返回倍数,除以标量返回新的Duration;
  • 取负:__neg__对各分量取反,负值时长可通过invert属性判断(total_seconds() < 0)。

对应测试见 tests/duration/test_arithmetic.py(乘法、除法、整除的年份/月份保留行为)与 test_construct.py(invert判定)。

6.2 与原生类型互转

>>> pi = pendulum.duration(seconds=3456.123456) >>> delta = pi.as_timedelta() # 返回原生 datetime.timedelta >>> isinstance(delta, timedelta) True >>> delta.total_seconds() 3456.123456

as_timedelta()通过timedelta(seconds=self.total_seconds())完成转换(duration.py),测试见 test_construct.py。

6.3 类常量与AbsoluteDuration

Duration还定义了与timedelta对齐的类常量(duration.py):

Duration.min # Duration(days=-999999999) Duration.max # Duration(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999) Duration.resolution # Duration(microseconds=1)

此外同文件还定义了内部使用的AbsoluteDuration子类(duration.py),它把所有分量归一为绝对值表达,total_seconds()恒为非负,并额外提供invert标记原始方向,用于 Pendulum 内部(如Interval的绝对差值)表达。

七、实践建议与注意事项

  1. 优先用pendulum.duration()帮助函数创建实例,它透传全部 9 个关键字参数,可读性最好;
  2. 年份/月份是近似值:1 年按 365 天、1 月按 30 天折算。涉及days、total_seconds()等原生语义时务必记住这一点,跨时区或跨闰年的精确日历差值请改用Interval(见 docs/docs/interval.md);
  3. 区分days与remaining_days、seconds与remaining_seconds:前者是与timedelta兼容的累计/整体值,后者才是"扣除更大单位后的余量",这是 Duration 属性体系中最容易混淆的一对概念;
  4. in_xxx()是截断而非四舍五入:int(total_xxx())直接丢弃小数部分,需要四舍五入时请自行使用round();
  5. 多语言输出直接依赖pendulum.set_locale()或in_words(locale=...)参数,中文可传'zh',词形数据位于 src/pendulum/locales/zh/;
  6. print()即in_words():__str__委托给in_words(),因此直接打印对象即可获得本地化的自然语言时长描述。

结语

Duration是 Pendulum 时间体系的基石之一:它以继承timedelta的方式保持生态兼容,又以直观归一化、年/月支持和细粒度属性分解补足了原生类的短板,配合in_words()的多语言输出能力,让"计算时间差并呈现给人看"这件事变得简单而可靠。无论是编写调度逻辑、统计耗时,还是生成人类可读的时长文案,Duration都值得作为你的默认选择。

  • 后端

【免费下载链接】pendulum

Python datetimes made easy

项目地址:https://gitcode.com/gh_mirrors/pe/pendulum
点击查看免费下载
上一篇:Cilium 的 `cilium-dbg map events` 实战指南:深入剖析 BPF Map 事件缓冲与事件流
下一篇:基于 Zeek dns.log 的 DNS 数据外泄检测实战:熵分析、长标签与查询量异常识别

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

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

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

立即咨询