Python 3.12 將淘汰 datetime.datetime 的 utcnow() 與 utcfromtimestamp()

Simon Willison 這邊看到「It's Time For A Change: datetime.utcnow() Is Now Deprecated」,引用的文章是「It's Time For A Change: datetime.utcnow() Is Now Deprecated」這篇。

文章裡面有提到歷史因素,以及這樣設計造成的問題。

在文章後面有提到替代方案,改了一下裡面的用法,等價於這個:

from datetime import datetime, timezone
datetime.now(timezone.utc)
datetime.fromtimestamp(timestamp, timezone.utc)

或是這樣:

import datetime
datetime.datetime.now(datetime.timezone.utc)
datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc)

要稍微注意一下這個歷史遺跡要被拆了... (StackOverflow 上面應該有很多用到這兩個 function 的解答)

RFC 3339 與 ISO 8601 用範例的比較

Hacker News 首頁上看到「RFC 3339 vs. ISO 8601 (ijmacd.github.io)」這個連結,原始網站是「RFC 3339 vs ISO 8601」。

兩個都是試著定義日期與時間的表達方式,其中 ISO 8601 是在 1988 年定義出來的,被廣泛應用在很多 spec 裡,但最大的問題就是 ISO 8601 沒辦法公開免費取得,這點在前幾個月也在 Hacker News 上被討論過:「ISO-8601 date format reference not publicly available (twitter.com/isostandards)」。

RFC 3339 是在 2002 年訂出來,其中一個目標就是試著解決 ISO 8601 不公開的問題 (沒講明就是了):

This document includes an Internet profile of the ISO 8601 [ISO8601] standard for representation of dates and times using the Gregorian calendar.

這份 RFC 3339 與 ISO 8601 的比較可以讓設計 spec 的人可以看一下,在大多數的情況下,RFC 3339 應該是夠用的...

Trac 1.2 的 Due Date...

在先前的文章提到了把自己在用的事件管理系統 Trac 從 1.0 升級到 1.2,然後 Due Date 的設計改變了:「Trac 1.1 增加的 time 欄位,以及 Due Date 資料的轉移」、「總算把手上的 Trac 1.0 升級到 1.2 了...」。

Trac 1.2 的資料型態是在底層存 unix timestamp 的變形 (乘以 1000000,然後前端補上 0 存成文字),這幾天用下來才發現一些以前沒遇到的問題。

一開始轉到 Trac 1.2 是設成 date,但意外的發現 (因為伺服器時間不是 UTC),不同時區的使用者在更新 ticket 時,系統會判定 Due Date 有變動而產生變更記錄,想了一下就改用 datetime 來處理這個問題。

用了 datetime 一陣子後,才發現先前的公司遇到的情境中,時區差異都很小,所以不會有 Due Date 理解上的問題 (像是從 +7 到 +9 的時區),如果今天是美國西岸跟台灣互相合作的話,只用 date 就會產生很明顯的理解問題了...

算是這陣子用 Trac 1.2 而對 Due Date 設計有不一樣的理解...

Facebook 在 MySQL 裡存時間的型態

MySQL at Facebook這邊說明提到了,Facebook 內部是使用 INT UNSIGNED 儲存時間:

Which gets us to the point that it is no different than storing INT (hello 2038?) or UNSIGNED INT (a bit later) or BIGINT (till the end of time) and possibly passing binary values in efficient protocols eventually.

If you got that far of this post, your likes in Facebook graph are stored with 'INT UNSIGNED' time field.

順道一提,INT 是 2038 年問題,INT UNSIGNED 是 2106 年問題。

而 Facebook 在 MySQL 上會選擇不使用 DATETIMETIMESTAMP 的原因其實跟技術搭不上太多關係,主因是因為 MySQL 根本沒打算修 XDDD

It is my favorite MySQL bug, simply because it forces any reasonable mind not to use TIMESTAMP, and MySQL is never going to fix it (nor will ever understand time). I lost my temper a bit on that bug: https://bugs.mysql.com/bug.php?id=38455

我的猜測是已經爛成一團了,而且大家都有 workaround (呃,其實就是 Facebook 推薦用 INT UNSIGNED 的方法),再考慮到有一票現有程式,在上面狂用 side effect 讓執行結果正確,不如就不要修這種吃力不討好的東西了 XDDD

另外一方面 timezone 資訊其實常常變化,常常需要更新 MySQL 的 timezone database (而這對於維運來說不是什麼開心的事情):

There're few ways around that. One of them is side-load and maintain timezone data inside MySQL itself - it has support for internal timezone database and tracks obscure time shifts like ones for "Pacific War Time" and "Pacific Peace Time". That is operationally feasible (you have to remind yourself to update the database whenever time rules change, and they do change a lot, if you consider every timezone in the world), but has limited value.

這就是為什麼大家遇到 MySQL 時都會推薦用 INT UNSIGNED 了...

另外可以參考三年前的文章「MySQL 裡儲存時間的方式...」,裡面引用了 Baron Schwartz 的說明:

All date and time columns shall be INT UNSIGNED NOT NULL, and shall store a Unix timestamp in UTC.

其實這已經是個 best practice 了...

MySQL 裡儲存時間的方式...

這應該是 MySQL 的 best practice 之一,不知道為什麼 Baron Schwartz 又拿出來講:「A simple rule for sane timestamps in MySQL」。

MySQL 內可以儲存「日期與時間」的資料型態是 DATETIME 與 TIMESTAMP 兩種,不過 DATETIME 沒有時區觀念,而 TIMESTAMP 只能是 UTC (GMT+0)。

相較於隔壁棚 PostgreSQLDate/Time Types 就一種 TIMESTAMP,但支援 with timezone 與 without timezone 直接解決問題。

這使得 MySQL 上在儲存「日期與時間」以及處理的時候一直有種 WTF 的感覺...

就如同 Baron Schwartz 的建議,如果使用 MySQL,目前比較好的方法是用 INT UNSIGNED NOT NULL 儲存,把 timezone 的處理都放到應用程式端來處理,這樣產生的問題會比較少...

真的需要在 INSERT 或是 UPDATE 時更新欄位,可以用 trigger 處理,彈性反而比內建功能大不少。