pytz-西方最快的步槍
Python部落(python.freelycode.com)組織翻譯,禁止轉載,歡迎轉發。
每當我談論時區時,總有人跑來告訴我說他們生產環境的代碼掛掉了,因為他們錯誤理解了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。
[4] 在2.7.0版中的其他改進中,dateutil添加了一個dateutil.tz.UTC單例(以前的dateutil.tz.tzutc為每個調用構造了一個新對象),並開始緩存對tz.gettz的調用,並且通常減少構造函數調用其他時區對象。
[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
※10本特價書,手快有,手慢無
※Python對象中的淺拷貝和深拷貝
TAG:Python部落 |