PEP 615 – 표준 라이브러리의 IANA 시간대 데이터베이스 지원
- Author:
- Paul Ganssle <paul at ganssle.io>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 22-Feb-2020
- Python-Version:
- 3.9
- Post-History:
- 25-Feb-2020, 29-Mar-2020
- Replaces:
- 431
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 IANA 시간대 데이터베이스를 지원하는 구체적인 시간대 구현을 제공하기 위해 zoneinfo 모듈을 추가할 것을 제안합니다. 기본적으로 zoneinfo는 시스템의 시간대 데이터가 있으면 그것을 사용합니다. 시스템 시간대 데이터가 없으면 라이브러리는 PyPI에 배포된 퍼스트 파티 패키지 tzdata를 대신 사용합니다. [d]
동기
datetime 라이브러리는 시간대를 처리하는 데 유연한 메커니즘을 사용합니다. 모든 변환과 시간대 정보 조회는 추상 datetime.tzinfo 베이스 클래스의 서브클래스 인스턴스에 위임됩니다. [10] 덕분에 사용자는 임의로 복잡한 시간대 규칙을 구현할 수 있지만, 실제로 대부분의 사용자가 원하는 것은 다음 세 가지 유형의 시간대 지원뿐입니다. [a]
- UTC 및 UTC 기준 고정 오프셋
- 시스템 로컬 시간대
- IANA 시간대
Python 3.2에서는 첫 번째 유형의 시간대를 지원하기 위해 datetime.timezone 클래스가 도입되었습니다(UTC를 위한 특별한 datetime.timezone.utc 싱글턴도 함께 도입되었습니다).
아직 “로컬” 시간대는 없지만, Python 3.0에서 나이브(naïve) 시간대의 의미 체계가 여러 “로컬 시간” 연산을 지원하도록 변경되었으며, 이제 로컬 시간에서 고정 시간대 오프셋을 얻을 수 있습니다:
>>> print(datetime(2020, 2, 22, 12, 0).astimezone())
2020-02-22 12:00:00-05:00
>>> print(datetime(2020, 2, 22, 12, 0).astimezone()
... .strftime("%Y-%m-%d %H:%M:%S %Z"))
2020-02-22 12:00:00 EST
>>> print(datetime(2020, 2, 22, 12, 0).astimezone(timezone.utc))
2020-02-22 17:00:00+00:00
그러나 IANA 시간대 데이터베이스(“tz” 데이터베이스 또는 Olson 데이터베이스라고도 합니다 [6])에 기술된 시간대는 여전히 지원되지 않습니다. 시간대 데이터베이스는 퍼블릭 도메인에 속하며 널리 배포되어 있습니다. 많은 Unix 계열 운영 체제에는 기본으로 포함되어 있습니다. 이 데이터베이스의 안정성에는 큰 주의를 기울이고 있습니다. 유지 관리 절차(RFC 6557)와 컴파일된 바이너리(TZif) 형식(RFC 8536) 모두에 대해 IETF RFC가 존재합니다. 따라서 표준 라이브러리의 릴리스 주기가 비교적 길더라도 IANA 데이터베이스의 컴파일된 출력물 지원을 추가하면 최종 사용자에게 큰 가치를 제공할 가능성이 높습니다.
제안
이 PEP는 세 가지 주요 사항을 다룹니다.
zoneinfo.ZoneInfo클래스의 의미 체계 (zoneinfo-class)- 사용되는 시간대 데이터 소스 (data-sources)
- 시간대 검색 경로 설정 옵션 (search-path-config)
이 제안은 복잡하므로 “명세”와 “근거” 섹션을 따로 두지 않고, 설계 결정과 그 근거를 주제별로 묶어 설명합니다.
zoneinfo.ZoneInfo 클래스
생성자
zoneinfo.ZoneInfo 클래스의 초기 설계에는 여러 생성자가 있습니다.
ZoneInfo(key: str)
기본 생성자는 단일 인자 key를 받습니다. 이 인자는 시스템 시간대 데이터베이스에 있는 존 파일의 이름을 나타내는 문자열이며(예: "America/New_York", "Europe/London"), 생성자는 검색 경로에서 처음으로 일치하는 데이터 소스로부터 생성한 ZoneInfo를 반환합니다(자세한 내용은 data-sources 섹션을 참조하십시오). 모든 존 정보는 생성 시점에 데이터 소스(보통 TZif 파일)에서 즉시 읽어 들여야 하며, 객체의 수명 동안 변경되어서는 안 됩니다(이 제약은 모든 ZoneInfo 생성자에 적용됩니다).
검색 경로에서 일치하는 파일을 찾지 못하면(시스템이 시간대 데이터를 제공하지 않거나 키가 유효하지 않은 경우), 생성자는 zoneinfo.ZoneInfoNotFoundError를 발생시킵니다. 이 예외는 KeyError의 서브클래스입니다.
이 생성자가 제공하는 다소 특이한 보장 중 하나는 동일한 인자로 호출하면 반드시 동일한 객체를 반환해야 한다는 것입니다. 구체적으로, key의 모든 값에 대해 다음 단언이 항상 성립해야 합니다 [b]:
a = ZoneInfo(key)
b = ZoneInfo(key)
assert a is b
그 이유는 datetime 연산(예: 비교, 산술)의 의미 체계가 관련된 datetime들이 같은 존을 나타내는지 다른 존을 나타내는지에 따라 달라지기 때문입니다. 두 datetime은 dt1.tzinfo is dt2.tzinfo인 경우에만 같은 존에 있습니다. [1] 불필요하게 ZoneInfo 객체가 늘어나는 것을 막아 얻는 약간의 성능상 이점에 더해, 이 보장을 제공하면 최종 사용자가 예상치 못한 동작을 겪는 일도 최소화될 것입니다.
dateutil.tz.gettz는 버전 2.7.0(2018년 3월 릴리스)부터 비슷한 보장을 제공해 왔습니다. [16]
Note
캐시 동작을 어떻게 구현할지는 구현체가 결정할 수 있지만, 여기서 보장해야 하는 것은 동일한 생성자 호출의 결과에 대한 두 참조가 존재하는 동안 두 참조가 같은 객체를 가리켜야 한다는 것뿐입니다. 이는 ZoneInfo 객체를 가리키는 참조가 더 이상 존재하지 않을 때 해당 객체를 제거하는 참조 카운트 기반 캐시(예를 들어 weakref.WeakValueDictionary로 구현한 캐시)와 부합합니다. 모든 ZoneInfo 객체를 무기한 유지하는 “강한(strong)” 캐시로 구현하는 것도 허용되지만, 필수 사항은 아니며 권장하지도 않습니다.
ZoneInfo.no_cache(key: str)
이것은 생성자의 캐시를 우회하는 대체 생성자입니다. 기본 생성자와 동일하지만, 호출할 때마다 새 객체를 반환합니다. 이 생성자는 테스트 목적으로 사용하거나, 명목상 같은 시간대를 가진 datetime 사이에 의도적으로 “다른 시간대” 의미론을 만들어 내는 데 가장 유용할 것입니다.
이 메서드로 생성한 객체가 캐시 미스에 해당하는 경우라도 캐시에 등록해서는 안 됩니다. 다시 말해, 다음 단언은 항상 참이어야 합니다:
>>> a = ZoneInfo.no_cache(key)
>>> b = ZoneInfo(key)
>>> a is not b
ZoneInfo.from_file(fobj: IO[bytes], /, key: str = None)
이것은 임의의 TZif 바이트 스트림으로부터 ZoneInfo 객체를 생성할 수 있게 해 주는 대체 생성자입니다. 이 생성자는 선택적 매개변수 key를 받으며, 이 매개변수는 __str__과 __repr__에 사용할 시간대 이름을 설정합니다(Representations을 참조하십시오).
기본 생성자와 달리 이 생성자는 항상 새 객체를 생성합니다. 이 생성자가 기본 생성자의 캐싱 동작을 따르지 않는 이유는 두 가지입니다. 스트림 객체는 가변 상태를 가지므로 두 입력이 동일한지 판단하기가 어렵거나 불가능하며, 파일로부터 객체를 생성하는 사용자는 캐시가 아니라 바로 그 파일에서 불러오기를 원할 가능성이 높습니다.
ZoneInfo.no_cache와 마찬가지로, 이 메서드로 생성한 객체는 캐시에 추가해서는 안 됩니다.
데이터 갱신 중의 동작
주어진 ZoneInfo 객체의 동작은 그 수명 동안 변하지 않아야 합니다. datetime의 utcoffset() 메서드는 동등성 계산과 해시 계산 모두에 사용되므로, datetime의 수명 동안 그 결과가 바뀐다면 모든 해시 가능 객체 [3] [4] 에 대해 x == y이면 hash(x) == hash(y) [c] 도 참이어야 한다는 불변 조건이 깨질 수 있기 때문입니다.
datetime의 불변 조건을 보존해야 한다는 점과, 동일한 인자로 호출하면 항상 같은 객체를 반환해야 한다는 기본 생성자의 계약을 모두 고려하면, 인터프리터 실행 중에 시간대 데이터 소스가 갱신되더라도 어떤 캐시도 무효화해서는 안 되며 기존 ZoneInfo 객체를 수정해서도 안 됩니다. 그러나 새로 생성되는 ZoneInfo 객체는 갱신된 데이터 소스에서 가져와야 합니다.
이는 새로 호출되는 ZoneInfo 생성자에 갱신된 데이터 소스가 반영되는 시점이 주로 캐싱 동작의 의미론에 달려 있다는 뜻입니다. 갱신된 데이터 소스로부터 ZoneInfo 객체를 확실하게 얻는 유일한 방법은 캐시 미스를 유도하는 것입니다. 캐시를 우회하여 ZoneInfo.no_cache를 사용하거나 캐시를 비우면 캐시 미스를 유도할 수 있습니다.
Note
명시된 캐시 동작은 캐시를 지연 방식으로 채울 것을 요구하지 않습니다. 한 번도 생성된 적 없는 시간대로 캐시를 미리 채워 두는 것도 (권장하지는 않지만) 명세에 부합합니다.
의도적인 캐시 무효화
사용자가 캐시를 우회할 수 있게 해 주는 ZoneInfo.no_cache 외에도, ZoneInfo는 캐시 전체 또는 캐시의 선택된 일부를 의도적으로 무효화하는 clear_cache 메서드를 제공합니다:
ZoneInfo.clear_cache(*, only_keys: Iterable[str]=None) -> None
인자를 전달하지 않으면 모든 캐시가 무효화되며, 캐시를 비운 후 각 키로 기본 ZoneInfo 생성자를 처음 호출하면 새 인스턴스가 반환됩니다.
>>> NYC0 = ZoneInfo("America/New_York")
>>> NYC0 is ZoneInfo("America/New_York")
True
>>> ZoneInfo.clear_cache()
>>> NYC1 = ZoneInfo("America/New_York")
>>> NYC0 is NYC1
False
>>> NYC1 is ZoneInfo("America/New_York")
True
선택적 매개변수 only_keys는 캐시에서 비울 키들의 이터러블을 받으며, 나머지 캐시는 그대로 둡니다.
>>> NYC0 = ZoneInfo("America/New_York")
>>> LA0 = ZoneInfo("America/Los_Angeles")
>>> ZoneInfo.clear_cache(only_keys=["America/New_York"])
>>> NYC1 = ZoneInfo("America/New_York")
>>> LA0 = ZoneInfo("America/Los_Angeles")
>>> NYC0 is NYC1
False
>>> LA0 is LA1
True
캐시 동작을 조작하는 것은 드문 사용 사례일 것으로 예상됩니다. 이 함수는 주로 테스트를 용이하게 하고, 특수한 요구 사항이 있는 사용자가 캐시 무효화 동작을 필요에 맞게 조정할 수 있도록 제공됩니다.
문자열 표현
ZoneInfo 클래스의 __str__ 표현은 key 매개변수에서 가져옵니다. 이는 한편으로는 key가 사람이 읽을 수 있는 문자열의 “이름”을 나타내기 때문이고, 다른 한편으로는 사용자가 노출되기를 원할 유용한 매개변수이기 때문입니다. 언어 간 직렬화를 위해 키를 노출하는 메커니즘을 제공해야 하며, 키는 CLDR(유니코드 공통 로케일 데이터 저장소 [5]) 같은 지역화 프로젝트의 기본 키이기도 합니다.
예시:
>>> zone = ZoneInfo("Pacific/Kwajalein")
>>> str(zone)
'Pacific/Kwajalein'
>>> dt = datetime(2020, 4, 1, 3, 15, tzinfo=zone)
>>> f"{dt.isoformat()} [{dt.tzinfo}]"
'2020-04-01T03:15:00+12:00 [Pacific/Kwajalein]'
key가 지정되지 않은 경우 str 연산은 실패해서는 안 되며, 객체의 __repr__을 반환해야 합니다:
>>> zone = ZoneInfo.from_file(f)
>>> str(zone)
'ZoneInfo.from_file(<_io.BytesIO object at ...>)'
ZoneInfo의 __repr__은 구현에 따라 정의되며 버전 간에 반드시 안정적이지는 않지만, 유효한 ZoneInfo 키여서는 안 됩니다. 유효한 __str__을 가진 키 기반 ZoneInfo와 __repr__로 대체된 파일 기반 ZoneInfo를 혼동하지 않도록 하기 위해서입니다.
키에 접근하기 위해 str()을 사용하는 방식으로는 키의 존재 여부를 쉽게 확인할 수 없으므로(유일한 방법은 그 값으로 ZoneInfo를 생성해 보고 예외가 발생하는지 감지하는 것입니다), ZoneInfo 객체는 읽기 전용 key 속성도 노출합니다. 키가 제공되지 않은 경우 이 속성은 None입니다.
피클 직렬화
모든 전환 데이터를 직렬화하는 대신 ZoneInfo 객체는 키로 직렬화됩니다. 원시 파일로부터 생성된 ZoneInfo 객체는 (key 값이 지정된 경우에도) 피클할 수 없습니다.
ZoneInfo 객체의 동작은 객체가 어떻게 생성되었는지에 따라 달라집니다:
ZoneInfo(key): 기본 생성자로 생성된ZoneInfo객체는 키로 직렬화됩니다. 역직렬화할 때도 기본 생성자를 사용하므로, 같은 시간대를 가리키는 다른 참조와 동일한 객체가 될 것으로 기대됩니다. 예를 들어europe_berlin_pkl이ZoneInfo("Europe/Berlin")으로부터 생성된 피클을 담은 문자열이라면, 다음과 같이 동작할 것으로 기대할 수 있습니다:>>> a = ZoneInfo("Europe/Berlin") >>> b = pickle.loads(europe_berlin_pkl) >>> a is b True
ZoneInfo.no_cache(key): 캐시를 우회하는 생성자로 생성된ZoneInfo객체도 키로 직렬화됩니다. 다만 역직렬화할 때는 캐시를 우회하는 생성자를 사용합니다.europe_berlin_pkl_nc가ZoneInfo.no_cache("Europe/Berlin")으로부터 생성된 피클을 담은 문자열이라면, 다음과 같이 동작할 것으로 기대할 수 있습니다:>>> a = ZoneInfo("Europe/Berlin") >>> b = pickle.loads(europe_berlin_pkl_nc) >>> a is b False
ZoneInfo.from_file(fobj, /, key=None): 파일로부터 생성된ZoneInfo객체는 피클링할 때 예외를 발생시킵니다. 최종 사용자가 파일로부터 생성된ZoneInfo를 피클하려면 래퍼 타입이나 사용자 정의 직렬화 함수를 사용할 것을 권장합니다. 키로 직렬화하는 방법도 있고, 파일 객체의 내용을 저장해 두고 그것을 직렬화하는 방법도 있습니다.
이 직렬화 방식에서는 필요한 키의 시간대 데이터가 직렬화하는 쪽과 역직렬화하는 쪽 모두에서 사용 가능해야 합니다. 이는 클래스와 함수에 대한 참조가 직렬화 환경과 역직렬화 환경 양쪽에 존재해야 하는 것과 마찬가지입니다. 또한 시간대 데이터의 버전이 다른 환경에서 피클된 ZoneInfo를 언피클하면 결과의 일관성이 보장되지 않습니다.
시간대 데이터의 출처
IANA 시간대 지원에서 가장 어려운 과제 중 하나는 데이터를 최신 상태로 유지하는 것입니다. 1997년부터 2020년까지 매년 3회에서 21회의 릴리스가 있었습니다. 이는 시간대 규칙이 사전 공지 없이 또는 거의 공지 없이 바뀐 데 대응한 것인 경우가 많았습니다(자세한 내용은 [7]를 참조하십시오). 데이터를 최신 상태로 유지하고 시스템 관리자가 데이터 출처를 제어할 수 있도록, 가능하면 언제나 시스템에 배포된 시간대 데이터를 사용할 것을 제안합니다. 그러나 모든 시스템이 공개적으로 접근 가능한 시간대 데이터베이스를 제공하는 것은 아닙니다. 특히 Windows는 시간대를 관리하는 데 다른 시스템을 사용합니다. 그래서 zoneinfo는 PyPI에서 받을 수 있는 설치 가능한 퍼스트 파티 패키지인 tzdata가 있으면 그것을 대체 수단으로 사용합니다. [d] 시스템 zoneinfo 파일을 찾을 수 없지만 tzdata가 설치되어 있다면, 기본 ZoneInfo 생성자는 tzdata를 시간대 출처로 사용합니다.
시스템 시간대 정보
많은 Unix 계열 시스템은 시간대 데이터를 기본으로 배포하거나, 표준 시간대 데이터 패키지(Arch Linux, Fedora, Debian에서처럼 흔히 tzdata라고 부릅니다)를 제공합니다. 가능하면 시스템 시간대 정보를 따르는 것이 바람직합니다. 그렇게 하면 모든 언어 스택의 시간대 정보를 한곳에서 갱신하고 유지 관리할 수 있기 때문입니다. Python 배포자는 가능하면 시간대 데이터가 Python과 함께 설치되도록 하는 것이 좋습니다(예: python 패키지의 의존성으로 tzdata를 선언).
zoneinfo 모듈은 PATH 환경 변수나 Python의 sys.path 변수와 유사한 “검색 경로” 전략을 사용합니다. zoneinfo.TZPATH 변수는 검색할 시간대 데이터 위치를 담은 순서 있는 목록이며, 읽기 전용입니다(자세한 내용은 search-path-config를 참조하십시오). 키로부터 ZoneInfo 인스턴스를 생성하면 경로에서 해당 키가 존재하는 첫 번째 데이터 소스로부터 시간대 파일이 생성됩니다. 예를 들어 TZPATH가 다음과 같고:
TZPATH = (
"/usr/share/zoneinfo",
"/etc/zoneinfo"
)
(매우 드문 경우이겠지만) /usr/share/zoneinfo에는 America/New_York만 있고 /etc/zoneinfo에는 America/New_York와 Europe/Moscow가 모두 있다고 합시다. 이때 ZoneInfo("America/New_York")는 /usr/share/zoneinfo/America/New_York에서 찾고, ZoneInfo("Europe/Moscow")는 /etc/zoneinfo/Europe/Moscow에서 찾습니다.
현재 Windows 시스템에서는 Windows가 시간대 데이터베이스 사본을 공식적으로 제공하지 않으므로 검색 경로의 기본값이 비어 있습니다. Windows가 아닌 시스템에서는 가장 흔히 쓰이는 검색 경로들의 목록이 검색 경로의 기본값입니다. 향후 버전에서 변경될 수 있지만, 처음 도입될 때의 기본 검색 경로는 다음과 같습니다:
TZPATH = (
"/usr/share/zoneinfo",
"/usr/lib/zoneinfo",
"/usr/share/lib/zoneinfo",
"/etc/zoneinfo",
)
이는 컴파일 시점과 런타임 모두에서 구성할 수 있습니다. 구성 옵션에 대한 자세한 정보는 search-path-config를 참조하십시오.
tzdata Python 패키지
모든 최종 사용자가 시간대 데이터에 쉽게 접근할 수 있도록, 이 PEP는 시스템 데이터를 사용할 수 없을 때의 대체 수단으로 데이터 전용 패키지 tzdata를 만들 것을 제안합니다. tzdata 패키지는 CPython 개발 팀이 유지 관리하는 “퍼스트 파티” 패키지 [d]로 PyPI에 배포됩니다.
tzdata 패키지는 데이터와 메타데이터만 포함하며, 외부에 공개된 함수나 클래스는 없습니다. 이 패키지는 새로운 importlib.resources [11] 접근 패턴과 pkgutil.get_data [12]와 같은 이전 접근 패턴 모두와 호환되도록 설계됩니다.
tzdata 패키지는 명시적으로 CPython에서 사용하도록 설계되었지만, 그 자체로 공개 패키지로서 의도된 것이며, 서드파티 Python 패키지를 위한 “공식” 시간대 데이터 출처로 사용될 수 있습니다.
검색 경로 구성
시간대 검색 경로는 시스템에 따라 크게 달라지고 때로는 애플리케이션에 따라서도 달라지므로, 이를 사용자 정의할 수 있는 옵션을 제공하는 것이 합당합니다. 이 PEP는 다음과 같은 세 가지 사용자 정의 방법을 제공합니다:
- 컴파일 타임 옵션을 통한 전역 설정
- 환경 변수를 통한 실행별 설정
reset_tzpath함수를 통한 런타임 설정 변경
모든 설정 방법에서 검색 경로는 상대 경로가 아닌 절대 경로로만 구성되어야 합니다. 구현은 절대 경로가 아닌 문자열을 발견하면 이를 무시하거나, 경고하거나, 예외를 발생시키도록 선택할 수 있습니다(그리고 상황에 따라 다르게 선택할 수도 있습니다 — 예를 들어 잘못된 경로가 reset_tzpath에 전달되면 예외를 발생시키지만, 환경 변수에 포함되어 있으면 경고만 할 수 있습니다). 예외를 발생시키지 않는 경우, 절대 경로가 아닌 문자열은 시간대 검색 경로에 포함되어서는 안 됩니다.
컴파일 타임 옵션
다운스트림 배포자는 자기 시스템의 시간대 데이터가 정확히 어디에 배치되는지 알고 있을 가능성이 가장 높으므로, 기본 검색 경로를 설정하기 위한 컴파일 타임 옵션 PYTHONTZPATH를 제공합니다.
PYTHONTZPATH 옵션은 os.pathsep으로 구분된 문자열이어야 하며, 시간대 데이터가 배치될 수 있는 위치(예: /usr/share/zoneinfo)를 나열합니다.
환경 변수
TZPATH를 초기화할 때(그리고 reset_tzpath가 인자 없이 호출될 때마다), zoneinfo 모듈은 환경 변수 PYTHONTZPATH가 존재하면 이를 사용하여 검색 경로를 설정합니다.
PYTHONTZPATH는 기본 시간대 경로를 (보강하는 것이 아니라) 대체하는 os.pathsep으로 구분된 문자열입니다. 제안된 의미 체계의 몇 가지 예는 다음과 같습니다:
$ python print_tzpath.py
("/usr/share/zoneinfo",
"/usr/lib/zoneinfo",
"/usr/share/lib/zoneinfo",
"/etc/zoneinfo")
$ PYTHONTZPATH="/etc/zoneinfo:/usr/share/zoneinfo" python print_tzpath.py
("/etc/zoneinfo",
"/usr/share/zoneinfo")
$ PYTHONTZPATH="" python print_tzpath.py
()
이러한 사용 사례는 다소 드문 틈새 수요일 가능성이 높으므로, 기본 검색 경로의 앞이나 뒤에 경로를 추가하는 내장 메커니즘은 제공하지 않습니다. 기본 검색 경로로 환경 변수를 채우는 작업은 상당히 쉽게 할 수 있을 것입니다:
$ export DEFAULT_TZPATH=$(python -c \
"import os, zoneinfo; print(os.pathsep.join(zoneinfo.TZPATH))")
reset_tzpath 함수
zoneinfo는 런타임에 검색 경로를 변경할 수 있는 reset_tzpath 함수를 제공합니다.
def reset_tzpath(
to: Optional[Sequence[Union[str, os.PathLike]]] = None
) -> None:
...
이 함수를 경로 시퀀스와 함께 호출하면 zoneinfo.TZPATH를 원하는 값으로 구성한 튜플로 설정합니다. 이 함수를 인자 없이 호출하거나 None으로 호출하면 zoneinfo.TZPATH를 기본 설정으로 재설정합니다.
이 함수는 주로 시스템 시간대 경로의 사용을 (영구적으로 또는 일시적으로) 비활성화하고 모듈이 tzdata 패키지를 사용하도록 강제하는 데 유용할 것입니다. 시간대 설정에 민감한 테스트 함수 정도를 제외하면 reset_tzpath가 흔히 쓰이는 작업이 되지는 않겠지만, TZPATH의 불변성을 우회하는 편법이 난무하도록 두기보다는 이를 변경하는 공식 메커니즘을 제공하는 편이 바람직해 보입니다.
Caution
실행 중에 TZPATH를 변경하는 것은 지원되는 작업이지만, 그렇게 하면 때때로 특이한 의미 체계가 나타날 수 있다는 점을 사용자에게 알려야 하며, 설계상 절충을 할 때에는 훨씬 더 일반적인 사용 사례인 정적 TZPATH 사용에 더 큰 비중을 둘 것입니다.
Constructors에서 언급했듯이, 기본 ZoneInfo 생성자는 동일하게 생성된 두 ZoneInfo 객체가 항상 동일한 것으로 비교되도록(즉, ZoneInfo(key) is ZoneInfo(key)) 캐시를 사용하며, 이 캐시의 특성은 구현에 따라 정의됩니다. 이는 같은 key를 서로 다른 TZPATH 값에서 사용할 경우, 어떤 상황에서는 ZoneInfo 생성자의 동작이 예측할 수 없게 일관되지 않을 수 있다는 뜻입니다. 예를 들면 다음과 같습니다.:
>>> reset_tzpath(to=["/my/custom/tzdb"])
>>> a = ZoneInfo("My/Custom/Zone")
>>> reset_tzpath()
>>> b = ZoneInfo("My/Custom/Zone")
>>> del a
>>> del b
>>> c = ZoneInfo("My/Custom/Zone")
이 예에서 My/Custom/Zone은 /my/custom/tzdb에만 존재하며 기본 검색 경로에는 존재하지 않습니다. 모든 구현에서 a에 대한 생성자는 성공해야 합니다. b에 대한 생성자가 성공하는지는 구현에 따라 정의되지만, 성공한다면 a와 b는 모두 같은 키에 대한 참조이므로 a is b가 참이어야 합니다. c에 대한 생성자가 성공하는지도 구현에 따라 정의됩니다. zoneinfo 구현은 이전 생성자 호출에서 생성된 객체를 반환할 수도 있고, 예외와 함께 실패할 수도 있습니다.
하위 호환성
이 제안은 새로운 API를 만드는 것이므로 하위 호환성 문제가 없습니다.
약간만 수정하면 Python 3.6 이상을 지원하는 zoneinfo 모듈의 백포트를 만들 수 있습니다.
tzdata 패키지는 “데이터 전용”으로 설계되었으며, 빌드할 수 있는 모든 Python 버전(Python 2.7 포함)을 지원해야 합니다.
보안 영향
이 제안을 구현하려면 디스크에서 zoneinfo 데이터를 파싱해야 합니다. 이 데이터는 대부분 시스템 위치에서 읽지만, 사용자가 제공한 데이터일 수도 있습니다. 구현상의 오류(특히 C 코드의 오류)가 보안 문제로 이어질 가능성은 있지만, 다른 파일 형식을 파싱하는 경우보다 특별히 더 위험하지는 않습니다.
시간대 데이터 키는 사실상 어떤 시간대 루트를 기준으로 한 상대 경로이므로, 구현에서는 경로 탐색 공격을 막도록 주의해야 합니다. ../../../path/to/something과 같은 키를 요청하더라도 시간대 경로 바깥에 있는 파일 시스템의 상태에 관한 정보가 전혀 드러나지 않아야 합니다.
참조 구현
초기 참조 구현은 https://github.com/pganssle/zoneinfo 에서 볼 수 있습니다.
이 구현은 나중에 3.6 이상용 백포트로 바뀔 수 있습니다.
기각된 아이디어
자체 tzdb 컴파일러 만들기
TZif 형식을 사용할 때의 주요 우려 사항 하나는 tzinfo.dst()가 반환할 값을 항상 올바르게 결정하는 데 필요한 정보가 이 형식에 실제로는 충분히 들어 있지 않다는 점입니다. 그 이유는 TZif가 각 시간대 오프셋에 대해 UTC 오프셋과 그 오프셋이 DST 오프셋인지 여부만 기록하는 반면, tzinfo.dst()는 DST 이동량 전체를 반환하여 datetime.utcoffset() - datetime.dst()로 “표준” 오프셋을 다시 구할 수 있게 하기 때문입니다. dst()에 사용할 값은 대응하는 STD 오프셋을 찾아 그 차이를 계산하면 구할 수 있습니다. 하지만 TZif 형식은 어떤 오프셋끼리 STD/DST 쌍을 이루는지 명시하지 않으므로, 이를 판단하려면 휴리스틱을 써야 합니다.
흔히 쓰는 휴리스틱 하나인 가장 최근의 표준 오프셋을 참고하는 방식은 특히 1992년과 1996년 포르투갈의 시간대 변경에서 실패합니다. 당시에는 DST 전환 중에 “표준” 오프셋이 1시간 옮겨졌기 때문에, 오프셋은 바뀌지 않은 채 STD 상태에서 DST 상태로 전환되었습니다. 사실 영구적으로 DST이고 표준 오프셋이 전혀 없는 시간대가 생길 수도 있습니다(실제로 그런 일이 있었던 적은 없습니다).
이 정보는 컴파일된 TZif 바이너리에는 빠져 있지만 원본 tzdb 파일에는 들어 있으므로, 이 정보를 직접 파싱해서 더 적합한 바이너리 형식을 만들 수도 있습니다.
이 아이디어는 다음과 같은 여러 이유로 기각되었습니다.
- 이 방식을 쓰면 시스템에 배포된 시간대 정보를 전혀 쓸 수 없게 됩니다. 시스템에 배포된 시간대 정보는 보통 TZif 형식으로만 존재합니다.
- 원본 tzdb 형식은 안정적이기는 하지만 TZif 형식보다는 덜 안정적입니다. 일부 다운스트림 tzdb 파서는 예전에 배포된 자체 파서가 최신 tzdb 릴리스와 호환되지 않는 문제를 이미 겪었으며, 이 때문에 전환을 쉽게 하려고 “rearguard” 형식이 만들어졌습니다. [8]
- 현재
dateutil과pytz에서는 과거와 현재의 알려진 모든 시간대에 대해 휴리스틱으로 충분하며, 휴리스틱으로 포착할 수 없는 새로운 시간대가 등장할 가능성은 그리 높지 않습니다. 다만 현재 세대의 휴리스틱으로 포착되지 않는 새로운 규칙이 등장할 가능성은 다소 더 높으며, 그 경우에는 바뀐 상황에 대응하기 위한 버그 수정이 필요합니다. - 애초에
dst()메서드의 유용성(그리고 사실 TZif의isdst매개변수의 유용성)부터가 다소 의문스럽습니다. 유용한 정보는 거의 모두utcoffset()과tzname()메서드에 들어 있으며, 이 메서드들에는 같은 문제가 없기 때문입니다.
요컨대 자체 tzdb 컴파일러나 컴파일된 패키지를 유지하면 CPython 개발 팀과 시스템 관리자 모두의 유지 보수 부담이 늘어납니다. 반면 주된 이점은 가상의 실패 사례에 대응하는 것인데, 그런 실패는 실제로 일어나더라도 현실에 미치는 영향이 아주 작을 것입니다.
tzdata를 표준 라이브러리에 기본으로 포함하기
CPython에 ensurepip 메커니즘을 도입한 PEP 453이 PyPI에서 유지 관리되는 표준 라이브러리 모듈을 위한 편리한 템플릿을 제공하기는 하지만, 이와 유사한 ensuretzdata 메커니즘은 상대적으로 덜 필요하며 상당히 복잡해질 것이므로 이 PEP의 범위를 벗어나는 것으로 간주합니다.
zoneinfo 모듈은 가능하면 언제나 시스템 시간대 데이터를 쓰도록 설계되었습니다. 따라서 시간대 데이터를 배포하는 시스템에서는 tzdata 패키지가 필요하지 않으며(오히려 바람직하지 않을 수도 있으며), 그러므로 tzdata를 CPython과 함께 배포하는 것이 꼭 필요해 보이지는 않습니다.
또한 이렇게 표준 라이브러리와 PyPI에 걸친 혼합형 모듈을 어떻게 업데이트해야 하는지도 아직 분명하지 않습니다(업데이트와 알림을 위한 자연스러운 메커니즘을 갖춘 pip은 예외입니다). 게다가 이 패키지는 모듈이 동작하는 데 꼭 필요하지 않으므로, 이런 제안은 미루는 것이 현명해 보입니다.
윤초 지원
IANA 시간대 데이터베이스는 시간대 오프셋과 이름 규칙 외에 윤초 데이터의 출처도 제공합니다. datetime.datetime은 현재 윤초를 지원하지 않으며, 윤초 데이터 문제는 윤초 지원이 추가될 때까지 미룰 수 있으므로 이는 범위 밖으로 간주됩니다.
퍼스트 파티 tzdata 패키지는 zoneinfo 모듈에서 사용하지 않더라도 윤초 데이터를 함께 제공해야 합니다.
pytz와 유사한 인터페이스 사용
pytz ([17])와 유사한 인터페이스가 PEP 431에서 제안되었지만, 모호한 datetime을 지원하지 못한다는 이유로 결국 철회/거부되었습니다. PEP 495는 이 문제를 해결하기 위해 fold 속성을 추가했지만, fold로 인해 pytz의 비표준 tzinfo 클래스가 필요 없어졌으므로 pytz와 유사한 인터페이스는 더 이상 필요하지 않습니다. [2]
zoneinfo 방식은 dateutil.tz에 더 밀접하게 기반을 두고 있으며, dateutil.tz는 Python 3.6 출시 직전에 fold 지원(이전 버전으로의 백포트 포함)을 구현했습니다.
Microsoft의 ICU API를 통한 Windows 지원
Windows는 시간대 데이터베이스를 TZif 파일로 제공하지 않지만, Windows 10의 2017 Creators Update부터 Microsoft는 유니코드 국제 컴포넌트(International Components for Unicode, ICU) 프로젝트 [13] [14] 와 상호 작용하기 위한 API를 제공하고 있습니다. 여기에는 IANA 시간대 데이터베이스에서 가져온 시간대 데이터에 접근하기 위한 API가 포함되어 있습니다. [15]
이에 대한 바인딩을 제공하면 tzdata 패키지를 설치하지 않고도 Windows를 “별도 설정 없이” 지원할 수 있지만, 안타깝게도 Windows에서 제공하는 C 헤더로는 기반 시간대 데이터에 전혀 접근할 수 없습니다 — 시스템에 전환 및 오프셋 정보를 질의하는 API만 사용할 수 있습니다. 이로 인해 ICU 기반 구현의 의미론이 ICU를 사용하지 않는 구현과 호환되지 않을 수 있는 방식으로 제약되며, 특히 캐시 동작과 관련하여 그렇습니다.
ICU를 ZoneInfo 객체의 단순한 추가 데이터 소스로 사용할 수는 없는 것으로 보이므로, 이 PEP는 ICU 지원을 범위 밖으로 간주하며, 이는 서드 파티 라이브러리에서 지원하는 편이 아마 더 나을 것입니다.
대안적인 환경 변수 구성
이 PEP는 단일 환경 변수 PYTHONTZPATH를 사용할 것을 제안합니다. 이는 시간대 경로를 조작하려는 사용자 대부분이 경로를 완전히 대체하기를 원할 것이며(예: “내 시간대 데이터가 정확히 어디에 있는지 알고 있습니다”), 기존 검색 경로 앞에 경로를 추가하는 것과 같은 다른 사용 사례는 덜 흔할 것이라는 가정에 기반합니다.
검토되었으나 거부된 다른 방식이 몇 가지 있습니다:
PYTHON_TZPATH를 두 개의 환경 변수DEFAULT_PYTHONTZPATH와PYTHONTZPATH로 분리합니다. 여기서PYTHONTZPATH는 기본 시간대 경로 뒤에 덧붙일(또는 앞에 붙일) 값을 담고,DEFAULT_PYTHONTZPATH는 기본 시간대 경로를 대체합니다. 주된 사용 사례가 확장이 아니라 대체라면 사용자에게 혼란을 줄 가능성이 높으므로 이 방식은 거부되었습니다.PYTHONTZPATH_PREPEND나PYTHONTZPATH_APPEND중 하나 또는 둘 다를 추가하여, 사용자가 기본 시간대 경로가 무엇인지 알아내려 하지 않고도 검색 경로의 양쪽 끝을 확장할 수 있게 합니다. 이 방식은 불필요할 가능성이 높고, 그러한 기능에 대한 수요가 많다면 향후 업데이트에서 하위 호환성을 유지하는 방식으로 쉽게 추가할 수 있으므로 거부되었습니다.PYTHONTZPATH변수만 사용하되, 기본 시간대 경로를 나타내는 사용자 정의 특수 값(예:<<DEFAULT_TZPATH>>)을 제공하여 사용자가 시간대 경로에 경로를 덧붙일 수 있게 합니다. 예를 들어PYTHONTZPATH=<<DEFAULT_TZPATH>>:/my/path를 사용하면 시간대 경로 끝에/my/path를 덧붙일 수 있습니다.이 방식의 한 가지 장점은 검색 경로에 파일 기반이 아닌 요소를 지정하기 위한 자연스러운 확장 지점이 생긴다는 것입니다. 예를 들어
tzdata가 존재할 경우 그 우선순위를 변경하거나, 향후 라이브러리에 TZDIST에 대한 네이티브 지원이 추가될 경우가 이에 해당합니다.이 방식이 거부된 주된 이유는 이러한 종류의 특수 값이
PATH와 같은 변수에서는 일반적으로 찾아볼 수 없고, 현재 제안된 유일한 사용 사례가 기본TZPATH를 대신하는 것인데, 이 값은 Python 프로그램을 실행하여 기본값을 조회하는 방식으로 얻을 수 있기 때문입니다. 이 방식을 거부한 또 다른 요인은,PYTHONTZPATH가 절대 경로만 허용하므로 유효한 절대 경로를 나타내지 않는 문자열은 모두 암묵적으로 향후 사용을 위해 예약되어 있어, 향후 버전의 라이브러리에서 필요에 따라 이러한 특수 값을 하위 호환성을 유지하는 방식으로 도입할 수 있다는 점입니다.
datetime 모듈 사용
한 가지 가능한 아이디어는 ZoneInfo를 별도의 모듈로 제공하는 대신 datetime 모듈에 추가하는 것입니다. 이 PEP는 별도의 zoneinfo 모듈을 사용하는 쪽을 선호하지만, 중첩된 datetime.zoneinfo 모듈도 검토 대상이었습니다.
ZoneInfo를 datetime에 직접 넣는 것에 반대하는 인자
datetime 모듈에는 다소 복잡하게 동작하는 클래스가 이미 많이 있어서 모듈이 다소 붐비는 상태입니다 — datetime.datetime, datetime.date, datetime.time, datetime.timedelta, datetime.timezone, datetime.tzinfo가 그렇습니다. 모듈의 구현과 문서는 이미 상당히 복잡하므로, 피할 수 있다면 문제를 더 키우지 않도록 하는 편이 아마 유익할 것입니다.
ZoneInfo 클래스는 또한 몇 가지 면에서 datetime이 제공하는 다른 모든 클래스와 다릅니다. 다른 클래스는 모두 가볍고 단순한 데이터 타입으로 의도된 반면, ZoneInfo 클래스는 더 복잡합니다. 이 클래스는 특정 형식(TZif)의 파서이자, 그 형식에 저장된 정보의 표현이며, 시스템의 잘 알려진 위치에서 정보를 조회하는 메커니즘이기도 합니다.
마지막으로, zoneinfo 모듈이 필요한 사람은 datetime 모듈도 필요하다는 것은 사실이지만, 그 반대가 반드시 참인 것은 아닙니다. 많은 사람이 zoneinfo 없이 datetime을 사용하고 싶어 할 것입니다. zoneinfo가 추가적인, 어쩌면 더 무거운 표준 라이브러리 모듈을 끌어올 가능성이 높다는 점을 고려하면, 두 모듈을 따로 임포트할 수 있게 하는 편이 바람직합니다 — 특히 향후 Python에 “트리 셰이킹” 배포판이 등장할 가능성이 있다면 더욱 그렇습니다. [9]
결국 zoneinfo의 클래스와 함수를 datetime에 직접 넣기보다는 zoneinfo를 별도의 문서 페이지를 가진 별도 모듈로 유지하는 것이 타당합니다.
zoneinfo 대신 datetime.zoneinfo 사용하기
더 받아들이기 쉬운 구성은 zoneinfo를 datetime 아래의 모듈인 datetime.zoneinfo로 중첩하는 것일 수 있습니다.
이 방식에 찬성하는 인자:
zoneinfo를datetime과 함께 하나의 네임스페이스로 깔끔하게 묶습니다.timezone클래스는 이미datetime에 있으므로, 어떤 시간대는datetime에 있고 다른 시간대는 최상위 모듈에 있다는 것이 이상하게 보일 수 있습니다.- 앞서 언급했듯이
zoneinfo를 임포트하려면 반드시datetime을 임포트해야 하므로, 부모 모듈을 임포트하도록 요구해도 부담이 되지 않습니다.
이 방식에 반대하는 인자:
- 모든
datetime사용자에게zoneinfo임포트를 강제하지 않으려면zoneinfo모듈을 지연 임포트해야 하며, 이는 최종 사용자가 (datetime을 임포트한 뒤 모듈의zoneinfo속성에 접근하는 대신)datetime.zoneinfo를 명시적으로 임포트해야 한다는 뜻입니다.dateutil이 이런 방식으로 동작하며(모든 하위 모듈이 지연 임포트됩니다), 이는 최종 사용자에게 끊임없는 혼란의 원인이 되고 있습니다.최종 사용자에게 요구되는 이 혼란스러운 요건은 PEP 562에 따라 모듈 수준의
__getattr__과__dir__를 사용하면 피할 수 있지만, 이렇게 하면datetime모듈의 구현이 다소 복잡해집니다. 모듈이나 클래스의 이러한 동작은 정적 분석 도구를 혼란스럽게 하는 경향이 있으며, 이는datetime처럼 널리 사용되고 중요한 라이브러리에는 바람직하지 않을 수 있습니다. - 구현을
datetime아래에 중첩하려면datetime을 단일 파일 모듈(datetime.py)에서__init__.py가 있는 디렉터리로 재구성해야 할 가능성이 높습니다. 이는 사소한 문제이지만,datetime모듈의 구조는 수년 동안 안정적으로 유지되어 왔으므로 가능하다면 불필요한 변경은 피하는 것이 바람직합니다.이 문제는
zoneinfo를_zoneinfo.py로 구현하고datetime내부에서 이를zoneinfo로 임포트하는 방식으로 완화될 수도 있지만, 이는 미적 관점이나 코드 구성 관점에서 바람직해 보이지 않으며, 최종 사용자가datetime.zoneinfo를 명시적으로 임포트해야 하는 방식의 중첩을 불가능하게 만듭니다.
이 PEP는 중첩의 이점이 실제 구현상의 우려를 압도할 만큼 크지 않으므로, 종합적으로 볼 때 별도의 최상위 zoneinfo 모듈을 사용하는 것이 최선이라는 입장을 취합니다.
각주
dateutil provides many time zone types,
but user support mostly focuses on these three types.ZoneInfo objects should be
identical objects may be violated if the user deliberately clears the time
zone cache.datetime is cached on first calculation, so
we do not need to worry about the possibly more serious issue that a given
datetime object’s hash would change during its lifetime.참고 문헌
기타 시간대 구현:
dateutil.tz.win: Windows 시간대를 감싸는 구체적인 시간대 구현- 시간대 https://dateutil.readthedocs.io/en/stable/tzwin.html
dateutil.tz
https://dateutil.readthedocs.io/en/stable/tz.htmlpytz
http://pytz.sourceforge.net/Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.