pytz 时区在夏令时期间有错误的偏移量

pytz timezone has the wrong offset during daylight saving time

我对 pytz 和夏令时有疑问。当我使用时区 Europe/Berlin 时,它总是使用不带 DST 的时区偏移量。

最小示例:

print(repr(pytz.timezone("Europe/Berlin")))
<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>
# Should probably be something like <DstTzInfo 'Europe/Berlin' CET+2:00:00 DST>

# Usage
from django.utils import timezone
from datetime import datetime
datetime_now = timezone.now()
print(my_time)
# Result: 00:00:00
print(datetime.combine(datetime_now, my_time, tzinfo=timezone.get_current_timezone()))
# Result: 2020-04-04 00:00:00+01:00, should be 2020-04-04 00:00:00+02:00

我的用例的最小示例是闹钟。用户将时钟设置为 06:00(不考虑时区),时钟应在当前时区的 06:00 响铃,即 06:00+02 时为 DST,否则为 06:00+01 Europe/Berlin.


该实现是一个 Django 模型,使用 django.models.TimeField 作为非感知时间(例如 06:00),我想通过创建一个 datetime 对象将它与当前时间和其他 TimeFields 进行比较当前日期和时间存储在 TimeField.

我愿意接受关于时间对象的不同建议(例如使用或不使用 django.utils.timezone),只要我可以创建日期时间对象,我可以相互比较并使用 timedelta 对象递增/递减(或一些类似的方法)。


另一个最小的例子(Django 仅用于获取当前时区):

from django.utils import timezone
import datetime

tz = timezone.get_current_timezone()
time_now = datetime.datetime.now(tz=tz)
clock_time = datetime.time(1,2)
combined_time = datetime.combine(time_now, clock_time, tzinfo=tz)
print(tz)
print(time)
print(time_now)
print(combined_time)

结果

Europe/Berlin
01:02:00
2020-04-12 18:50:11.934754+02:00
2020-04-12 01:02:00+01:00

在构建时区感知日期时间时避免使用 tzinfo。参见 this post

由于您使用的是 Django,假设 TIME_ZONE = 'Europe/Berlin',我们可以使用 make_aware:

from django.utils import timezone
from datetime import datetime, time

# Get a localized datetime so that .combine gets the local date
local_now = timezone.localtime()
# localtime() is a shortcut for
# timezone.now().astimezone(timezone.get_current_timezone())

clock_time = time(1, 2)
combined_time = timezone.make_aware(datetime.combine(local_now, clock_time))
print(combined_time)

它将打印

2020-04-21 01:02:00+02:00

或者,无论如何都使用 localize function in pytz (which is used in the make_aware function definition,但请查看下面的详细信息):

tz = timezone.get_current_timezone()  # or pytz.timezone('Europe/Berlin')
combined_time = tz.localize(datetime.combine(local_now, clock_time))
# 2020-04-21 01:02:00+02:00

如果你看到 Django code for timezone.py,这些函数基本上是 pytz 包装器。特别是检查 make_awarelocaltimenow.

的定义

不过 make_awarelocalize 之间有一个特别的区别。两者都接受参数 is_dst,但对于 Django 的 make_aware 默认情况下是 None,而它是 False for pytz. This difference matters in your case if a user writes a time that doesn't exist, or happens twice, when entering DST. Here, having is_dst=None will make the function raise NonExistentTimeError or AmbiguousTimeError, respectively. Otherwise, a boolean value will cause it to guess.


示例: 今年 Europe/Berlin,时钟在 3 月 29 日 2:00 上午拨快了一小时。因此,2:30am 不是当地时间。 Python 根据 is_dst:

处理此输入
time_doesnt_exist = datetime(2020, 3, 29, 2, 30, 0)
print(tz.localize(time_doesnt_exist, is_dst=None))
# Raises NonExistentTimeError
print(tz.localize(time_doesnt_exist, is_dst=True))
2020-03-29 02:30:00+02:00
print(tz.localize(time_doesnt_exist, is_dst=False))
2020-03-29 02:30:00+01:00

要使用 localize 获得引发异常的行为:

combined_time = tz.localize(datetime.combine(local_now, clock_time), is_dst=None)

要使 make_aware 不加注:

combined_time = timezone.make_aware(
  datetime.combine(local_now, clock_time),
  is_dst=False,  # Or True...
)

注意事项:本地化时间的算术

对本地化日期时间进行算术运算需要 calling normalize 作为 DST 问题出现时的解决方法

time_before_dst = datetime(2020, 3, 29, 1, 50, 0)
local_time_before_dst = tz.localize(time_before_dst) 
new_time = local_time_before_dst + timedelta(minutes=40)
print(new_time)
# 2020-03-29 02:30:00+01:00
# Didn't switch to DST!
print(tz.normalize(new_time))
# 2020-03-29 03:30:00+02:00
# Correctly did the switch