- 后端
【免费下载链接】pendulum
Python datetimes made easy
导读
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 33.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 = " ") -> strlocale:指定语言环境,默认使用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.123456as_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的绝对差值)表达。
七、实践建议与注意事项
- 优先用
pendulum.duration()帮助函数创建实例,它透传全部 9 个关键字参数,可读性最好; - 年份/月份是近似值:1 年按 365 天、1 月按 30 天折算。涉及
days、total_seconds()等原生语义时务必记住这一点,跨时区或跨闰年的精确日历差值请改用Interval(见 docs/docs/interval.md); - 区分
days与remaining_days、seconds与remaining_seconds:前者是与timedelta兼容的累计/整体值,后者才是"扣除更大单位后的余量",这是 Duration 属性体系中最容易混淆的一对概念; in_xxx()是截断而非四舍五入:int(total_xxx())直接丢弃小数部分,需要四舍五入时请自行使用round();- 多语言输出直接依赖
pendulum.set_locale()或in_words(locale=...)参数,中文可传'zh',词形数据位于 src/pendulum/locales/zh/; print()即in_words():__str__委托给in_words(),因此直接打印对象即可获得本地化的自然语言时长描述。
结语
Duration是 Pendulum 时间体系的基石之一:它以继承timedelta的方式保持生态兼容,又以直观归一化、年/月支持和细粒度属性分解补足了原生类的短板,配合in_words()的多语言输出能力,让"计算时间差并呈现给人看"这件事变得简单而可靠。无论是编写调度逻辑、统计耗时,还是生成人类可读的时长文案,Duration都值得作为你的默认选择。
- 后端
【免费下载链接】pendulum
Python datetimes made easy
相关推荐
Jenkins Job DSL安全最佳实践:保护你的CI/CD管道
Jenkins Job DSL安全最佳实践:保护你的CI/CD管道 Jenkins Job DSL是一种基于Groovy的领域特定语言,用于以编程方式定义Jen
Pendulum Duration类详解:精确时间间隔计算的终极指南
Pendulum Duration类详解:精确时间间隔计算的终极指南 在Python日期时间处理中,Pendulum Duration类提供了比标准库更强大和直
后端Pendulum 属性与特性详解:超越标准 datetime 的日期时间属性
Pendulum 属性与特性详解:超越标准 datetime 的日期时间属性 Pendulum 在标准库 datetime 基础上提供了一组更丰富的属性与特性,
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考