取得指定年月的天數是常見需求。Python 標準庫提供 `calendar.monthrange`、手動計算等多種方式。本文完整解析各方法原理、效能比較、邊界情況與最佳實務。
import calendar
# 回傳 (weekday_of_first_day, number_of_days)
# weekday: 0=Monday ... 6=Sunday
result = calendar.monthrange(2024, 2) # 閏年二月
print(result) # (3, 29) -> 週四開始, 29天
# 直接取得天數
days = calendar.monthrange(2024, 2)[1]
print(f"2024年2月有 {days} 天") # 29
# 常見年份測試
for year in [2023, 2024, 2000, 1900, 2025]:
days = calendar.monthrange(year, 2)[1]
print(f"{year}年2月: {days}天")
# 2023: 28, 2024: 29, 2000: 29, 1900: 28, 2025: 28
from datetime import date, timedelta
def days_in_month_datetime(year, month):
"""利用下個月第一天減去一天"""
if month == 12:
next_month = date(year + 1, 1, 1)
else:
next_month = date(year, month + 1, 1)
last_day = next_month - timedelta(days=1)
return last_day.day
# 測試
print(days_in_month_datetime(2024, 2)) # 29
print(days_in_month_datetime(2023, 2)) # 28
def is_leap_year(year):
"""格里曆閏年規則"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
def days_in_month_manual(year, month):
if month == 2:
return 29 if is_leap_year(year) else 28
elif month in {4, 6, 9, 11}:
return 30
else:
return 31
# 查表法 (最快)
_MONTH_DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def days_in_month_lookup(year, month):
if month == 2 and is_leap_year(year):
return 29
return _MONTH_DAYS[month]
import timeit
import calendar
from datetime import date, timedelta
def bench_calendar():
return calendar.monthrange(2024, 2)[1]
def bench_datetime():
next_month = date(2025, 3, 1)
return (next_month - timedelta(days=1)).day
def bench_manual():
year, month = 2024, 2
return 29 if (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)) else 28
def bench_lookup():
year, month = 2024, 2
if month == 2 and (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)):
return 29
return _MONTH_DAYS[month]
# 執行測試
iterations = 1_000_000
print(f"calendar.monthrange: {timeit.timeit(bench_calendar, number=iterations):.3f}s")
print(f"datetime 計算: {timeit.timeit(bench_datetime, number=iterations):.3f}s")
print(f"手動閏年判斷: {timeit.timeit(bench_manual, number=iterations):.3f}s")
print(f"查表法: {timeit.timeit(bench_lookup, number=iterations):.3f}s")
# 典型結果 (相對速度):
# 查表法: 1.0x (基準)
# 手動閏年判斷: 1.2x
# calendar: 3.5x (函數呼叫開銷)
# datetime: 8.0x (物件建立開銷)
import calendar
# 1. 月曆矩陣 (每週一列,0 表示該月外的日期)
cal = calendar.monthcalendar(2024, 2)
print("2024年2月月曆:")
for week in cal:
print(week)
# [[0, 0, 0, 1, 2, 3, 4], [5, 6, 7, 8, 9, 10, 11], ...]
# 2. 文字月曆
print(calendar.month(2024, 2))
# February 2024
# Mo Tu We Th Fr Sa Su
# 1 2 3 4
# 5 6 7 8 9 10 11
# ...
# 3. 完整年曆
# print(calendar.calendar(2024))
# 4. 判斷閏年
print(calendar.isleap(2024)) # True
print(calendar.isleap(1900)) # False
# 5. 取得月份名稱
print(calendar.month_name[2]) # 'February'
print(calendar.month_abbr[2]) # 'Feb'
# 6. 取得星期名稱
print(calendar.day_name[0]) # 'Monday'
print(calendar.day_abbr[0]) # 'Mon'
# 7. 設定週首日 (預設 0=Monday)
calendar.setfirstweekday(calendar.SUNDAY)
cal = calendar.monthcalendar(2024, 2) # 現在週日為第一欄
from datetime import date, timedelta
import calendar
def iterate_month_days(year, month):
"""產生該月所有日期"""
days = calendar.monthrange(year, month)[1]
for day in range(1, days + 1):
yield date(year, month, day)
def get_month_range(year, month):
"""取得該月第一天與最後一天 (date 物件)"""
first = date(year, month, 1)
last_day = calendar.monthrange(year, month)[1]
last = date(year, month, last_day)
return first, last
# 使用範例
first, last = get_month_range(2024, 2)
print(f"2024-02 範圍: {first} 至 {last}")
# 批次處理該月所有工作日
workdays = [d for d in iterate_month_days(2024, 2) if d.weekday() < 5]
print(f"2024-02 工作日數: {len(workdays)}")
from datetime import datetime
from zoneinfo import ZoneInfo
import calendar
def days_in_month_tz(year, month, tz_name="Asia/Taipei"):
"""考慮時區的月份天數 (極少數情況跨日)"""
tz = ZoneInfo(tz_name)
# 該月第一天 00:00
start = datetime(year, month, 1, tzinfo=tz)
# 下個月第一天 00:00
if month == 12:
end = datetime(year + 1, 1, 1, tzinfo=tz)
else:
end = datetime(year, month + 1, 1, tzinfo=tz)
# 計算天數差 (通常與無時區版相同)
diff = end - start
return diff.days
# 正常情況下結果相同
print(days_in_month_tz(2024, 2)) # 29
| 場景 | 推薦方法 | 理由 |
|---|---|---|
| 一般應用、可讀性優先 | calendar.monthrange | 標準庫、語意清晰、功能豐富 |
| 高效能、大量呼叫 (百萬級) | 查表法 + 內聯閏年判斷 | 無函數呼叫、無物件建立、最快 |
| 已使用 datetime 物件 | datetime 計算法 | 程式碼一致性、物件導向 |
| 需同時取得月初星期 | calendar.monthrange | 一次取得雙資訊 |
| 教學/展示閏年邏輯 | 手動計算 | 邏輯透明、易理解 |
| 跨時區精確計算 | zoneinfo + datetime | 處理夏令時間邊界情況 |
import calendar
# 1. 歷史年份 (格里曆 1582 年起)
print(calendar.monthrange(1582, 10)) # (4, 31) - 格里曆採用月份
# 2. 極大年份
print(calendar.monthrange(9999, 12)) # (4, 31)
# 3. 無效參數
try:
calendar.monthrange(2024, 13)
except Exception as e:
print(f"預期錯誤: {type(e).__name__}") # Illegal month value
# 4. 閏秒不影響天數計算
# 閏秒只影響時間精度,不影響日期
# 5. 不同曆法 (需第三方庫)
# 伊斯蘭曆、農曆、波斯曆等需使用 convertdate、hijri-converter 等庫
掌握 `calendar.monthrange` 核心用法,理解替代方案效能特性,能根據應用場景選擇最適合的月份天數計算策略。