如何使用 datetime 模块对时区进行运算?tzinfo,timezone 类介绍
关注 1800
时区
按照正式的方式,世界被划分为 24 个时区,零时区的时间与格林威治时区的时间可被视为相同,其余时区与相邻的时区之间的时间差值为1个小时。
datetime 模块的感知型和单纯型日期时间对象
如果datetime模块的time或datetime对象包含有效的时区信息,那么这些对象被称为感知型的日期时间对象,相反的,如果time或datetime对象不包含有效的时区信息,那么这些对象被称为单纯型日期时间对象。
因为拥有时区信息,感知型的time或datetime对象一般不会产生歧义,而单纯型的time或datetime对象所表示的日期时间的含义需要由开发人员或用户自行决定(一般会作为本地时间使用)。
上述“有效的时区信息”是指time或datetime对象的utcoffset方法没有返回空值None(允许utcoffset方法返回等价于timedelta()的timedelta对象)。
datetime 模块的 tzinfo,timezone 类
datetime模块的tzinfo类,用于表示时区信息,该类是一个抽象基类,因此不能直接创建tzinfo类的实例。
由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对象,你可以查看datetime 模块的 datetime 类,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).获取协调世界零时区
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)获取某个日期时间在夏令时中的时间差值
抽象基类tzinfo的dst方法,可用于计算某个日期时间在tzinfo所表示的夏令时中的时间差值,该方法通常被datetime和time对象的dst方法调用,也就是计算datetime或time所表示的日期时间在夏令时中的时间差值。因此,抽象基类tzinfo的派生类应在方法dst中实现以下功能,判断日期时间是否在夏令时的范围内,如果属于夏令时则返回日期时间在夏令时中的时间差值,如果未启用夏令时或不属于夏令时则应返回空值None或等价于timedelta()的timedelta对象。
tzinfo.dst(dt)
- dt 参数
dt参数是一个datetime对象,将计算该对象所表示的日期时间在夏令时中的时间差值。
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方法。
造成上述差别的原因可能在于,夏令时的确定需要日期信息,而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获取某个日期时间与协调世界零时区之间的时间差值
抽象基类tzinfo的utcoffset方法,可用于计算某个日期时间与协调世界零时区之间的时间差值,这应包括受夏令时影响而产生的时间差值(因此,抽象基类tzinfo的派生类应在方法utcoffset中调用其自身的方法dst),该方法通常被datetime和time对象的utcoffset方法调用,也就是计算datetime或time所表示的日期时间与协调世界零时区之间的时间差值。
需要说明的是,抽象基类tzinfo的utcoffset方法,只需根据自身的时区信息即可确定与协调世界零时区之间的时间差值,参数dt主要用于方法dst的调用,以确定日期时间在夏令时中的时间差值,两个时间差值相加(相加时最好判断时间差值是否为空值None)的结果即为utcoffset方法应该返回的值。
tzinfo.utcoffset(dt)
- dt 参数
dt参数是一个datetime对象,将计算该对象所表示的日期时间与协调世界零时区之间的时间差值。
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获取某个日期时间的时区名称
抽象基类tzinfo的tzname方法,可用于获取某个日期时间的时区名称,该方法通常被的datetime和time对象的tzname方法调用,也就是返回datetime或time所表示的日期时间的时区名称。
需要说明的是,抽象基类tzinfo的tzname方法,并不需要根据日期时间来返回时区的名称,其参数dt多被用于返回夏令时中的特殊时区名称。
tzinfo.tzname(dt)
- dt 参数
dt参数是一个datetime对象,将返回该对象所表示的日期时间的时区名称。
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为日期时间转换时区
大多数情况下,抽象基类tzinfo的fromutc方法不会被开发人员直接使用,而是由datetime对象的astimezone方法来调用,后者可以转换日期时间的时区(返回新的datetime对象),转换时区并不是简单的替换datetime对象的时区信息,而是会计算出原有日期时间在新时区中的值。
另外,抽象基类tzinfo的fromutc方法不是抽象的,因此,tzinfo类的派生类并不需要重写该方法。在调用tzinfo对象的fromutc方法时,其参数dt被视为零时区的日期时间,并被转换为tzinfo对象所表示的时区的日期时间(返回新的datetime对象,并包括了夏令时带来的影响)。需要指出,参数dt的tzinfo属性必须是tzinfo对象自身,否则将导致异常。
tzinfo.fromutc(dt)
- dt 参数
dt参数是一个datetime对象,该对象被视为零时区的日期时间,并被转换为tzinfo对象所表示时区的日期时间。
至于datetime对象的astimezone方法,如果忽略参数tz,那么日期时间将被转换为操作系统所指定的时区的日期时间。
datetime.astimezone(tz=None)
- tz 参数
tz参数是一个tzinfo对象,日期时间的时区将转换为该对象所表示的时区。
datetime 模块
如果你仅希望简单的替换datetime或time对象中的时区信息,那么可以查看修改日期时间一段。
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 # 直接调用 fromutctzinfo 时区对象对日期时间运算的影响
在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对于感知型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对于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)))