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

Python 개선 제안 한국어 번역

PEP 495 – 지역 시간 모호성 해소

Author:
Alexander Belopolsky <alexander.belopolsky at gmail.com>, Tim Peters <tim.peters at gmail.com>
Discussions-To:
Datetime-SIG list
Status:
Final
Type:
Standards Track
Created:
02-Aug-2015
Python-Version:
3.6
Resolution:
Datetime-SIG message

Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 datetime.time과 datetime.datetime 클래스의 인스턴스에 새 속성 fold를 추가합니다. 이 속성은 지역 시간이 같은 두 시점을 구별하는 데 사용할 수 있습니다. fold 속성에 허용되는 값은 0과 1이며, 모호한 지역 시간을 해석할 수 있는 두 가지 방법 중 0은 더 이른 시점에, 1은 더 늦은 시점에 해당합니다.

근거

세계 대부분의 지역에서는 지역 시계를 뒤로 돌리는 때가 있었고 앞으로도 있을 것입니다. [1] 그런 때에는 같은 날 지역 시계가 같은 시간을 두 번 표시하는 구간이 생깁니다. 이러한 상황에서는 지역 시계에 표시되는(또는 Python datetime 인스턴스에 저장되는) 정보만으로는 특정 시점을 식별하기에 충분하지 않습니다. 제안하는 해결책은 datetime 인스턴스에 0과 1의 값을 갖는 속성을 추가하여 두 개의 모호한 시간을 열거하는 것입니다.

힘센 남자가 커다란 시계의 바늘을 멈추려고 애쓰는 만화입니다. 캡션 내용: 시간을 멈출 수는 없습니다... 하지만 오전 2시에 한 시간 되돌릴 수는 있습니다. 일광 절약 시간이 끝나고 표준시가 시작되는 10월 28일에 말입니다."

용어

시계를 뒤로 돌리면 시간에 폴드(fold) [2]가 생긴다고 말합니다. 시계를 앞으로 돌리면 갭(gap)이 생깁니다. 폴드에 속하는 지역 시간을 모호한 시간이라고 부릅니다. 갭에 속하는 지역 시간을 누락된 시간이라고 부릅니다.

제안

“fold” 속성

datetime.time과 datetime.datetime 클래스의 인스턴스에 fold라는 속성을 추가할 것을 제안합니다. 이 속성은 모호한 경우에서 (시간 순으로) 두 번째 시점을 나타내는 인스턴스를 제외한 모든 인스턴스에서 0 값을 가져야 합니다. 그런 인스턴스의 값은 1이 됩니다. [3]

영향을 받는 API

속성

datetime.time 및 datetime.datetime 클래스의 인스턴스에는 0과 1 두 가지 값을 가질 수 있는 새 속성 fold가 추가됩니다.

생성자

datetime.time 및 datetime.datetime 클래스의 __new__ 메서드에는 기본값이 0인 fold라는 새 키워드 전용 인자가 추가됩니다. fold 인자의 값은 반환되는 인스턴스의 fold 속성 값을 초기화하는 데 사용됩니다.

메서드

datetime.time 및 datetime.datetime 클래스의 replace() 메서드에는 fold라는 새 키워드 전용 인자가 추가됩니다. 이 인자는 다른 replace() 인자와 비슷하게 동작합니다. fold 인자가 지정되고 0 또는 1 값이 주어지면, replace()가 반환하는 새 인스턴스의 fold 속성은 그 값으로 설정됩니다. CPython에서는 정수가 아닌 fold 값은 모두 TypeError를 발생시키지만, 다른 구현에서는 None 값이 fold가 주어지지 않았을 때와 동일하게 동작하도록 허용할 수 있습니다. [4] (이는 Python 구현마다 이 메서드의 다른 위치에 있는 None 인자를 다르게 처리하는 기존의 차이를 고려한 것이며, 향후 fold=None을 다르게 해석할 여지를 남겨 두려는 의도는 아닙니다.) fold 인자가 지정되지 않으면 fold 속성의 원래 값이 결과에 복사됩니다.

C-API

PyDateTime_DateTime 및 PyDateTime_Time 객체에서 fold 값을 추출하는 접근 매크로가 정의됩니다.

int PyDateTime_DATE_GET_FOLD(PyDateTime_DateTime *o)

fold의 값을 C int로 반환합니다.

int PyDateTime_TIME_GET_FOLD(PyDateTime_Time *o)

fold의 값을 C int로 반환합니다.

생성되는 인스턴스의 fold 값을 지정하는 추가 인자를 받는 새 생성자가 정의됩니다:

PyObject* PyDateTime_FromDateAndTimeAndFold(
    int year, int month, int day, int hour, int minute,
    int second, int usecond, int fold)

지정된 연, 월, 일, 시, 분, 초, 마이크로초 및 fold를 가진 datetime.datetime 객체를 반환합니다.

PyObject* PyTime_FromTimeAndFold(
    int hour, int minute, int second, int usecond, int fold)

지정된 시, 분, 초, 마이크로초 및 fold를 가진 datetime.time 객체를 반환합니다.

영향을 받는 동작

지금 몇 시입니까?

인자 없이 호출된 datetime.now() 메서드는 시스템 지역 시간의 폴드에서 모호한 두 시각 중 두 번째 시각을 반환할 때 fold=1을 설정합니다. tzinfo 인자와 함께 호출되면 fold 값은 tzinfo.fromutc() 구현에 의해 결정됩니다. datetime.timezone 클래스(표준 라이브러리의 고정 오프셋 tzinfo 서브클래스, 예: datetime.timezone.utc)의 인스턴스가 tzinfo로 전달되면, 반환되는 datetime 인스턴스는 항상 fold=0을 가집니다. datetime.utcnow() 메서드는 영향을 받지 않습니다.

naive에서 aware로의 변환

naive datetime 인스턴스를 aware 인스턴스로 쉽게 변환할 수 있도록 새 기능을 제안합니다.

이제 astimezone() 메서드는 naive self에 대해서도 동작합니다. 이 경우 시스템 지역 시간대로 가정하며, 모호한 경우에는 fold 플래그를 사용하여 어느 지역 시간대가 적용되는지 결정합니다.

예를 들어, US/Eastern 시간대로 설정된 시스템에서는:

>>> dt = datetime(2014, 11, 2, 1, 30)
>>> dt.astimezone().strftime('%D %T %Z%z')
'11/02/14 01:30:00 EDT-0400'
>>> dt.replace(fold=1).astimezone().strftime('%D %T %Z%z')
'11/02/14 01:30:00 EST-0500'

이는 datetime.now(tz)가 datetime.now().astimezone(tz)와 완전히 동등하다는 것을 의미합니다(tz가 PEP 이후의 tzinfo 구현, 즉 fold를 올바르게 처리하고 설정하는 구현의 인스턴스라고 가정할 때).

EPOCH 기준 POSIX 초로부터의 변환

datetime.datetime의 fromtimestamp() 정적 메서드는 반환되는 객체의 fold 속성을 적절히 설정합니다.

예를 들어, US/Eastern 시간대로 설정된 시스템에서는:

>>> datetime.fromtimestamp(1414906200)
datetime.datetime(2014, 11, 2, 1, 30)
>>> datetime.fromtimestamp(1414906200 + 3600)
datetime.datetime(2014, 11, 2, 1, 30, fold=1)

EPOCH 기준 POSIX 초로의 변환

datetime.datetime의 timestamp() 메서드는 fold 속성 값만 다른 datetime.datetime 인스턴스들에 대해, 해당 인스턴스들이 모호한 시각이나 누락된 시각을 나타내는 경우에만 서로 다른 값을 반환합니다.

datetime.datetime 인스턴스 dt가 모호한 시각을 나타낼 때, 다음을 만족하는 두 값 s0과 s1이 존재합니다:

datetime.fromtimestamp(s0) == datetime.fromtimestamp(s1) == dt

(이는 ==가 fold 값을 무시하기 때문입니다 – 아래를 참조하십시오.)

이 경우 dt.timestamp()는 dt.fold == 0이면 s0과 s1 값 중 작은 값을, 그렇지 않으면 큰 값을 반환합니다.

예를 들어, US/Eastern 시간대로 설정된 시스템에서는:

>>> datetime(2014, 11, 2, 1, 30, fold=0).timestamp()
1414906200.0
>>> datetime(2014, 11, 2, 1, 30, fold=1).timestamp()
1414909800.0

datetime.datetime 인스턴스 dt가 누락된 시각을 나타낼 때, 다음을 만족하는 값 s는 존재하지 않습니다:

datetime.fromtimestamp(s) == dt

하지만 간격의 크기(초 단위)만큼 차이 나는 두 개의 “알아 두면 좋은” s 값을 만들 수 있습니다. 하나는 UTC 오프셋이 항상 간격 직전의 오프셋과 같은 시간대에서 dt에 대응하는 s 값이고, 다른 하나는 UTC 오프셋이 항상 간격 직후의 오프셋과 같은 시간대에서의 비슷한 값입니다.

누락된 dt가 주어졌을 때 dt.timestamp()가 반환하는 값은 dt.fold == 0이면 두 “알아 두면 좋은” 값 중 큰 값이고, 그렇지 않으면 작은 값입니다. (이는 오타가 아닙니다 – 모호한 시각에 대한 규칙과 의도적으로 반대로 정한 것입니다.)

예를 들어, US/Eastern 시간대로 설정된 시스템에서는:

>>> datetime(2015, 3, 8, 2, 30, fold=0).timestamp()
1425799800.0
>>> datetime(2015, 3, 8, 2, 30, fold=1).timestamp()
1425796200.0

어웨어 datetime 인스턴스

PEP 이전의 tzinfo 구현을 사용하는 사용자는 자신의 어웨어 datetime 인스턴스 동작에서 아무런 변화도 보지 못할 것입니다. fold 속성 값만 다른 그러한 두 인스턴스는 fold 값에 명시적으로 접근하는 것 외에는 어떤 방법으로도 구별할 수 없습니다. (이는 이러한 PEP 이전 구현들이 fold 속성을 사용하지 않기 때문입니다.)

반면에 객체의 tzinfo가 fold를 인식하는 구현으로 설정되어 있다면, 폴드나 간격 안에서 fold 값은 여러 메서드의 결과에 영향을 줍니다: utcoffset(), dst(), tzname(), astimezone(), strftime() (형식 명세에 “%Z”나 “%z” 지시자를 사용한 경우), isoformat(), timetuple().

날짜와 시간의 결합과 분리

datetime.datetime.combine() 메서드는 fold 속성 값을 결과 datetime.datetime 인스턴스에 복사합니다.

datetime.datetime.time() 메서드는 fold 속성 값을 결과 datetime.time 인스턴스에 복사합니다.

피클

fold 속성 값은 프로토콜 버전 4(Python 3.4에서 도입) 이상으로 생성된 피클에만 저장됩니다.

datetime.datetime과 datetime.time 객체의 피클 크기는 바뀌지 않습니다. fold 값은 datetime.datetime 피클 페이로드의 세 번째 바이트의 첫 번째 비트에, 그리고 datetime.time 페이로드의 첫 번째 바이트의 첫 번째 비트에 인코딩됩니다. current implementation에서 이 바이트들은 월(1-12)과 시(0-23) 값을 저장하는 데 사용되며 첫 번째 비트는 항상 0입니다. 이 바이트들을 선택한 이유는 현재의 언피클 코드가 검사하는 유일한 바이트들이기 때문입니다. 따라서 PEP 이후의 fold=1 피클을 PEP 이전의 Python에서 로드하면 범위를 벗어난 구성 요소를 가진 인스턴스가 만들어지는 대신 예외가 발생합니다.

표준 라이브러리의 tzinfo 구현

이 PEP에서는 datetime.tzinfo 추상 클래스의 새로운 구현을 제안하지 않습니다. 기존의 (고정 오프셋) 시간대는 모호한 지역 시각을 만들지 않으며, 그 utcoffset() 구현은 fold 값과 관계없이 지금과 같은 상수 값을 반환합니다.

추상 datetime.tzinfo 클래스에 있는 fromutc()의 기본 구현은 바뀌지 않습니다. 이 구현은 현재 표준 라이브러리 어디에서도 사용되지 않습니다. 포함된 유일한 tzinfo 구현(고정 오프셋 시간대를 구현하는 datetime.timezone 클래스)이 fromutc()를 재정의하기 때문입니다. 기본 구현을 바꾸지 않고 유지하면, 기본 fromutc()를 상속하는 PEP 이전의 서드파티 구현들이 의도치 않게 영향을 받지 않는다는 이점이 있습니다.

새로운 tzinfo 구현을 위한 지침

DST 등의 이유로 달라지는 UTC 오프셋을 지원하려는 구체 datetime.tzinfo 서브클래스 구현자는 다음 지침을 따라야 합니다.

모르는 게 약입니다

utcoffset(), tzname(), dst() 메서드의 새 구현은 모호하거나 누락된 시각에 대해 호출된 경우가 아니라면 fold 값을 무시해야 합니다.

폴드 안에서

새 서브클래스는 베이스 클래스의 fromutc() 메서드를 재정의해야 합니다. 이때 서로 다른 두 UTC 시각 u0와 u1(u0 <u1)이 같은 지역 시각 t에 대응하는 모든 경우에 fromutc(u0)는 fold=0인 인스턴스를 반환하고 fromutc(u1)은 fold=1인 인스턴스를 반환하도록 구현해야 합니다. 그 밖의 모든 경우에 반환되는 인스턴스는 fold=0이어야 합니다.

utcoffset(), tzname(), dst() 메서드는 fold 속성의 값을 사용하여, fold 속성이 없다면 모호했을 시각 t가 전환 이전의 시각인지 이후의 시각인지 판별해야 합니다. 정의상 폴드를 만드는 모든 전환에서 utcoffset()은 전환 이전에 더 크고 전환 이후에 더 작습니다. tzname()과 dst()가 반환하는 값은 전환의 종류에 따라 fold 속성의 값에 의존할 수도 있고 의존하지 않을 수도 있습니다.

Diagram of relationship between UTC and local time around a fall-back transition – see full description on page.

위 그림은 시계를 되돌리는(fall-back) 전환 전후의 UTC 시각과 지역 시각 사이의 관계를 보여 줍니다. 지그재그 선은 fromutc()가 구현하는 함수의 그래프입니다. UTC 축에서 전환 지점에 인접해 있고 길이가 전환 시의 시간 이동 크기와 같은 두 구간은 지역 시각 축의 같은 구간으로 매핑됩니다. fromutc() 메서드의 새 구현은 self가 UTC 축에서 노란색으로 표시된 영역에 있을 때 fold 속성을 1로 설정해야 합니다. (모든 구간은 왼쪽이 닫히고 오른쪽이 열린 구간으로 취급해야 합니다.)

갭에 주의하십시오

fromutc() 메서드는 절대로 갭 안의 시각을 만들어 내서는 안 됩니다.

갭에 속하는 지역 시각에 대해 utcoffset(), tzname(), dst() 메서드가 호출되면, fold=0인 경우 전환 이전에 적용되던 규칙을 사용해야 합니다. 그렇지 않으면 전환 이후에 적용되는 규칙을 사용해야 합니다.

Diagram of relationship between UTC and local time around a spring-forward transition – see full description on page.

위 그림은 시계를 앞당기는(spring-forward) 전환 전후의 UTC 시각과 지역 시각 사이의 관계를 보여 줍니다. 전환 시점에 지역 시계는 갭 안의 시각을 건너뛰며 앞당겨집니다. utcoffset(), tzname(), dst()의 값을 결정할 때, fold=0인 갭 안의 시각에 대응하는 UTC 시각은 전환 이전의 선을 앞으로 연장하여 구하고, fold=1인 인스턴스는 전환 이후의 선을 뒤로 연장하여 구합니다.

전환 시 규칙 요약

모호하거나 누락된 시간에 대해 utcoffset()은 다음 표에 따른 반환값을 돌려주어야 합니다:

fold=0 fold=1
폴드 oldoff newoff = oldoff - delta
갭 oldoff newoff = oldoff + delta

여기서 oldoff(newoff)는 전환 이전(이후)의 UTC 오프셋이고, delta는 폴드 또는 갭의 절대 크기입니다.

fold 속성의 해석은 fold 경우와 gap 경우에서 일관된다는 점에 유의하십시오. 두 경우 모두 fold=0 (fold=1)은 전환 이전(이후)의 fromutc() 선을 사용하여 UTC 시간을 찾는다는 뜻입니다. “Fold” 경우에만 UTC 시간 u0과 u1이 방정식 fromutc(u) == t의 “실제” 해이며, “Gap” 경우에는 “허수” 해입니다.

DST 전환

DST 시작 시 생기는 누락된 시간에서 utcoffset()과 dst() 메서드가 반환하는 값은 다음과 같아야 합니다

fold=0 fold=1
utcoffset() stdoff stdoff + dstoff
dst() zero dstoff

DST 종료 시 생기는 모호한 시간에서 utcoffset()과 dst() 메서드가 반환하는 값은 다음과 같아야 합니다

fold=0 fold=1
utcoffset() stdoff + dstoff stdoff
dst() dstoff zero

여기서 stdoff는 표준(DST가 아닌) 오프셋이고, dstoff는 DST 보정값(일반적으로 dstoff = timedelta(hours=1))이며, zero = timedelta(0)입니다.

시간 산술 연산과 비교 연산자

In mathematicks he was greater
Than Tycho Brahe, or Erra Pater:
For he, by geometric scale,
Could take the size of pots of ale;
Resolve, by sines and tangents straight,
If bread or butter wanted weight,
And wisely tell what hour o’ th’ day
The clock does strike by algebra.
– 새뮤얼 버틀러, “Hudibras”

fold 속성의 값은 naive datetime 인스턴스와의 모든 연산에서 무시됩니다. 결과적으로 fold 값만 다른 naive datetime.datetime 또는 datetime.time 인스턴스는 서로 같은 것으로 비교됩니다. 이러한 인스턴스를 구별해야 하는 애플리케이션은 fold 값을 명시적으로 확인하거나, 해당 인스턴스를 모호한 시간이 없는 시간대(UTC 등)로 변환해야 합니다.

aware이든 naive이든 datetime 인스턴스에 timedelta를 더하거나 뺄 때에도 fold 값은 무시됩니다. datetime에 timedelta를 더한(datetime에서 timedelta를 뺀) 결과는 원래 datetime 인스턴스가 fold=1이었더라도 항상 fold가 0으로 설정됩니다.

datetime 인스턴스 t와 s에 대해 차이 t - s를 계산하는 방식은 바꾸지 않습니다. 두 인스턴스가 모두 나이브(naive)이거나 t.tzinfo가 s.tzinfo와 같은 인스턴스이면(t.tzinfo is s.tzinfo가 True로 평가되면), t - s는 s + d == t를 만족하는 timedelta d입니다. 앞 단락에서 설명했듯이 timedelta 덧셈은 fold와 tzinfo 속성을 모두 무시하며, 같은 시간대 내의 datetime 뺄셈이나 naive datetime 뺄셈도 마찬가지입니다.

나이브 비교와 영역 내 비교는 fold 값을 무시하고 지금과 같은 결과를 반환합니다. (이것이 하위 호환성을 유지하는 유일한 방법입니다. fold를 사용하는 어웨어(aware) 영역 내 비교가 필요하다면 먼저 양쪽을 모두 UTC로 변환하십시오.)

영역 간(inter-zone) 뺄셈은 지금과 같이 정의됩니다. t - s는 (t - t.utcoffset()) - (s - s.utcoffset()).replace(tzinfo=t.tzinfo)로 계산되지만, t.tzinfo나 s.tzinfo 중 어느 하나라도 이 PEP를 따르는(post-PEP) 구현이면 결과는 t.fold와 s.fold 값에 따라 달라집니다. [5]

어웨어 datetime 동등 비교

어웨어 datetime 비교 연산자는 지금과 같이 동작하며, 피연산자 중 하나의 utcoffset() 값이 fold에 의존하면 결과도 fold 값의 영향을 간접적으로 받습니다. 단, 예외가 하나 있습니다. 서로 다른 시간대 간 비교에서 피연산자 중 하나 또는 둘 모두의 utcoffset()이 해당 fold fold 속성의 값에 의존하는 경우, 결과는 항상 False입니다. [6]

t.tzinfo is s.tzinfo가 False로 평가될 때의 t == s는 형식적으로 다음과 같이 정의할 수 있습니다. toutc(t, fold)를 어웨어 datetime 인스턴스 t를 받아서, 주어진 fold 값을 가정하고 같은 시각을 UTC로 나타내는 나이브 인스턴스를 반환하는 함수라고 합시다:

def toutc(t, fold):
    u = t - t.replace(fold=fold).utcoffset()
    return u.replace(tzinfo=None)

그러면 t == s는 다음과 동등합니다

toutc(t, fold=0) == toutc(t, fold=1) == toutc(s, fold=0) == toutc(s, fold=1)

하위 호환성과 상위 호환성

fold 플래그를 명시적으로 읽지 않고, 그 플래그를 읽는 tzinfo 구현도 사용하지 않는 프로그램에는 이 제안이 거의 영향을 주지 않습니다. 이런 프로그램에서 눈에 띄는 유일한 변화는 이제 POSIX 타임스탬프로 변환했다가 다시 되돌리는 왕복 변환이 (부동 소수점 반올림 오차 범위 안에서) 올바르게 된다는 점입니다. 예전의 잘못된 동작을 우회하는 코드를 구현한 프로그램은 수정해야 할 수도 있습니다.

이전 프로그램이 만든 피클은 앞으로도 완전히 호환됩니다. 새 버전에서 피클링된 fold=1인 datetime/time 인스턴스만 이전 Python 버전에서 읽을 수 없게 됩니다. fold=0(기본값)인 인스턴스의 피클은 바뀌지 않습니다.

질문과 답변

새 플래그를 “isdst”라고 부르지 않는 이유는 무엇입니까?

비기술적인 답변

  • Alice: Bob, 내일 새벽 01:30에 별 보기 파티를 합시다!
  • Bob: 그 시각에 일광 절약 시간제가 적용된다고 처음부터 가정해야 합니까, 아니면 적용되지 않는다고 가정해야 합니까?
  • Alice: 네?

  • Bob: Alice, 내일 새벽 01:30에 별 보기 파티를 합시다!
  • Alice: 저기, Bob, 내일은 새벽 01:30이 두 번 옵니다. 어느 쪽 시간을 생각하고 계십니까?
  • Bob: 그건 생각해 보지 않았지만, 첫 번째로 합시다.

(같은 등장인물, 한 시간 뒤)


  • Bob: Alice, 제 Py-O-Clock 기기를 내일 새벽 01:30으로 맞추려고 하니 fold=0과 fold=1 중에서 고르라고 합니다. 어떻게 해야 합니까?
  • Alice: Py-O-Clock이라는 건 들어 본 적이 없지만, 아마 fold=0이 첫 번째 새벽 01:30이고 fold=1이 두 번째일 것입니다.

기술적인 이유

time.struct_time 객체의 tm_isdst 필드를 사용해 폴드(fold) 구간의 지역 시간을 구분할 수는 있지만, 그러한 구분의 의미는 이 PEP의 제안과 완전히 다릅니다.

tm_isdst 필드의 주된 문제는 tzinfo 구현만 알 수 있는 시간대 세부 정보를 모르고서는 tm_isdst에 어떤 값이 적절한지 알 수 없다는 점입니다. 따라서 tm_isdst는 time.localtime 같은 메서드의 출력으로는 유용하지만, time.mktime 같은 메서드의 입력으로는 다루기 번거롭습니다.

프로그래머가 time.mktime에 tm_isdst의 음이 아닌 값을 잘못 지정하면 결과는 1시간 어긋난 시간이 되며, time.mktime을 호출하기 전에 DST에 대해 무언가를 알 방법은 거의 없으므로 보통 유일하게 합리적인 선택은 tm_isdst=-1입니다.

tm_isdst와 달리, 제안된 fold 속성은 그 속성이 없을 때 두 가지 해석이 가능하거나 아무 해석도 불가능한 경우가 아니라면 datetime 인스턴스의 해석에 아무런 영향을 주지 않습니다.

tm_isdst와 의미가 같지 않은 것을 isdst라고 부르면 매우 혼란스러울 것이므로 다른 이름이 필요합니다. 게다가 datetime.datetime 클래스에는 이미 dst()라는 메서드가 있어서, fold를 “isdst”라고 부른다면 “isdst”는 0인데 dst()는 0이 아니거나 그 반대인 상황이 필연적으로 생길 것입니다.

왜 “fold”입니까?

Guido van Rossum이 제안했으며, 저자 한 명은 찬성했습니다(다른 한 명은 처음에 반대했습니다). 속성에 허용되는 값을 False/True에서 0/1로 바꾼 뒤 합의에 이르렀습니다. 명사 “fold”는 올바른 함의를 지니고 기억하기 쉬우면서도, 근거 없는 가정을 불러일으키지 않습니다.

“first”는 무엇입니까?

이것은 처음에 선택된 속성의 가칭이었는데, 명백한 대안(“second”)이 기존 속성과 충돌했기 때문입니다. 이 이름은 주로 True를 기본값으로 만든다는 이유로 기각되었습니다.

다음과 같은 대체 이름들도 검토되었습니다:

later
“fold”의 유력한 경쟁 후보였습니다. 저자 한 명은 똑같이 적합한 “latter”와 혼동될 수 있다는 이유로 이 이름을 좋아하지 않지만, 어디서나 자동 완성을 쓰는 시대에 이는 사소한 고려 사항입니다. 더 강력한 반대 논거는 존재하지 않는 시간의 경우 later=True인 인스턴스가 .astimezone(timezone.utc)에 의해 later=False인 인스턴스보다 더 이른 시간으로 변환된다는 점일 것입니다. 그러나 이것 역시 원래 시간이 유효하지 않다는 바람직한 표시로 해석될 수 있습니다.
which
localtime 함수 브랜치 인덱스의 원래 임시 이름은 모호성 해소 속성의 이름으로 독립적으로 제안되었으며 어느 정도 지지를 받았습니다.
repeated
메일링 리스트에서 아무런 지지도 받지 못했습니다.
ltdf
(Local Time Disambiguation Flag, 지역 시간 모호성 해소 플래그) - 짧고, 문서를 읽지 않고 그 의미를 추측하려는 사람은 없을 것입니다. (이 약어는 PEP 토론에서 대안 중 어느 것도 지지하고 싶지 않았던 사람들이 ltdf=False가 더 이른 쪽을 뜻한다는 의미로 사용했습니다.)

두 개의 값으로 충분합니까?

fold 속성에 None 또는 -1 값을 허용해야 한다는 몇 가지 이유가 제기되었습니다: 하위 호환성, tm_isdst와의 유사성, 그리고 유효하지 않은 시간에 대한 엄격한 검사입니다.

하위 호환성

fold 플래그의 기본값이 PEP 이전의 동작을 요청한다는 신호가 되는 None이라면 하위 호환성이 향상될 수 있다는 제안이 있었습니다. 아래의 분석에 근거하여, 우리는 fold=0을 기본값으로 하는 제안된 변경 사항이 충분히 하위 호환된다고 판단합니다.

이 PEP는 다른 점에서는 동일한 두 datetime 인스턴스가 서로 다른 fold 값을 가진다는 것을 프로그램이 알아낼 수 있는 방법을 세 가지만 제공합니다: (1) fold 속성을 명시적으로 검사하는 것, (2) 인스턴스가 나이브한 경우 - astimezone() 메서드를 사용하여 다른 시간대로 변환하는 것, (3) timestamp() 메서드를 사용하여 float로 변환하는 것입니다.

fold는 새로운 속성이므로, 첫 번째 방법은 기존 프로그램에서 사용할 수 없습니다. (2)번 방법은 시스템 시간대에서 우연히 폴드나 갭에 해당하는 나이브 datetime에 대해서만 동작한다는 점에 유의하십시오. 그 밖의 모든 경우에는, 인스턴스가 PEP 이전의 프로그램에서는 사용할 수 없는 fold를 인식하는 tzinfo를 사용하지 않는 한 변환 시 fold 값은 무시됩니다. 마찬가지로, astimezone()은 현재 나이브 datetime에서 동작하지 않으므로, 나이브 인스턴스에 대해 호출하는 astimezone()도 그러한 프로그램에서는 사용할 수 없습니다.

따라서 이 PEP가 구현된 후 기존 프로그램이 다른 결과를 내기 시작할 수 있는 상황은 단 하나만 남습니다: 우연히 폴드나 갭에 해당하는 나이브 datetime 인스턴스에 대해 datetime.timestamp() 메서드가 호출되는 경우입니다. 현재 구현에서는 그 결과가 정의되어 있지 않습니다. 시스템의 mktime 구현에 따라, 프로그램은 이러한 경우에 서로 다른 결과나 오류를 보게 될 수 있습니다. 이 PEP가 적용되면 이러한 경우에도 타임스탬프 값이 잘 정의되지만, fold 플래그의 값에 따라 달라지게 됩니다. 우리는 datetime.timestamp() 메서드 동작의 변경을 이 PEP로 가능해진 버그 수정으로 간주합니다. 기존 동작에 의존하는 사용자는 dt.timestamp() 대신 time.mktime(dt.timetuple()) + 1e-6*dt.microsecond를 작성하여 여전히 기존 동작을 흉내 낼 수 있습니다.

tm_isdst와의 유사성

time.mktime 인터페이스는 tm_isdst 플래그에 -1, 0, 1의 세 가지 값을 허용합니다. 앞서 설명했듯이, -1(나머지 필드로부터 주어진 시간에 DST가 적용되는지를 mktime이 판단하도록 요청하는 값)이 실제로 유용한 유일한 선택지입니다.

그러나 fold 플래그를 사용하면, DST 전환이 있는 대부분의 시간대에서 datetime.timestamp()는 99.98%의 시간 동안 tm_isdst=-1을 지정한 mktime과 같은 값을 반환합니다. 게다가 tm_isdst=-1과 같은 동작은 fold 값과 관계없이 지정됩니다.

datetime.timestamp()와 tm_isdst=-1을 지정한 mktime의 결과가 다를 수 있는 것은 0.02%의 경우(연간 2시간)뿐입니다. 그러나 이 경우에도, 관련 표준은 이러한 경우에 mktime이 -1을 반환하고 오류 코드를 설정하는 것을 허용하지만, 대부분의 mktime 구현은 fold=0 또는 fold=1 값을 반환합니다.

다시 말해, tm_isdst=-1 동작이 이 PEP에서 빠져 있는 것이 아닙니다. 오히려 그것은 잘 정의된 서로 다른 두 가지 방식으로 제공되는 유일한 동작입니다. 빠져 있는 동작은 잘못 지정된 tm_isdst 때문에 주어진 지역 시각이 다른 지역 시각으로 해석되는 경우입니다.

예를 들어, 북반구에서 DST를 시행하는 시간대(6월에 DST가 적용되는 곳)에서는 다음과 같은 결과를 얻을 수 있습니다

>>> from time import mktime, localtime
>>> t = mktime((2015, 6, 1, 12, 0, 0, -1, -1, 0))
>>> localtime(t)[:]
(2015, 6, 1, 13, 0, 0, 0, 152, 1)

mktime이 12:00을 13:00으로 해석했다는 점에 유의하십시오. datetime.timestamp, datetime.fromtimestamp를 사용하면 현재 다음이 보장됩니다

>>> t = datetime.datetime(2015, 6, 1, 12).timestamp()
>>> datetime.datetime.fromtimestamp(t)
datetime.datetime(2015, 6, 1, 12, 0)

이 PEP는 같은 보장을 fold의 두 값 모두로 확장합니다:

>>> t = datetime.datetime(2015, 6, 1, 12, fold=0).timestamp()
>>> datetime.datetime.fromtimestamp(t)
datetime.datetime(2015, 6, 1, 12, 0)
>>> t = datetime.datetime(2015, 6, 1, 12, fold=1).timestamp()
>>> datetime.datetime.fromtimestamp(t)
datetime.datetime(2015, 6, 1, 12, 0)

따라서 fold=-1의 제안된 용도 중 하나인 기존 동작과의 일치는 필요하지 않습니다. 기존 동작이 정의되지 않았던 몇 가지 경우를 제외하면 fold를 어느 쪽으로 선택하든 기존 동작과 일치합니다.

잘못된 시각의 엄격한 검사

또 다른 제안은 fold=-1 또는 fold=None을 사용하여 프로그램에 폴드와 갭을 처리할 수단이 전혀 없음을 나타내고, dt가 모호하거나 존재하지 않는 지역 시각을 나타낼 때마다 dt.utcoffset()이 오류를 발생시키도록 하자는 것이었습니다.

이 제안의 주된 문제는 dt.utcoffset()이 오류를 발생시킬 수 없는 상황에서 내부적으로 사용된다는 점입니다. 딕셔너리 조회나 리스트/집합 멤버십 검사가 그 예입니다. 따라서 엄격한 갭/폴드 검사 동작은 dt.utcoffset(raise_on_gap=True, raise_on_fold=False)와 같은 별도의 플래그로 제어해야 합니다. 그러나 이 기능은 사용자 코드에서 쉽게 구현할 수 있습니다:

def utcoffset(dt, raise_on_gap=True, raise_on_fold=False):
    u = dt.utcoffset()
    v = dt.replace(fold=not dt.fold).utcoffset()
    if u == v:
        return u
    if (u < v) == dt.fold:
        if raise_on_fold:
            raise AmbiguousTimeError
    else:
        if raise_on_gap:
            raise MissingTimeError
    return u

게다가 문제가 되는 경우에 오류를 발생시키는 것은 가능한 여러 해결책 중 하나일 뿐입니다. 대화형 프로그램은 사용자에게 추가 입력을 요청할 수 있고, 서버 프로세스는 경고를 기록하고 적절한 기본 동작을 수행할 수 있습니다. 가능한 모든 사용자 요구 사항에 맞는 함수를 제공하는 것은 불가능하지만, 이 PEP는 원하는 어떤 동작이든 몇 줄의 코드로 구현할 수 있는 수단을 제공합니다.

구현

Picture Credit

This image is a work of a U.S. military or Department of Defense employee, taken or made as part of that person’s official duties. As a work of the U.S. federal government, the image is in the public domain.