Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at zoneinfo.

×

See PEP 1 for how to propose changes.

초록

이는 IANA 시간대 데이터베이스를 지원하는 구체적인 시간대 구현을 제공하기 위해 zoneinfo 모듈을 추가할 것을 제안합니다. 기본적으로 zoneinfo는 시스템 시간대 데이터를 사용할 수 있는 경우 이를 사용합니다. 시스템 시간대 데이터를 사용할 수 없는 경우 라이브러리는 PyPI에 배포된 공식 패키지 tzdata를 대신 사용합니다. [d]

동기

datetime라이브러리는 시간대를 처리하기 위해 유연한 메커니즘을 사용합니다. 모든 변환과 시간대 정보 조회는 추상 datetime.tzinfo베이스 클래스의 서브클래스 인스턴스에 위임됩니다. [10] 이를 통해 사용자는 임의로 복잡한 시간대 규칙을 구현할 수 있지만, 실제로 대부분의 사용자는 다음 세 가지 시간대 유형만 지원받기를 원합니다. [a]

  1. UTC 및 그에 해당하는 고정 오프셋
  2. 시스템 로컬 시간대
  3. IANA 시간대

Python 3.2에서는 첫 번째 유형의 시간대를 지원하기 위해 datetime.timezone클래스가 도입되었습니다(UTC를 위한 특별한 datetime.timezone.utc싱글턴과 함께).

여전히 “로컬” 시간대는 없지만, Python 3.0에서는 순진한 시간대의 의미가 여러 “로컬 시간” 연산을 지원하도록 변경되었으며, 이제 로컬 시간에서 고정 시간대 오프셋을 가져올 수 있습니다.:

>>> 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의 주요 관심사는 세 가지입니다.

  1. zoneinfo.ZoneInfo 클래스의 의미론 (zoneinfo-class)
  2. 사용되는 시간대 데이터 소스 (data-sources)
  3. 시간대 검색 경로를 구성하는 옵션 (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 객체를 무기한 유지하는 “강한” 캐시로 이를 구현하는 것도 허용되지만, 필수 사항은 아니며 권장되지도 않습니다.

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 객체의 동작이 수명 동안 변경되지 않는 것이 중요합니다. datetimeutcoffset() 메서드는 동등성 및 해시 계산에 모두 사용되며, 그 결과가 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(Unicode Common Locale Data Repository [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이 됩니다.

Pickle 직렬화

모든 전환 데이터를 직렬화하는 대신, ZoneInfo 객체는 키를 기준으로 직렬화되며, 원시 파일에서 생성된 ZoneInfo 객체는 (key에 값이 지정된 객체도 포함하여) 피클링할 수 없습니다.

ZoneInfo 객체의 동작은 객체가 어떻게 생성되었는지에 따라 달라집니다.

  1. ZoneInfo(key): 기본 생성자로 생성된 경우 ZoneInfo 객체는 키를 기준으로 직렬화되며, 역직렬화할 때 역직렬화 프로세스에서 기본 생성자를 사용하므로 동일한 시간대에 대한 다른 참조와 동일한 객체일 것으로 예상됩니다. 예를 들어, europe_berlin_pklZoneInfo("Europe/Berlin")에서 생성된 피클을 포함하는 문자열이라면 다음과 같은 동작이 예상됩니다.
    >>> a = ZoneInfo("Europe/Berlin")
    >>> b = pickle.loads(europe_berlin_pkl)
    >>> a is b
    True
    
  2. ZoneInfo.no_cache(key): 캐시를 우회하는 생성자로 생성된 경우에도 ZoneInfo 객체는 키를 기준으로 직렬화되지만, 역직렬화할 때는 캐시를 우회하는 생성자를 사용합니다. europe_berlin_pkl_ncZoneInfo.no_cache("Europe/Berlin")에서 생성된 피클을 포함하는 문자열이라면 다음과 같은 동작이 예상됩니다.
    >>> a = ZoneInfo("Europe/Berlin")
    >>> b = pickle.loads(europe_berlin_pkl_nc)
    >>> a is b
    False
    
  3. 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 모듈은 Python의 PATH 환경 변수 또는 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_YorkEurope/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] 와 같은 이전 접근 패턴 모두와 호환되도록 설계됩니다.

CPython에서 사용하도록 명시적으로 설계되었지만, tzdata 패키지는 그 자체로 공개 패키지이며 타사 Python 패키지에서 시간대 데이터의 “공식” 소스로 사용할 수 있습니다.

검색 경로 구성

시간대 검색 경로는 시스템에 매우 의존적이며, 때로는 애플리케이션에도 의존하므로 이를 사용자 정의할 수 있는 옵션을 제공하는 것이 타당합니다. 이 PEP는 이러한 사용자 정의를 위한 세 가지 방법을 제공합니다:

  1. 컴파일 시 옵션을 통한 전역 구성
  2. 환경 변수를 통한 실행별 구성
  3. 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)). 이 캐시의 특성은 구현에 따라 정의됩니다. 이는 서로 다른 TZPATH값에서 동일한 key를 사용하면 일부 상황에서 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의 생성자가 성공하는지는 구현에 따라 정의되지만, 성공한다면 ab가 동일한 키에 대한 참조이므로 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 파일에는 있으며, 이 정보를 직접 구문 분석하여 더 적합한 바이너리 형식을 만드는 것도 가능합니다.

이 아이디어는 여러 가지 이유로 거부되었습니다.

  1. 이는 일반적으로 TZif 형식으로만 제공되는 시스템 배포 시간대 정보를 사용할 수 없게 합니다.
  2. 원시 tzdb 형식은 안정적이기는 하지만 TZif 형식보다 안정적입니다. 일부 다운스트림 tzdb 파서는 사용자 지정 파서의 오래된 배포본이 최신 tzdb 릴리스와 호환되지 않게 되면서 이미 문제를 겪었으며, 이로 인해 전환을 용이하게 하기 위한 “rearguard” 형식이 만들어졌습니다. [8]
  3. 현재 알려진 모든 시간대(과거 및 현재)에 대해서는 dateutilpytz에서 휴리스틱으로 충분하며, 휴리스틱으로 포착할 수 없는 새로운 시간대가 나타날 가능성은 높지 않습니다. 다만 현재 휴리스틱 세대가 포착하지 못하는 새로운 규칙이 나타날 가능성은 다소 더 높으며, 그런 경우에는 변경된 상황에 대응하기 위한 버그 수정이 필요합니다.
  4. dst()메서드의 유용성(실제로 TZif의 isdst 매개변수도 마찬가지임)은 애초에 다소 의문스럽습니다. 유용한 정보의 거의 전부가 utcoffset()tzname() 메서드에 포함되어 있으며, 이 메서드들은 동일한 문제의 영향을 받지 않기 때문입니다.

요컨대 사용자 지정 tzdb 컴파일러 또는 컴파일된 패키지를 유지 관리하면 CPython 개발 팀과 시스템 관리자 모두에게 유지 관리 부담이 추가되며, 그 주된 이점은 실제로 발생하더라도 현실 세계에 미치는 영향이 최소화될 가능성이 높은 가상의 오류에 대응하는 것입니다.

기본적으로 표준 라이브러리에 tzdata 포함하기

CPython에 ensurepip 메커니즘을 도입한 PEP 453은 PyPI에서 유지 관리되는 표준 라이브러리 모듈에 편리한 본보기를 제공하지만, 이와 유사할 수 있는 ensuretzdata 메커니즘은 그다지 필요하지 않으며 충분히 복잡하므로 이 PEP의 범위를 벗어나는 것으로 간주됩니다.

zoneinfo 모듈은 가능한 경우 시스템 시간대 데이터를 사용하도록 설계되었으므로, 시간대 데이터를 배포하는 시스템에서는 tzdata 패키지가 필요하지 않으며(바람직하지 않을 수도 있으므로), 따라서 CPython과 함께 tzdata를 제공하는 것이 중요해 보이지 않습니다.

또한 이러한 하이브리드 표준 라이브러리/PyPI 모듈을 어떻게 업데이트해야 하는지도 아직 명확하지 않습니다(pip 외에는 업데이트 및 알림을 위한 자연스러운 메커니즘이 없음). 모듈의 작동에 필수적인 사항도 아니므로 그러한 제안은 보류하는 것이 현명해 보입니다.

윤초 지원

IANA 시간대 데이터베이스는 시간대 오프셋 및 이름 규칙뿐 아니라 윤초 데이터의 출처도 제공합니다. 이는 datetime.datetime이 현재 윤초를 지원하지 않으며 윤초 데이터에 관한 문제는 윤초 지원이 추가될 때까지 미룰 수 있기 때문에 범위 밖으로 간주합니다.

최초 제공자인 tzdata 패키지는 zoneinfo 모듈에서 사용하지 않더라도 윤초 데이터를 제공해야 합니다.

pytz와 유사한 인터페이스 사용

pytz와 유사한 ([17]) 인터페이스가 PEP 431에서 제안되었지만, 모호한 datetime 지원이 부족하여 결국 철회/거부되었습니다. PEP 495에서는 이 문제를 해결하기 위해 fold속성을 추가했지만, fold로 인해 pytz의 비표준 tzinfo 클래스가 필요하지 않게 되었으므로 pytz와 유사한 인터페이스는 더 이상 필요하지 않습니다. [2]

zoneinfo 방식은 Python 3.6 출시 직전에 fold지원을 구현한 dateutil.tz를 더욱 밀접하게 기반으로 하며, 여기에는 이전 버전으로의 백포트도 포함됩니다.

Microsoft의 ICU API를 통한 Windows 지원

Windows는 시간대 데이터베이스를 TZif 파일로 제공하지 않지만, Windows 10의 2017 Creators Update부터 Microsoft는 IANA 시간대 데이터베이스에서 가져온 시간대 데이터에 액세스하는 API를 포함하여 International Components for Unicode(ICU) 프로젝트와 상호 작용하기 위한 API를 제공해 왔습니다 [13] [14] . [15]

이에 대한 바인딩을 제공하면 tzdata 패키지를 설치하지 않고도 Windows를 “즉시 사용 가능한” 상태로 지원할 수 있지만, 안타깝게도 Windows가 제공하는 C 헤더는 기반 시간대 데이터에 액세스할 방법을 제공하지 않으며, 전환 및 오프셋 정보를 시스템에 질의하는 API만 사용할 수 있습니다. 이로 인해 ICU 기반 구현의 의미론은 비-ICU 기반 구현과 호환되지 않을 수 있는 방식으로 제한되며, 특히 캐시의 동작과 관련하여 그러합니다.

ICU를 ZoneInfo 객체의 단순한 추가 데이터 소스로 사용할 수 없는 것으로 보이므로, 이 PEP에서는 ICU 지원을 범위 밖으로 간주하며 서드 파티 라이브러리에서 지원하는 편이 더 나을 것으로 봅니다.

대체 환경 변수 구성

이 PEP에서는 단일 환경 변수인 PYTHONTZPATH를 사용할 것을 제안합니다. 이는 시간대 경로를 조작하려는 사용자 대다수가 해당 경로를 완전히 대체하기를 원할 것이라는 가정에 기반합니다(예: “내 시간대 데이터가 정확히 어디에 있는지 알고 있습니다”). 기존 검색 경로 앞에 추가하는 것과 같은 다른 사용 사례는 덜 일반적일 것입니다.

검토되었지만 거부된 몇 가지 다른 방식이 있습니다.

  1. PYTHON_TZPATH를 두 환경 변수인 DEFAULT_PYTHONTZPATHPYTHONTZPATH로 분리합니다. 여기서 PYTHONTZPATH는 기본 시간대 경로에 추가할(또는 앞에 삽입할) 값을 포함하고, DEFAULT_PYTHONTZPATH는 기본 시간대 경로를 대체합니다. 주요 사용 사례가 확장이 아니라 대체인 경우 사용자에게 혼란을 일으킬 가능성이 높기 때문에 거부되었습니다.
  2. PYTHONTZPATH_PREPEND, PYTHONTZPATH_APPEND 중 하나 또는 둘 다를 추가하여 사용자가 기본 시간대 경로가 무엇인지 확인하지 않고도 검색 경로의 양쪽 끝에 경로를 확장할 수 있도록 합니다. 불필요할 가능성이 높고, 이러한 기능에 대한 수요가 많아질 경우 향후 업데이트에서 하위 호환성을 유지하는 방식으로 쉽게 추가할 수 있기 때문에 거부되었습니다.
  3. PYTHONTZPATH 변수만 사용하되 기본 시간대 경로를 나타내는 사용자 지정 특수 값(예: <<DEFAULT_TZPATH>>)을 제공하여, 사용자가 시간대 경로에 값을 추가할 수 있도록 합니다. 예를 들어 PYTHONTZPATH=<<DEFAULT_TZPATH>>:/my/path를 사용하여 /my/path를 시간대 경로 끝에 추가할 수 있습니다.

    이 방식의 한 가지 장점은 검색 경로에서 파일 기반이 아닌 요소를 지정하기 위한 자연스러운 확장 지점을 추가한다는 것입니다. 예를 들어 tzdata가 존재할 경우 그 우선순위를 변경하거나, 향후 라이브러리에 TZDIST에 대한 네이티브 지원이 추가되는 경우가 이에 해당합니다.

    이러한 종류의 특수 값은 일반적으로 PATH와 유사한 변수에서 발견되지 않으며, 현재 제안된 유일한 사용 사례도 기본 TZPATH를 대신하는 값이기 때문에 주로 거부되었습니다. 기본값은 기본값을 질의하는 Python 프로그램을 실행하여 얻을 수 있습니다. 이를 거부한 추가적인 요인은 PYTHONTZPATH가 절대 경로만 허용한다는 점입니다. 따라서 유효한 절대 경로를 나타내지 않는 모든 문자열은 암묵적으로 향후 사용을 위해 예약되며, 라이브러리의 향후 버전에서 필요에 따라 이러한 특수 값을 하위 호환성을 유지하는 방식으로 도입할 수 있습니다.

datetime 모듈 사용

한 가지 가능한 방안은 ZoneInfo를 별도의 모듈로 제공하는 대신 datetime 모듈에 추가하는 것입니다. 이 PEP에서는 별도의 zoneinfo 모듈을 사용하는 방식을 선호하지만, 중첩된 datetime.zoneinfo 모듈도 고려 대상이었습니다.

ZoneInfodatetime에 직접 배치하는 것에 대한 반론

datetime 모듈은 이미 다소 복잡합니다. 어느 정도 복잡한 동작을 하는 많은 클래스를 포함하고 있기 때문이며, 여기에는 datetime.datetime, datetime.date, datetime.time, datetime.timedelta, datetime.timezonedatetime.tzinfo가 있습니다. 모듈의 구현과 문서도 이미 상당히 복잡하므로, 피할 수 있다면 문제를 더욱 악화하지 않도록 하는 편이 바람직할 것입니다.

ZoneInfo 클래스는 다른 datetime 제공 클래스와 여러 면에서 다릅니다. 다른 클래스는 모두 간결하고 단순한 데이터 형식을 의도하지만, ZoneInfo 클래스는 더 복잡합니다. 특정 형식(TZif)을 위한 파서이자 해당 형식에 저장된 정보의 표현이며, 시스템의 잘 알려진 위치에서 정보를 조회하는 메커니즘이기 때문입니다.

마지막으로 zoneinfo 모듈이 필요한 사람에게 datetime 모듈도 필요하다는 것은 사실이지만, 그 역은 반드시 참이 아닙니다. 많은 사람이 zoneinfo없이 datetime을 사용하려 할 것이기 때문입니다. zoneinfo가 추가적인, 어쩌면 더 무거운 표준 라이브러리 모듈을 가져올 가능성을 고려하면, 두 모듈을 별도로 가져올 수 있도록 하는 편이 바람직합니다. 특히 잠재적인 “트리 셰이킹” 배포판이 Python의 미래에 포함될 수 있다는 점에서 그러합니다. [9]

최종적으로 보면, zoneinfo를 별도의 문서 페이지와 함께 별도 모듈로 유지하는 것이 해당 클래스와 함수를 datetime에 직접 넣는 것보다 타당합니다.

zoneinfo 대신 datetime.zoneinfo 사용

더 수용하기 쉬운 구성은 zoneinfodatetime아래의 모듈로 중첩하여 datetime.zoneinfo로 만드는 것일 수 있습니다.

이에 찬성하는 근거:

  1. zoneinfodatetime와 함께 깔끔하게 네임스페이스화합니다.
  2. timezone 클래스는 이미 datetime에 있으며, 일부 시간대는 datetime에 있고 다른 시간대는 최상위 모듈에 있다는 점이 이상하게 보일 수 있습니다.
  3. 앞서 언급했듯이, zoneinfo를 가져오려면 반드시 datetime을 가져와야 하므로 부모 모듈을 가져오도록 요구해도 부담이 되지 않습니다.

이에 반대하는 근거:

  1. 모든 datetime 사용자에게 zoneinfo 모듈을 가져오도록 강제하지 않으려면 zoneinfo 모듈을 지연해서 가져와야 하며, 이는 최종 사용자가 datetime.zoneinfo를 명시적으로 가져와야 한다는 뜻입니다 (datetime를 가져온 다음 모듈의 zoneinfo속성에 접근하는 것과는 반대입니다). 이는 dateutil이 작동하는 방식이며 (모든 하위 모듈을 지연해서 가져옵니다), 최종 사용자에게 지속적으로 혼란을 일으키는 원인입니다.

    최종 사용자에게 이런 혼란스러운 요구 사항은 PEP 562에 따라 모듈 수준의 __getattr____dir__을 사용하면 피할 수 있지만, 이렇게 하면 datetime 모듈의 구현에 약간의 복잡성이 추가될 것입니다. 모듈이나 클래스에서 이런 종류의 동작은 정적 분석 도구를 혼란스럽게 하는 경향이 있으며, 이는 datetime만큼 널리 쓰이고 중요한 라이브러리에는 바람직하지 않을 수 있습니다.

  2. datetime 아래에 구현을 중첩시키려면 datetime을 단일 파일 모듈(datetime.py)에서 __init__.py를 가진 디렉터리로 재구성해야 할 가능성이 높습니다. 이는 사소한 문제이지만, datetime 모듈의 구조는 수년간 안정적으로 유지되어 왔으며, 가능하다면 불필요한 변경을 피하는 것이 바람직할 것입니다.

    이 문제는 zoneinfo_zoneinfo.py로 구현하고 datetime 내부에서 이를 zoneinfo로 임포트함으로써 완화 수도 있지만, 이는 미학적 또는 코드 구성 관점에서 바람직해 보이지 않으며, 최종 사용자가 datetime.zoneinfo를 명시적으로 임포트해야 하는 형태의 중첩을 배제하게 될 것입니다.

이 PEP는 중첩의 이점이 실질적인 구현상의 문제를 압도할 만큼 크지 않기 때문에, 전체적으로 볼 때 별도의 최상위 zoneinfo 모듈을 사용하는 것이 최선이라는 입장을 취합니다.

각주

[a]
The claim that the vast majority of users only want a few types of time zone is based on anecdotal impressions rather than anything remotely scientific. As one data point, dateutil provides many time zone types, but user support mostly focuses on these three types.
[b]
The statement that identically constructed ZoneInfo objects should be identical objects may be violated if the user deliberately clears the time zone cache.
[c]
The hash value for a given 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.
[d] (1, 2, 3)
The term “first party” here is distinguished from “third party” in that, although it is distributed via PyPI and is not currently included in Python by default, it is to be considered an official sub-project of CPython rather than a “blessed” third-party package.

참고 문헌

기타 시간대 구현: