如何使用 Python datetime 模組對時區進行運算?tzinfo,timezone 類別介紹

閱讀 19:45·字數 5925·發佈 
Youtube 頻道
訂閱 133

時區

按照正式的方式,世界被劃分為 24 個時區,零時區的時間與格林威治時區的時間可被視為相同,其余時區與相鄰的時區之間的時間差值為1個小時。

Python datetime 模組的感知型和單純型日期時間物件

如果 Pythondatetime模組的timedatetime物件包含有效的時區資訊,那麽這些物件被稱為感知型的日期時間物件,相反的,如果timedatetime物件不包含有效的時區資訊,那麽這些物件被稱為單純型日期時間物件。

因為擁有時區資訊,感知型的timedatetime物件一般不會產生歧義,而單純型的timedatetime物件所表示的日期時間的含義需要由開發人員或使用者自行決定(一般會作為本機時間使用)。

上述“有效的時區資訊”是指timedatetime物件的utcoffset方法沒有傳回空值None(允許utcoffset方法傳回等價於timedelta()timedelta物件)。

Python datetime 模組的 tzinfo,timezone 類別

Pythondatetime模組的tzinfo類別,用於表示時區資訊,該類別是一個抽象基底類別,因此不能直接建立tzinfo類別的執行個體。

由 Pythondatetime模組的tzinfo類別的衍生類別所產生的執行個體,一般會作為建立datetimetime物件時所需的參數tzinfo,並為datetimetime物件提供與時區相關的操作。

當然,datetime模組提供了一個實作了tzinfo的類別timezone,開發人員可使用timezone來建立時區資訊(timezone不提供某些特殊運算,比如夏令時),其建構子如下。

timezone(offset, name='UTC')

offset 參數

offset參數是一個timedelta物件,表示了timezone物件對應的時區與國際標準零時區之間的時間差值,該差值不能大於或等於運算式timedelta(hours=24)所表示的時間差值,也不能小於或等於運算式-timedelta(hours=24)所表示的時間差值,否則將引發例外狀況ValueError

name 參數

name參數是一個表示時區名稱的字串(預設值為'UTC'),他不能被設定為空值None

datetime 模組

關於如何建立datetimetime物件,你可以檢視Python datetime 模組的 datetime 類別Python datetime 模組的 time 類別兩段。

from datetime import timezone, timedelta
# 建立時區物件
timezone(timedelta(days=0.9999), 'My Zone')
datetime.timezone(datetime.timedelta(seconds=86391, microseconds=360000), 'My Zone')
# 時間差值大於或等於 24 小時將導致錯誤
timezone(timedelta(minutes=1440))

ValueError: offset must be a timedelta strictly between -timedelta(hours=24) and timedelta(hours=24), not datetime.timedelta(days=1).

使用 Python datetime 模組的 timezone 物件取得國際標準零時區

Pythondatetime模組的timezone物件的utc變數,以及datetime模組的UTC變數,是一個表示國際標準零時區的timezone物件,其效果等同於使用運算式timezone(timedelta())

timezone.utc
datetime.UTC

from datetime import time, timezone, timedelta
time(tzinfo=timezone.utc)
datetime.time(0, 0, tzinfo=datetime.timezone.utc)
time(tzinfo=timezone(timedelta()))
datetime.time(0, 0, tzinfo=datetime.timezone.utc)
time(tzinfo=UTC)
datetime.time(0, 0, tzinfo=datetime.timezone.utc)

通過 Python datetime 模組的 tzinfo 物件取得某個日期時間在夏令時中的時間差值

Pythondatetime模組的抽象基底類別tzinfodst方法,可用於計算某個日期時間在tzinfo所表示的夏令時中的時間差值,該方法通常被datetime模組的datetimetime物件的dst方法呼叫,也就是計算datetimetime所表示的日期時間在夏令時中的時間差值。因此,抽象基底類別tzinfo的衍生類別應在方法dst中實作以下功能,判斷日期時間是否在夏令時的範圍內,如果屬於夏令時則傳回日期時間在夏令時中的時間差值,如果未啟用夏令時或不屬於夏令時則應傳回空值None或等價於timedelta()timedelta物件。

tzinfo.dst(dt)

dt 參數

dt參數是一個datetime物件,將計算該物件所表示的日期時間在夏令時中的時間差值。

Pythondatetime模組的timedatetime物件同樣擁有名稱為dst的方法,同樣可計算日期時間在夏令時中的時間差值,其中datetime物件的dst方法會傳回運算式self.tzinfo.dst(self)的運算結果(如果datetime物件的tzinfo屬性為None,則dst方法直接傳回None),即datetime物件會將自身傳遞給其所包含的tzinfo物件的dst方法,而time物件的dst方法,會傳回運算式self.tzinfo.dst(None)的運算結果(如果time物件的tzinfo屬性為None,則dst方法直接傳回None),即time物件會將空值None傳遞給其所包含的tzinfo物件的dst方法。

造成上述差別的原因可能在於,夏令時的確定需要日期資訊,而 Pythondatetime模組的time物件只包含時間資訊,因此傳遞空值Nonetzinfo物件的dst方法是不難理解的。

time|datetime.dst()

什麽是夏令時?

從效果上來說,夏令時可以被視為一些日期時間段,處於這些日期時間段的日期時間的時區會被改變,以讓人們適當的調整作息時間。

在下面的範例中,我們定了自己的時區類別CustomDST,該類別會將一年中的 6,7,8 三個月份視為夏令時,對於任何的time物件,CustomDST會傳回timedelta(minutes=15)作為其在夏令時中的時間差值。

custom_dst.py
from datetime import time, datetime, timedelta, tzinfo

# 自訂時區 CustomDST class CustomDST(tzinfo): # 傳回日期時間在夏令時中的時間差值 def dst(self, dt: datetime):
# 當呼叫 time 物件的 dst 方法時,dt 為 None if not dt: # 這裏應該傳回 None 或 timedelta(),但我們嘗試傳回其他值 return timedelta(minutes=15)
# 時區 CustomDST 將 6,7,8 月份視為夏令時 if dt.month >= 6 and dt.month <= 8: # 夏令時的時間差值為 1 小時 return timedelta(hours=1) else: return timedelta()
cz = CustomDST() # 6 月 1 日屬於夏令時 print(cz.dst(datetime(2024, 6, 1))) # 9 月 1 日不屬於夏令時 print(datetime(2024, 9, 1, tzinfo=cz).dst()) # 時區 CustomDST 會為 time 物件傳回 timedelta(minutes=15) print(time().dst(), time(tzinfo=cz).dst())
1:00:00 # 夏令時
0:00:00
None 0:15:00

通過 Python datetime 模組的 tzinfo 物件取得某個日期時間與國際標準零時區之間的時間差值

Pythondatetime模組的抽象基底類別tzinfoutcoffset方法,可用於計算某個日期時間與國際標準零時區之間的時間差值,這應包括受夏令時影響而產生的時間差值(因此,抽象基底類別tzinfo的衍生類別應在方法utcoffset中呼叫其自身的方法dst),該方法通常被datetime模組的datetimetime物件的utcoffset方法呼叫,也就是計算datetimetime所表示的日期時間與國際標準零時區之間的時間差值。

需要說明的是,Pythondatetime模組的抽象基底類別tzinfoutcoffset方法,只需根據自身的時區資訊即可確定與國際標準零時區之間的時間差值,參數dt主要用於方法dst的呼叫,以確定日期時間在夏令時中的時間差值,兩個時間差值相加(相加時最好判斷時間差值是否為空值None)的結果即為utcoffset方法應該傳回的值。

tzinfo.utcoffset(dt)

dt 參數

dt參數是一個datetime物件,將計算該物件所表示的日期時間與國際標準零時區之間的時間差值。

Pythondatetime模組的timedatetime物件同樣擁有名稱為utcoffset的方法,同樣可計算日期時間與國際標準零時區之間的時間差值,其中datetime物件的utcoffset方法會傳回運算式self.tzinfo.utcoffset(self)的運算結果(如果datetime物件的tzinfo屬性為None,則utcoffset方法直接傳回None),即datetime物件會將自身傳遞給其所包含的tzinfo物件的utcoffset方法,而time物件的utcoffset方法,會傳回運算式self.tzinfo.utcoffset(None)的運算結果(如果time物件的tzinfo屬性為None,則utcoffset方法直接傳回None),即time物件會將空值None傳遞給其所包含的tzinfo物件的utcoffset方法。

time|datetime.utcoffset()

在下面的範例中,我們定了自己的時區類別CustomUTCOffset,該類別會將全年都視為夏令時,並將小時作為時區的單位,在其utcoffset方法中,需要判斷dst方法的傳回值是否為None

custom_utcoffset.py
from datetime import time, datetime, timedelta, tzinfo

# 自訂時區 CustomUTCOffset class CustomUTCOffset(tzinfo): # 初始化自訂時區 def __init__(self, hours): self.hours = timedelta(hours=hours)
# 計算日期時間與零時區之間的時間差值 def utcoffset(self, dt): # 需要判斷是否為 None,因為 dst 方法可能傳回 None dst = self.dst(dt) return self.hours + (timedelta(0) if dst is None else dst)
# 全年處於夏令時 def dst(self, dt): return timedelta(hours=1) if dt else None
# 與零時區相差 5 個小時 cz = CustomUTCOffset(5) # 時區 CustomUTCOffset 為 datetime 物件傳回的夏令時時間差值為 1 小時 print(cz.utcoffset(datetime(2024, 9, 1))) # 時區 CustomUTCOffset 為 time 物件傳回的夏令時時間差值為 None print(time().utcoffset(), time(tzinfo=cz).utcoffset())
6:00:00 # 包含夏令時時間差值
None 5:00:00

通過 Python datetime 模組的 tzinfo 物件取得某個日期時間的時區名稱

Pythondatetime模組的抽象基底類別tzinfotzname方法,可用於取得某個日期時間的時區名稱,該方法通常被datetime模組的datetimetime物件的tzname方法呼叫,也就是傳回datetimetime所表示的日期時間的時區名稱。

需要說明的是,Pythondatetime模組的抽象基底類別tzinfotzname方法,並不需要根據日期時間來傳回時區的名稱,其參數dt多被用於傳回夏令時中的特殊時區名稱。

tzinfo.tzname(dt)

dt 參數

dt參數是一個datetime物件,將傳回該物件所表示的日期時間的時區名稱。

Pythondatetime模組的timedatetime物件同樣擁有名稱為tzname的方法,同樣可取得某個日期時間的時區名稱,其中datetime物件的tzname方法會傳回運算式self.tzinfo.tzname(self)的運算結果(如果datetime物件的tzinfo屬性為None,則tzname方法直接傳回None),即datetime物件會將自身傳遞給其所包含的tzinfo物件的tzname方法,而time物件的tzname方法,會傳回運算式self.tzinfo.tzname(None)的運算結果(如果time物件的tzinfo屬性為None,則tzname方法直接傳回None),即time物件會將空值None傳遞給其所包含的tzinfo物件的tzname方法。

time|datetime.tzname()

在下面的範例中,我們定了自己的時區類別CustomTZName,該類別會為time物件,處於夏令時的datetime物件,未處於夏令時的datetime物件傳回不同的時區名稱。

custom_tzname.py
from datetime import time, datetime, tzinfo

# 自訂時區 CustomTZName class CustomTZName(tzinfo): # 初始化自訂時區 def __init__(self, name): self.name = name
# 傳回時區名稱 def tzname(self, dt):
if not dt: # 如果是 time 物件,則傳回 TIME 作為時區名稱 return 'TIME' elif dt.month < 6 or dt.month > 8: # 如果不是夏令時,則傳回建立 CustomTZName 物件時指定的時區名稱 return self.name else: # 如果是夏令時,則追加尾碼 DST return self.name + ' DST'
# 時區名稱為 My Zone cz = CustomTZName('My Zone') # 時區 CustomTZName 為 datetime 物件傳回的非夏令時時區名稱為 My Zone print(cz.tzname(datetime(2024, 9, 1))) # 時區 CustomTZName 為 datetime 物件傳回的夏令時時區名稱為 My Zone DST print(cz.tzname(datetime(2024, 7, 1))) # 時區 CustomTZName 為 time 物件傳回的時區名稱為 TIME print(time().tzname(), time(tzinfo=cz).tzname())
My Zone
My Zone DST # 夏令時時區名稱
None TIME

通過 Python datetime 模組的 tzinfo 物件為日期時間轉換時區

大多數情況下,Pythondatetime模組的抽象基底類別tzinfofromutc方法不會被開發人員直接使用,而是由datetime模組的datetime物件的astimezone方法來呼叫,後者可以轉換日期時間的時區(傳回新的datetime物件),轉換時區並不是簡單的取代datetime物件的時區資訊,而是會計算出原有日期時間在新時區中的值。

另外,Pythondatetime模組的抽象基底類別tzinfofromutc方法不是抽象的,因此,tzinfo類別的衍生類別並不需要覆寫該方法。在呼叫tzinfo物件的fromutc方法時,其參數dt被視為零時區的日期時間,並被轉換為tzinfo物件所表示的時區的日期時間(傳回新的datetime物件,並包括了夏令時帶來的影響)。需要指出,參數dttzinfo屬性必須是tzinfo物件自身,否則將導致例外狀況。

tzinfo.fromutc(dt)

dt 參數

dt參數是一個datetime物件,該物件被視為零時區的日期時間,並被轉換為tzinfo物件所表示時區的日期時間。

至於 Pythondatetime模組的datetime物件的astimezone方法,如果忽略參數tz,那麽日期時間將被轉換為作業系統所指定的時區的日期時間。

datetime.astimezone(tz=None)

tz 參數

tz參數是一個tzinfo物件,日期時間的時區將轉換為該物件所表示的時區。

datetime 模組

如果你僅希望簡單的取代datetimetime物件中的時區資訊,那麽可以檢視使用 Python datetime 模組修改日期時間一段。

custom_fromutc.py
from datetime import datetime, timedelta, tzinfo, timezone

# 自訂時區 MyZone class MyZone(tzinfo): # 時區 MyZone 與零時區之間的固定時間差值為 3 小時 def utcoffset(self, dt): return timedelta(hours=3) + self.dst(dt)
# 時區 MyZone 將 7 月份視為夏令時 def dst(self, dt): if dt and dt.month == 7: return timedelta(hours=1) else: return timedelta()

# 建立 MyZone 時區 myz = MyZone() # 將零時區的日期時間轉換為 MyZone 時區的日期時間 print(datetime(2024, 2, 1, tzinfo=timezone.utc).astimezone(myz)) # 將零時區的日期時間轉換為 MyZone 時區的夏令時日期時間 print(datetime(2024, 7, 1, tzinfo=timezone.utc).astimezone(myz)) # 將零時區的日期時間轉換本機日期時間(假設作業系統的時區為 UTC+8) print(datetime(2024, 3, 1, tzinfo=timezone.utc).astimezone()) # 將本機日期時間(假設作業系統的時區為 UTC+8)轉換為 MyZone 時區的日期時間 print(datetime(2024, 5, 1).astimezone(MyZone()))
# 直接呼叫 fromutc 方法,效果等同於上面的第一個時區轉換 print(myz.fromutc(datetime(2024, 2, 1, tzinfo=myz)))
2024-02-01 03:00:00+03:00
2024-07-01 04:00:00+04:00 # 包含夏令時時間差值
2024-03-01 08:00:00+08:00
2024-04-30 19:00:00+03:00
2024-02-01 03:00:00+03:00 # 直接呼叫 fromutc

Python datetime 模組的時區物件 tzinfo 對日期時間運算的影響

在 Python 的datetime模組中,簡單型的timedatetime物件不能與感知型的timedatetime物件進行運算,除了判斷是否相等(==!=),當然,簡單型和感知型並不會相等。

from datetime import time, datetime, timezone
time() < time(tzinfo=timezone.utc)

TypeError: can't compare offset-naive and offset-aware times
datetime(2024, 1, 1) - datetime(2024, 1, 1, tzinfo=timezone.utc)

TypeError: can't subtract offset-naive and offset-aware datetimes
# 只能判斷是否相等
datetime(2024, 1, 1) == datetime(2024, 1, 1, tzinfo=timezone.utc)
False

對於 Pythondatetime模組中的感知型timedatetime物件,時區資訊會被運算考慮,其效果類似於將所有日期時間轉換為零時區對應的日期時間,然後再進行運算(當然,具體實作方式可能並非如此),這也意味著兩個timedatetime物件可能是相同的,即便他們擁有不同的時區資訊或tzinfo屬性。

from datetime import time, datetime, timezone, timedelta
tz8 = timezone(timedelta(hours=8))
# 零時區的 0 點大於 UTC+8 時區的 0 點
datetime(2024, 1, 1, tzinfo=timezone.utc) > datetime(2024, 1, 1, tzinfo=tz8)
True
# 零時區的 0 點等於 UTC+8 時區的 8 點
datetime(2024, 1, 1, tzinfo=timezone.utc) - datetime(2024, 1, 1, 8, tzinfo=tz8)
datetime.timedelta(0)
# 實際上相同的兩個時區
time(tzinfo=timezone.tz8) == time(8, tzinfo=timezone(timedelta(minutes=480)))
True

對於 Pythondatetime模組中的timedelta物件與感知型datetime物件之間的運算,其運算結果的時區資訊與原有感知型datetime物件的時區資訊相同,時區資訊在運算過程中不會起到任何作用。

from datetime import datetime, timezone, timedelta
# 時區資訊不會改變
datetime(2024, 1, 1, tzinfo=timezone(timedelta(hours=8))) + timedelta(hours=11)
datetime.datetime(2024, 1, 1, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=28800)))

程式碼

src/zh-hant/date_times/datetime·codebeatme/python·GitHub