不管是哪門語言,碰觸時間處理相關議題時,如果開發者要認真面對,往往都會感到異常複雜。

複雜來自兩個部份:時間本身就因為歷史、經濟、政治等考量而複雜,API本身的設計經常令人困惑或易於犯錯。

因此,如果想要避開後者,唯一能憑藉的,就是對於前者的認識。

舊有的time模組

對於時間處理,Python內建的標準程式庫有著兩個模組‥舊有的time模組,以及自Python 2.3開始出現的datetime模組。不少文件或書籍兩者都會介紹,並且鼓勵開發者應該使用datetime模組。

然而,實際上,並不是那麼簡單的分野。畢竟,在Python 3.x之中,time模組還是存在的,從2.3到3.x這麼長的時間裡,都未被廢棄,突顯了time模組仍有其存在的價值。

第一個價值來自於time函式。

因為,這表示了一個絕對時間:自epoch開始至今經過的秒數。雖然大多數的系統epoch,都會是1970年1月1日0時0分0秒,不過,gmtime(0)可以告訴開發者正確的答案,儘管API上有gmt字樣,實際上,表示了UTC。即便有許多開發者不知道這個事實,然而,GMT時間經常不嚴謹(且有爭議性)地被當成是UTC時間。

其他有價值的部分,則是mktime函式與struct_time。

struct_time是個橋樑,擔任著人類時間概念與機器時間概念之間的轉換工作。如果開發者手中有個包含了時間各屬性的struct_time實例,可以透過mktime轉換為epoch秒數。舉例來說,開發者若手中有個代表人類時間概念的datetime實例,可以透過timetuple方法取得struct_time,這樣就能透過mktime轉換為epoch秒數。

除此之外,對於time模組中其他API,基本上,就不鼓勵使用了。

這特別是由於time模組中許多行為,都與底層平臺相依,它們會呼叫平臺上的C程式庫,而有些函式底層可能行為不同或不支援,像是time.tzset()就只在Unix環境中,才可使用,這連帶使得strptime函式在某些情況下,無法正常運作。

使用time模組來進行時間運算,或者是時區處理,也是極度不建議的嘗試。

人類時間概念的datetime模組

人類在時間的表達上,有時只需要日期、有時只需要時間,有時會一起表達日期與時間,而且,通常不會特別聲明時區,可能只會提及年、月、年月、月日、時分秒等,簡而言之,人類在時間概念的表達,大多是籠統、片段的資訊。

Python的datetime模組,基本上可用來表達人類的時間概念。因為當中的datetime、date、time預設沒有時區資訊,單純用來表示一個日期或時間,不過這是API上的定義。若程式運行時不需處理時區轉換問題,通常所在時區就暗示著是datetime、date、time的時區,因為人們若不特別提及時區,其實就是指本地時區居多。在《Effective Python》中的〈做法45〉,就建議:「本地時鐘使用datetime而非time」。

datetime模組中,有一個不錯的設計是,datetime本身不可變動(immutable),也考量了時間運算的問題,可以使用timedelta方法指定days、seconds、microseconds、milliseconds等單位,來建立一個時間計量(duration),這支援了+、-、*、/、//、%甚至abs等運算。因此,想知道現在的時間3天又5小時28分之後,會是什麼時間,只要撰寫datetime.now() + timedelta(days = 3, hours=-5, minutes = 28)就可以了。

不過,就算使用了datetime或date的today(),或者是datetime的now()、utcnow(),謹記著它們也是不帶時區資訊的,因此嚴格來說,開發者不能說datetime.utcnow()建立的datetime實例,代表著UTC時間。例如,datetime.utcnow()若建立了datetime(2016, 5, 23, 6, 55, 30, 505080),這純粹就只是代表著:「2016年 5月23日6點55分30秒505080微秒」這個時間概念罷了。

複雜的時區處理

對於日期與時間的處理議題上,只要涉及時區,往往就會變得極端複雜,因為牽涉了地理、法律、經濟、社會,甚至政治等問題。

例如,Python的datetime實例在建立時,可以透過tzinfo參數指定時區資訊,這必須是tzinfo的實例,然而tzinfo是個抽象類別,Python官方文件中,提供了一些如何實作tzinfo子類別的範例,並且自Python 3.2起,新增了timezone類別作為tzinfo的子類別,用來提供基本的UTC偏移時區實作,其中的timezone.utc,就是指偏移為0小時的UTC時間。

因此,現在可以正式做個區分了。datetime(1975, 5, 26, 3, 20, 50, 0)只是個時間,然而t = datetime(1975, 5, 26, 3, 20, 50, 0, tzinfo = timezone.utc)就可以說它是個代表著UTC時間了,當我們想要轉換至臺灣時區的時間,由於臺灣時區基本上就是偏移8個小時,所以,我們可以撰寫為t.astimezone(timezone(offset = timedelta(hours = 8))。

不過,Python內建的timezone只單純考量了UTC偏移,不考量日光節約時間等其他因素,若需要timezone以外的其他時區定義,目前來說,得額外安裝社群貢獻的pytz模組(PEP431規範了時區支援的改進,未來可能取代pytz模組)。而pytz模組使用的是Olson時區資料庫,是許多語言及作業系統的時區資料來源。

儘管如此,時區與時區之間的轉換,依舊複雜而麻煩,因此若應用程式需要儲存時間資訊,或甚至進行時間運算,常見的建議是使用絕對的UTC時間,然後,在需要時,再透過astimezone的幫忙,轉換為當地時區。

舉個例子來說,應用程式在儲存留言時間時,可以使用UTC時間,然而網頁上要把時間呈現給使用者看時,才依照UTC時間轉為對應時區的時間。如果需要在應用程式之間交換時間資訊,以UTC時間來作為交換,也會是個好選擇。

語言間真正能過渡的部份

如果開發者從未認真處理過時間的問題,對於以上的時間概念,像是epoch、GMT、UTC等沒有明確的認知,等到開始面對time或datetime模組等,也許會感到十分困惑。

針對時區的處理,開發者更可能不解API在使用上何以如此複雜,若是如此,建議看看我先前專欄〈從JDK時間API演進,看時間處理〉,瞭解幾個需要知道的時間概念,雖然這邊是在講Python,然後過去探查JDK時間API演進而獲取的時間知識,卻是非常的受用。

舉例而言,我就曾一度被datetime的now()、utcnow(),以及today()等混淆,誤認為它們帶有時區的概念。然而,後來我開始察覺到它們傳回的物件上tzinfo是None,這令人迅速聯想到JSR310的LocalDateTime、LocalDate、LocalTime等類別,以及背後所代表的概念。於是,我將time模組與datetime模組,整個重新探查了一遍,釐清機器時間與人類時間概念間的差別,接下來,相關API如何使用,也就明朗起來了。

其實時間處理只是一個例子,在探查處理特定議題的API時,對於該門議題的背景知識,往往比找出API的參數、傳回值、相依性等外在形式來得重要。

事實上,背景知識能引導開發者思考,如何正確地使用API,避開那些令人困惑的誤區,這才是語言間真正能過渡的部份。

作者簡介


Advertisement

更多 iThome相關內容