如何使用 Python datetime 模組對時區進行運算?tzinfo,timezone 類別介紹
時區
按照正式的方式,世界被劃分為 24 個時區,零時區的時間與格林威治時區的時間可被視為相同,其余時區與相鄰的時區之間的時間差值為1
個小時。
Python datetime 模組的感知型和單純型日期時間物件
如果 Pythondatetime
模組的time
或datetime
物件包含有效的時區資訊,那麽這些物件被稱為感知型的日期時間物件,相反的,如果time
或datetime
物件不包含有效的時區資訊,那麽這些物件被稱為單純型日期時間物件。
因為擁有時區資訊,感知型的time
或datetime
物件一般不會產生歧義,而單純型的time
或datetime
物件所表示的日期時間的含義需要由開發人員或使用者自行決定(一般會作為本機時間使用)。
上述“有效的時區資訊”是指time
或datetime
物件的utcoffset
方法沒有傳回空值None
(允許utcoffset
方法傳回等價於timedelta()
的timedelta
物件)。
Python datetime 模組的 tzinfo,timezone 類別
Pythondatetime
模組的tzinfo
類別,用於表示時區資訊,該類別是一個抽象基底類別,因此不能直接建立tzinfo
類別的執行個體。
由 Pythondatetime
模組的tzinfo
類別的衍生類別所產生的執行個體,一般會作為建立datetime
或time
物件時所需的參數tzinfo
,並為datetime
或time
物件提供與時區相關的操作。
當然,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 模組
關於如何建立datetime
和time
物件,你可以檢視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
模組的抽象基底類別tzinfo
的dst
方法,可用於計算某個日期時間在tzinfo
所表示的夏令時中的時間差值,該方法通常被datetime
模組的datetime
和time
物件的dst
方法呼叫,也就是計算datetime
或time
所表示的日期時間在夏令時中的時間差值。因此,抽象基底類別tzinfo
的衍生類別應在方法dst
中實作以下功能,判斷日期時間是否在夏令時的範圍內,如果屬於夏令時則傳回日期時間在夏令時中的時間差值,如果未啟用夏令時或不屬於夏令時則應傳回空值None
或等價於timedelta()
的timedelta
物件。
tzinfo.dst(dt)
- dt 參數
dt
參數是一個datetime
物件,將計算該物件所表示的日期時間在夏令時中的時間差值。
Pythondatetime
模組的time
和datetime
物件同樣擁有名稱為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
物件只包含時間資訊,因此傳遞空值None
到tzinfo
物件的dst
方法是不難理解的。
time|datetime.dst()
什麽是夏令時?
從效果上來說,夏令時可以被視為一些日期時間段,處於這些日期時間段的日期時間的時區會被改變,以讓人們適當的調整作息時間。
在下面的範例中,我們定了自己的時區類別CustomDST
,該類別會將一年中的 6,7,8 三個月份視為夏令時,對於任何的time
物件,CustomDST
會傳回timedelta(minutes=15)
作為其在夏令時中的時間差值。
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
模組的抽象基底類別tzinfo
的utcoffset
方法,可用於計算某個日期時間與國際標準零時區之間的時間差值,這應包括受夏令時影響而產生的時間差值(因此,抽象基底類別tzinfo
的衍生類別應在方法utcoffset
中呼叫其自身的方法dst
),該方法通常被datetime
模組的datetime
和time
物件的utcoffset
方法呼叫,也就是計算datetime
或time
所表示的日期時間與國際標準零時區之間的時間差值。
需要說明的是,Pythondatetime
模組的抽象基底類別tzinfo
的utcoffset
方法,只需根據自身的時區資訊即可確定與國際標準零時區之間的時間差值,參數dt
主要用於方法dst
的呼叫,以確定日期時間在夏令時中的時間差值,兩個時間差值相加(相加時最好判斷時間差值是否為空值None
)的結果即為utcoffset
方法應該傳回的值。
tzinfo.utcoffset(dt)
- dt 參數
dt
參數是一個datetime
物件,將計算該物件所表示的日期時間與國際標準零時區之間的時間差值。
Pythondatetime
模組的time
和datetime
物件同樣擁有名稱為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
。
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
模組的抽象基底類別tzinfo
的tzname
方法,可用於取得某個日期時間的時區名稱,該方法通常被datetime
模組的datetime
和time
物件的tzname
方法呼叫,也就是傳回datetime
或time
所表示的日期時間的時區名稱。
需要說明的是,Pythondatetime
模組的抽象基底類別tzinfo
的tzname
方法,並不需要根據日期時間來傳回時區的名稱,其參數dt
多被用於傳回夏令時中的特殊時區名稱。
tzinfo.tzname(dt)
- dt 參數
dt
參數是一個datetime
物件,將傳回該物件所表示的日期時間的時區名稱。
Pythondatetime
模組的time
和datetime
物件同樣擁有名稱為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
物件傳回不同的時區名稱。
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
模組的抽象基底類別tzinfo
的fromutc
方法不會被開發人員直接使用,而是由datetime
模組的datetime
物件的astimezone
方法來呼叫,後者可以轉換日期時間的時區(傳回新的datetime
物件),轉換時區並不是簡單的取代datetime
物件的時區資訊,而是會計算出原有日期時間在新時區中的值。
另外,Pythondatetime
模組的抽象基底類別tzinfo
的fromutc
方法不是抽象的,因此,tzinfo
類別的衍生類別並不需要覆寫該方法。在呼叫tzinfo
物件的fromutc
方法時,其參數dt
被視為零時區的日期時間,並被轉換為tzinfo
物件所表示的時區的日期時間(傳回新的datetime
物件,並包括了夏令時帶來的影響)。需要指出,參數dt
的tzinfo
屬性必須是tzinfo
物件自身,否則將導致例外狀況。
tzinfo.fromutc(dt)
- dt 參數
dt
參數是一個datetime
物件,該物件被視為零時區的日期時間,並被轉換為tzinfo
物件所表示時區的日期時間。
至於 Pythondatetime
模組的datetime
物件的astimezone
方法,如果忽略參數tz
,那麽日期時間將被轉換為作業系統所指定的時區的日期時間。
datetime.astimezone(tz=None)
- tz 參數
tz
參數是一個tzinfo
物件,日期時間的時區將轉換為該物件所表示的時區。
datetime 模組
如果你僅希望簡單的取代datetime
或time
物件中的時區資訊,那麽可以檢視使用 Python datetime 模組修改日期時間一段。
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
模組中,簡單型的time
和datetime
物件不能與感知型的time
和datetime
物件進行運算,除了判斷是否相等(==
和!=
),當然,簡單型和感知型並不會相等。
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
模組中的感知型time
和datetime
物件,時區資訊會被運算考慮,其效果類似於將所有日期時間轉換為零時區對應的日期時間,然後再進行運算(當然,具體實作方式可能並非如此),這也意味著兩個time
或datetime
物件可能是相同的,即便他們擁有不同的時區資訊或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)))