如何使用 Python datetime 模块对时区进行运算?tzinfo,timezone 类介绍
时区
按照正式的方式,世界被划分为 24 个时区,零时区的时间与格林威治时区的时间可被视为相同,其余时区与相邻的时区之间的时间差值为1
个小时。
Python datetime 模块的感知型和单纯型日期时间对象
如果 Pythondatetime
模块的time
或datetime
对象包含有效的时区信息,那么这些对象被称为感知型的日期时间对象,相反的,如果time
或datetime
对象不包含有效的时区信息,那么这些对象被称为单纯型日期时间对象。
因为拥有时区信息,感知型的time
或datetime
对象一般不会产生歧义,而单纯型的time
或datetime
对象所表示的日期时间的含义需要由开发人员或用户自行决定(一般会作为本地时间使用)。
上述“有效的时区信息”是指time
或datetime
对象的utcoffset
方法没有返回空值None
(允许utcoffset
方法返回等价于timedelta()
的timedelta
对象)。
Python datetime 模块的 tzinfo,timezone 类
Pythondatetime
模块的tzinfo
类,用于表示时区信息,该类是一个抽象基类,因此不能直接创建tzinfo
类的实例。
由 Pythondatetime
模块的tzinfo
类的派生类所产生的实例,一般会作为创建datetime
或time
对象时所需的参数tzinfo
,并为datetime
或time
对象提供与时区相关的操作。
当然,datetime
模块提供了一个实现了tzinfo
的类timezone
,开发人员可使用timezone
来创建时区信息(timezone
不提供某些特殊运算,比如夏令时),其构造器如下。
timezone(offset, name='UTC')
- offset 参数
offset
参数是一个timedelta
对象,表示了timezone
对象对应的时区与协调世界零时区之间的时间差值,该差值不能大于或等于表达式timedelta(hours=24)
所表示的时间差值,也不能小于或等于表达式-timedelta(hours=24)
所表示的时间差值,否则将引发异常ValueError
。- name 参数
name
参数是一个表示时区名称的字符串(默认值为'UTC'
),他不能被设置为空值None
。
datetime 模块
关于如何创建datetime
和time
对象,你可以查看Python datetime 模块的 datetime 类,Python datetime 模块的 time 类两段。
from datetime import timezone, timedelta
# 创建时区对象
timezone(timedelta(days=0.9999), 'My Zone')
datetime.timezone(datetime.timedelta(seconds=86391, microseconds=360000), 'My Zone')
# 时间差值大于或等于 24 小时将导致错误
timezone(timedelta(minutes=1440))
…
ValueError: offset must be a timedelta strictly between -timedelta(hours=24) and timedelta(hours=24), not datetime.timedelta(days=1).
使用 Python datetime 模块的 timezone 对象获取协调世界零时区
Pythondatetime
模块的timezone
对象的utc
变量,以及datetime
模块的UTC
变量,是一个表示协调世界零时区的timezone
对象,其效果等同于使用表达式timezone(timedelta())
。
timezone.utc
datetime.UTC
from datetime import time, timezone, timedelta
time(tzinfo=timezone.utc)
datetime.time(0, 0, tzinfo=datetime.timezone.utc)
time(tzinfo=timezone(timedelta()))
datetime.time(0, 0, tzinfo=datetime.timezone.utc)
time(tzinfo=UTC)
datetime.time(0, 0, tzinfo=datetime.timezone.utc)
通过 Python datetime 模块的 tzinfo 对象获取某个日期时间在夏令时中的时间差值
Pythondatetime
模块的抽象基类tzinfo
的dst
方法,可用于计算某个日期时间在tzinfo
所表示的夏令时中的时间差值,该方法通常被datetime
模块的datetime
和time
对象的dst
方法调用,也就是计算datetime
或time
所表示的日期时间在夏令时中的时间差值。因此,抽象基类tzinfo
的派生类应在方法dst
中实现以下功能,判断日期时间是否在夏令时的范围内,如果属于夏令时则返回日期时间在夏令时中的时间差值,如果未启用夏令时或不属于夏令时则应返回空值None
或等价于timedelta()
的timedelta
对象。
tzinfo.dst(dt)
- dt 参数
dt
参数是一个datetime
对象,将计算该对象所表示的日期时间在夏令时中的时间差值。
Pythondatetime
模块的time
和datetime
对象同样拥有名称为dst
的方法,同样可计算日期时间在夏令时中的时间差值,其中datetime
对象的dst
方法会返回表达式self.tzinfo.dst(self)
的运算结果(如果datetime
对象的tzinfo
属性为None
,则dst
方法直接返回None
),即datetime
对象会将自身传递给其所包含的tzinfo
对象的dst
方法,而time
对象的dst
方法,会返回表达式self.tzinfo.dst(None)
的运算结果(如果time
对象的tzinfo
属性为None
,则dst
方法直接返回None
),即time
对象会将空值None
传递给其所包含的tzinfo
对象的dst
方法。
造成上述差别的原因可能在于,夏令时的确定需要日期信息,而 Pythondatetime
模块的time
对象只包含时间信息,因此传递空值None
到tzinfo
对象的dst
方法是不难理解的。
time|datetime.dst()
什么是夏令时?
从效果上来说,夏令时可以被视为一些日期时间段,处于这些日期时间段的日期时间的时区会被改变,以让人们适当的调整作息时间。
在下面的示例中,我们定了自己的时区类CustomDST
,该类会将一年中的 6,7,8 三个月份视为夏令时,对于任何的time
对象,CustomDST
会返回timedelta(minutes=15)
作为其在夏令时中的时间差值。
from datetime import time, datetime, timedelta, tzinfo
# 自定义时区 CustomDST
class CustomDST(tzinfo):
# 返回日期时间在夏令时中的时间差值
def dst(self, dt: datetime):
# 当调用 time 对象的 dst 方法时,dt 为 None
if not dt:
# 这里应该返回 None 或 timedelta(),但我们尝试返回其他值
return timedelta(minutes=15)
# 时区 CustomDST 将 6,7,8 月份视为夏令时
if dt.month >= 6 and dt.month <= 8:
# 夏令时的时间差值为 1 小时
return timedelta(hours=1)
else:
return timedelta()
cz = CustomDST()
# 6 月 1 日属于夏令时
print(cz.dst(datetime(2024, 6, 1)))
# 9 月 1 日不属于夏令时
print(datetime(2024, 9, 1, tzinfo=cz).dst())
# 时区 CustomDST 会为 time 对象返回 timedelta(minutes=15)
print(time().dst(), time(tzinfo=cz).dst())
1:00:00 # 夏令时
0:00:00
None 0:15:00
通过 Python datetime 模块的 tzinfo 对象获取某个日期时间与协调世界零时区之间的时间差值
Pythondatetime
模块的抽象基类tzinfo
的utcoffset
方法,可用于计算某个日期时间与协调世界零时区之间的时间差值,这应包括受夏令时影响而产生的时间差值(因此,抽象基类tzinfo
的派生类应在方法utcoffset
中调用其自身的方法dst
),该方法通常被datetime
模块的datetime
和time
对象的utcoffset
方法调用,也就是计算datetime
或time
所表示的日期时间与协调世界零时区之间的时间差值。
需要说明的是,Pythondatetime
模块的抽象基类tzinfo
的utcoffset
方法,只需根据自身的时区信息即可确定与协调世界零时区之间的时间差值,参数dt
主要用于方法dst
的调用,以确定日期时间在夏令时中的时间差值,两个时间差值相加(相加时最好判断时间差值是否为空值None
)的结果即为utcoffset
方法应该返回的值。
tzinfo.utcoffset(dt)
- dt 参数
dt
参数是一个datetime
对象,将计算该对象所表示的日期时间与协调世界零时区之间的时间差值。
Pythondatetime
模块的time
和datetime
对象同样拥有名称为utcoffset
的方法,同样可计算日期时间与协调世界零时区之间的时间差值,其中datetime
对象的utcoffset
方法会返回表达式self.tzinfo.utcoffset(self)
的运算结果(如果datetime
对象的tzinfo
属性为None
,则utcoffset
方法直接返回None
),即datetime
对象会将自身传递给其所包含的tzinfo
对象的utcoffset
方法,而time
对象的utcoffset
方法,会返回表达式self.tzinfo.utcoffset(None)
的运算结果(如果time
对象的tzinfo
属性为None
,则utcoffset
方法直接返回None
),即time
对象会将空值None
传递给其所包含的tzinfo
对象的utcoffset
方法。
time|datetime.utcoffset()
在下面的示例中,我们定了自己的时区类CustomUTCOffset
,该类会将全年都视为夏令时,并将小时作为时区的单位,在其utcoffset
方法中,需要判断dst
方法的返回值是否为None
。
from datetime import time, datetime, timedelta, tzinfo
# 自定义时区 CustomUTCOffset
class CustomUTCOffset(tzinfo):
# 初始化自定义时区
def __init__(self, hours):
self.hours = timedelta(hours=hours)
# 计算日期时间与零时区之间的时间差值
def utcoffset(self, dt):
# 需要判断是否为 None,因为 dst 方法可能返回 None
dst = self.dst(dt)
return self.hours + (timedelta(0) if dst is None else dst)
# 全年处于夏令时
def dst(self, dt):
return timedelta(hours=1) if dt else None
# 与零时区相差 5 个小时
cz = CustomUTCOffset(5)
# 时区 CustomUTCOffset 为 datetime 对象返回的夏令时时间差值为 1 小时
print(cz.utcoffset(datetime(2024, 9, 1)))
# 时区 CustomUTCOffset 为 time 对象返回的夏令时时间差值为 None
print(time().utcoffset(), time(tzinfo=cz).utcoffset())
6:00:00 # 包含夏令时时间差值
None 5:00:00
通过 Python datetime 模块的 tzinfo 对象获取某个日期时间的时区名称
Pythondatetime
模块的抽象基类tzinfo
的tzname
方法,可用于获取某个日期时间的时区名称,该方法通常被datetime
模块的datetime
和time
对象的tzname
方法调用,也就是返回datetime
或time
所表示的日期时间的时区名称。
需要说明的是,Pythondatetime
模块的抽象基类tzinfo
的tzname
方法,并不需要根据日期时间来返回时区的名称,其参数dt
多被用于返回夏令时中的特殊时区名称。
tzinfo.tzname(dt)
- dt 参数
dt
参数是一个datetime
对象,将返回该对象所表示的日期时间的时区名称。
Pythondatetime
模块的time
和datetime
对象同样拥有名称为tzname
的方法,同样可获取某个日期时间的时区名称,其中datetime
对象的tzname
方法会返回表达式self.tzinfo.tzname(self)
的运算结果(如果datetime
对象的tzinfo
属性为None
,则tzname
方法直接返回None
),即datetime
对象会将自身传递给其所包含的tzinfo
对象的tzname
方法,而time
对象的tzname
方法,会返回表达式self.tzinfo.tzname(None)
的运算结果(如果time
对象的tzinfo
属性为None
,则tzname
方法直接返回None
),即time
对象会将空值None
传递给其所包含的tzinfo
对象的tzname
方法。
time|datetime.tzname()
在下面的示例中,我们定了自己的时区类CustomTZName
,该类会为time
对象,处于夏令时的datetime
对象,未处于夏令时的datetime
对象返回不同的时区名称。
from datetime import time, datetime, tzinfo
# 自定义时区 CustomTZName
class CustomTZName(tzinfo):
# 初始化自定义时区
def __init__(self, name):
self.name = name
# 返回时区名称
def tzname(self, dt):
if not dt:
# 如果是 time 对象,则返回 TIME 作为时区名称
return 'TIME'
elif dt.month < 6 or dt.month > 8:
# 如果不是夏令时,则返回创建 CustomTZName 对象时指定的时区名称
return self.name
else:
# 如果是夏令时,则追加后缀 DST
return self.name + ' DST'
# 时区名称为 My Zone
cz = CustomTZName('My Zone')
# 时区 CustomTZName 为 datetime 对象返回的非夏令时时区名称为 My Zone
print(cz.tzname(datetime(2024, 9, 1)))
# 时区 CustomTZName 为 datetime 对象返回的夏令时时区名称为 My Zone DST
print(cz.tzname(datetime(2024, 7, 1)))
# 时区 CustomTZName 为 time 对象返回的时区名称为 TIME
print(time().tzname(), time(tzinfo=cz).tzname())
My Zone
My Zone DST # 夏令时时区名称
None TIME
通过 Python datetime 模块的 tzinfo 对象为日期时间转换时区
大多数情况下,Pythondatetime
模块的抽象基类tzinfo
的fromutc
方法不会被开发人员直接使用,而是由datetime
模块的datetime
对象的astimezone
方法来调用,后者可以转换日期时间的时区(返回新的datetime
对象),转换时区并不是简单的替换datetime
对象的时区信息,而是会计算出原有日期时间在新时区中的值。
另外,Pythondatetime
模块的抽象基类tzinfo
的fromutc
方法不是抽象的,因此,tzinfo
类的派生类并不需要重写该方法。在调用tzinfo
对象的fromutc
方法时,其参数dt
被视为零时区的日期时间,并被转换为tzinfo
对象所表示的时区的日期时间(返回新的datetime
对象,并包括了夏令时带来的影响)。需要指出,参数dt
的tzinfo
属性必须是tzinfo
对象自身,否则将导致异常。
tzinfo.fromutc(dt)
- dt 参数
dt
参数是一个datetime
对象,该对象被视为零时区的日期时间,并被转换为tzinfo
对象所表示时区的日期时间。
至于 Pythondatetime
模块的datetime
对象的astimezone
方法,如果忽略参数tz
,那么日期时间将被转换为操作系统所指定的时区的日期时间。
datetime.astimezone(tz=None)
- tz 参数
tz
参数是一个tzinfo
对象,日期时间的时区将转换为该对象所表示的时区。
datetime 模块
如果你仅希望简单的替换datetime
或time
对象中的时区信息,那么可以查看使用 Python datetime 模块修改日期时间一段。
from datetime import datetime, timedelta, tzinfo, timezone
# 自定义时区 MyZone
class MyZone(tzinfo):
# 时区 MyZone 与零时区之间的固定时间差值为 3 小时
def utcoffset(self, dt):
return timedelta(hours=3) + self.dst(dt)
# 时区 MyZone 将 7 月份视为夏令时
def dst(self, dt):
if dt and dt.month == 7:
return timedelta(hours=1)
else:
return timedelta()
# 创建 MyZone 时区
myz = MyZone()
# 将零时区的日期时间转换为 MyZone 时区的日期时间
print(datetime(2024, 2, 1, tzinfo=timezone.utc).astimezone(myz))
# 将零时区的日期时间转换为 MyZone 时区的夏令时日期时间
print(datetime(2024, 7, 1, tzinfo=timezone.utc).astimezone(myz))
# 将零时区的日期时间转换本地日期时间(假设操作系统的时区为 UTC+8)
print(datetime(2024, 3, 1, tzinfo=timezone.utc).astimezone())
# 将本地日期时间(假设操作系统的时区为 UTC+8)转换为 MyZone 时区的日期时间
print(datetime(2024, 5, 1).astimezone(MyZone()))
# 直接调用 fromutc 方法,效果等同于上面的第一个时区转换
print(myz.fromutc(datetime(2024, 2, 1, tzinfo=myz)))
2024-02-01 03:00:00+03:00
2024-07-01 04:00:00+04:00 # 包含夏令时时间差值
2024-03-01 08:00:00+08:00
2024-04-30 19:00:00+03:00
2024-02-01 03:00:00+03:00 # 直接调用 fromutc
Python datetime 模块的时区对象 tzinfo 对日期时间运算的影响
在 Python 的datetime
模块中,简单型的time
和datetime
对象不能与感知型的time
和datetime
对象进行运算,除了判断是否相等(==
和!=
),当然,简单型和感知型并不会相等。
from datetime import time, datetime, timezone
time() < time(tzinfo=timezone.utc)
…
TypeError: can't compare offset-naive and offset-aware times
datetime(2024, 1, 1) - datetime(2024, 1, 1, tzinfo=timezone.utc)
…
TypeError: can't subtract offset-naive and offset-aware datetimes
# 只能判断是否相等
datetime(2024, 1, 1) == datetime(2024, 1, 1, tzinfo=timezone.utc)
False
对于 Pythondatetime
模块中的感知型time
和datetime
对象,时区信息会被运算考虑,其效果类似于将所有日期时间转换为零时区对应的日期时间,然后再进行运算(当然,具体实现方式可能并非如此),这也意味着两个time
或datetime
对象可能是相同的,即便他们拥有不同的时区信息或tzinfo
属性。
from datetime import time, datetime, timezone, timedelta
tz8 = timezone(timedelta(hours=8))
# 零时区的 0 点大于 UTC+8 时区的 0 点
datetime(2024, 1, 1, tzinfo=timezone.utc) > datetime(2024, 1, 1, tzinfo=tz8)
True
# 零时区的 0 点等于 UTC+8 时区的 8 点
datetime(2024, 1, 1, tzinfo=timezone.utc) - datetime(2024, 1, 1, 8, tzinfo=tz8)
datetime.timedelta(0)
# 实际上相同的两个时区
time(tzinfo=timezone.tz8) == time(8, tzinfo=timezone(timedelta(minutes=480)))
True
对于 Pythondatetime
模块中的timedelta
对象与感知型datetime
对象之间的运算,其运算结果的时区信息与原有感知型datetime
对象的时区信息相同,时区信息在运算过程中不会起到任何作用。
from datetime import datetime, timezone, timedelta
# 时区信息不会改变
datetime(2024, 1, 1, tzinfo=timezone(timedelta(hours=8))) + timedelta(hours=11)
datetime.datetime(2024, 1, 1, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=28800)))