学习
实践
活动
专区
工具
TVP
写文章

pytz-西方最快的步枪

每当我谈论时区时,总有人跑来告诉我说他们生产环境的代码挂掉了,因为他们错误理解了pytz的工作方式。这是因为Pytz使用它自己的非标准接口来处理时区信息,这部分信息与Python的日期时间库的工作方式不完全兼容,这导致了许多困惑,很多人天真地使用pytz作为时区提供者。 这种不兼容性解释了为什么从Python 3.6开始,tzinfo文档推荐使用dateutil.tz而不是pytz作为IANA时区提供者。[1]

在这篇文章中,我将介绍这两种时区模型,如果我不能说服你切换到dateutil.tz,至少要提供一些有关pytz和标准时区模型之间差异的知识。

Python的时区模型

在日期时间模块中,Python提供对时区而不是时区偏移量的支持 - 也就是说,datetime.tzinfo对象预计不会提供固定的偏移量和名称,而是提供用于解释时区信息是什么的一组规则作为datetime的函数。 这就是下面这段代码会起作用的原因:

如果NYC是一个静态的固定偏移量,则需要为每个日期时间附加一个不同的tzinfo,具体取决于是否处于标准时间或夏令时,并且只要在对日期时间进行了数学计算,就不得不重新进行计算以防偏移量已经改变。 因此,Python的模型是任何tzinfo子类都应该实现以下三种方法:

tzname(self,dt):给定日期时间偏移量的名称(例如EST,PDT)

utcoffset(self,dt):给定日期时间与UTC的偏移量

dst(self,dt):当前偏移量与区域的“标准偏移量”之差[2]

这些值不仅仅作为一个函数来实现,它们也被懒调用,所以datetime构造函数中没有调用这些值的钩子 - 只有当用户想要知道这些信息中的一个或多个时才会调用它们。

Pytz的时区模型

人们用pytz犯的最大错误就是简单地将它的时区附加到构造函数中,因为这是在Python中为日期时间添加时区的标准方式。 如果你尝试这样做,最好的情况是你会得到明显荒谬的东西:

为什么时间偏移-04:56而不是-05:00? 因为那是在纽约采用标准化时区之前的当地太阳能平均时间,因此是美国/纽约时区的第一个入口。 为什么pytz会返回这种结果? 因为与标准库的懒惰计算时区信息模型不同,Pytz采用了一种立即计算方法。 每当你从一个初始的日期构造一个已知的日期时间,你需要调用它的本地化函数:

每个Pytz时区都包含一个可能的固定偏移“时区”对象列表,这些对象在该时区中的不同时间有效,并且localize函数计算出哪个在当地日期和时间有效并且附加到当前日期。 在这种情况下,它会正确检测到2018-02-14应该是EST偏移-05:00和DST偏移-00:00。 现在,当您在本地化的日期时间执行计算时会发生什么?

由于localize函数急切地将EST附加到日期时间,因此不会响应计算,不会更新偏移量。 为了解决这个错误,每当你在pytz-aware datetime上进行任何算术运算时,你都需要调用normalize函数:

这又一次做了急切的计算,而这本应该由一个dateutil.tz时区对象进行懒计算[3].

模糊的日期时间

既然它与Python的标准时区模型没有很好的匹配,为什么pytz还要以这种方式设计?考虑在夏令时转换期间发生的模糊日期时间的情况,例如, 2018-11-04 01:30-04:00,以及一小时后,2018-11-04 01:30-05:00。 你将如何编写一个函数,该函数采用该日期时间的2018-11-04 01:30部分,并返回正确答案? 不可能,因为有两个正确的答案。

pytz能够解决这个问题,因为在本地化步骤中,时区可以获取有关您是否想要转换为DST或者STD的额外信息:

这是因为有些关于日期时间的信息(它表示的DST的哪一侧)现在被编码在附加到它的tzinfo中。 使用标准的Python接口,直到Python 3.6引入PEP 495才解决了这个问题,PEP 495将fold属性添加到了datetime类中。 这允许“我不确定的日期时间的哪一侧”的决策在日期时间本身进行编码,从而允许延迟计算不明确的日期时间。 如果dt.fold为0,则不明确的日期时间将解析为给定区域中第一次出现的时间,如果它为1,则它们解析为第二次出现。 这里给出代表上面的例子:

此外,dateutil能够通过提供一个tz.enfold函数将它反向移植到早期版本的Python,如果需要,可以创建一个提供fold属性的datetime子类。 所以,在Python 2.7上你会得到:

现在这个问题已经解决了,pytz不但具有处理模糊的日期时间的最佳方式的优点,而且保留了其有点笨拙的界面和急切计算的时区信息的缺点。

西方最快的步枪

这篇文章的标题声称,pytz是西方最快的步枪,我的意思是pytz是一个非常优化的库,历史上它比dateutil.tz更快。 在最近的几次发布中,性能差距已经大大缩小[4],但由于计算的惰性和急切性,在某些用例中性能仍然存在持续差距。 为了演示,我使用IPython的%timeit魔法在python 3.6上定时pytz == 2018.3和python-dateutil == 2.7.0,并且在每个函数之后都将耗费的时间放在注释中:

正如你所看到的,Pytz时区的构造成本更高,并且初始本地化调用比dateutil的utcoffset()调用慢得多,但pytz的utcoffset()调用比dateutil快很多(因为结果被缓存)。 如果计划构建一堆日期时间并相对不频繁地查询其时区信息(平均每个日期时间为2-3次),则看起来dateutil在“首次utcoffset调用的总时间小于pytz:

从一个时区转换到另一个时区的处理都是类似的(尽管pytz比我期望的要好):

dateutil在这个操作中速度较慢,因为它需要计算纽约和洛杉矶的UTC偏移量,而pytz显然从UTC比从最初的日期时间能更快地创建本地化的日期时间(否则会花费至少另一本地化的代价)。 但是,如果您只计划在每个本地化日期时间内只进行一次时区转换,则操作的全部成本为:

在任何情况下,都没有明显的“整体”性能赢家。 如果您计划本地化日期时间,然后重复查询其utcoffset或将其转换为其他时区,则pytz可能会表现更好。 如果每个日期时间仅平均调用2-3次utcoffset调用,则可能会从pytz得到更好的性能,因为偏移值将被缓存。

我怀疑,对于大多数实际情况来说,通过实现最近最少使用的缓存策略,来减少存储开销,可以显著减小额外时区计算的边际成本,但值得注意的是,pytz的设计使用了实际上无上限的缓存。 无论如何,标准免责声明均适用 - 不必担心这些微型基准,除非您确定它们是您的操作瓶颈。

结论

在创建时,pytz巧妙地设计为优化性能和正确性,但随着PEP 495引入的更改和性能改进,使用它的理由正在减少。 正如前面几节所述,Pytz的IANA时区更快,但常见的用例也比较慢。 从历史上看,他们提供了一个“更加正确”的时区实现,但现在他们以一种与Python时区模型不一致的方式解决了模糊的时间问题。

使用dateutil over pytz的最大原因是dateutil使用标准接口,结果很容易导致pytz错误。 即使你现在知道使用pytz的正确方法,你确定你不打算把你的datetime传递给一个函数,期望使用标准的tzinfo接口吗? 你确定任何维护你的代码或使用其输出的人都会知道避免这些错误吗?

备注

[1] 向下滚动到“另请参阅”部分,该部分没有自己的锚链接。

[2] 我会注意到,“标准偏移量”的存在可能是Python时区模型的一种无根据的假设。 在我所了解的所有时区中都有一些可识别的“标准”偏移量,但没有任何东西阻止人们观察一个奇怪的时区,由于夏时制以外的原因在一年中在三个同样有效的“时区”之间切换时间。

[3] 通过提供一个完全不同的tzinfo对象,这也会干扰Python的时区感知算术语义模型,该模型依赖于同一区域中的两个日期时间,满足dt1.tzinfo为dt2.tzinfo。

[5] 当然,可以简单地将缓存添加到dateutil的时区函数中,但pytz具有内在的优点,即tzinfo缓存内置在每个datetime对象中 - 为了实现dateutil时区的缓存,每个tzinfo必须 保留已计算偏移量的日期时间值的历史记录,并将其映射到正确的查找值。 相比之下,pytz将这个映射存储在tzinfo参数中,因为每个dateutil都存储一个对其所在位置的偏移量的引用,以每个可能偏移量增加一个额外的tzinfo对象的成本为代价(在大多数区域中,这将是3 -5个额外的tzinfo对象)。

[6] 请记住,dateutil和pytz都广泛使用缓存,所以这些数字是“渐近”的行为。 如果你最终在一个紧密的循环中一遍又一遍地做同样的事情,这些数字是非常有用的。

英文原文:https://blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.html

译者:javylee

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180430A0DBYW00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

关注

腾讯云开发者公众号
10元无门槛代金券
洞察腾讯核心技术
剖析业界实践案例
腾讯云开发者公众号二维码

扫码关注腾讯云开发者

领取腾讯云代金券