PEP 410 – 타임스탬프에 decimal.Decimal 타입 사용
- Author:
- Victor Stinner <vstinner at python.org>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 01-Feb-2012
- Python-Version:
- 3.3
- Resolution:
- Python-Dev message
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 통지
이 PEP는 거부되었습니다. https://mail.python.org/pipermail/python-dev/2012-February/116837.html을 참조하십시오.
개요
Decimal은 정밀도 손실 없이 나노초 해상도를 사용하는 새로운 함수를 Python에서 지원할 수 있도록 고해상도 타임스탬프의 공식 타입이 됩니다.
근거
Python 2.3에서는 초 미만 해상도를 지원하기 위해 실수 타임스탬프를 도입했습니다. Python 2.5부터 os.stat()은 기본적으로 실수 타임스탬프를 사용합니다. Python 3.3에서는 나노초 해상도를 지원하는 함수를 도입했습니다.
- os 모듈: futimens(), utimensat()
- time 모듈: clock_gettime(), clock_getres(), monotonic(), wallclock()
os.stat()은 나노초 타임스탬프를 읽지만 타임스탬프를 실수로 반환합니다.
Python의 실수 타입은 IEEE 754 표준의 binary64 형식을 사용합니다. 1나노초의 해상도(10-9)에서는 224초보다 큰 값(194일: Epoch 타임스탬프 기준 1970-07-14)에서 실수 타임스탬프의 정밀도가 손실됩니다.
나노초 타임스탬프를 지원하는 파일 시스템(예: ext4, btrfs, NTFS, …)에서 정확한 수정 시간을 설정하려면 나노초 해상도가 필요합니다. 또한 파일이 다른 파일보다 최신인지 확인하기 위해 수정 시간을 비교하는 데에도 도움이 됩니다. 사용 사례: shutil.copystat()을 사용하여 파일의 수정 시간을 복사하거나, tarfile 모듈로 TAR 아카이브를 만들거나, mailbox 모듈로 메일박스를 관리하는 등의 작업이 있습니다.
더 나은 해상도가 필요할 때 API를 변경하지 않도록 고정 해상도(예: 나노초)보다 임의 해상도를 선호합니다. 예를 들어 NTP 프로토콜은 232초의 분수(약 2.3 × 10-10초)를 사용하는 반면, NTP 프로토콜 버전 4는 264초의 분수(5.4 × 10-20초)를 사용합니다.
Note
1마이크로초의 해상도(10-6)에서는 233초보다 큰 값(272년: Epoch 타임스탬프 기준 2242-03-16)에서 실수 타임스탬프의 정밀도가 손실됩니다. 100나노초의 해상도(10-7, Windows에서 사용되는 해상도)에서는 229초보다 큰 값(17년: Epoch 타임스탬프 기준 1987-01-05)에서 실수 타임스탬프의 정밀도가 손실됩니다.
사양
타임스탬프를 위한 새로운 타입으로 decimal.Decimal을 추가합니다. Decimal은 모든 타임스탬프 해상도를 지원하고, 산술 연산을 지원하며, 비교할 수 있습니다. 변환 과정에서 정밀도가 손실될 수 있더라도 Decimal을 float으로 변환할 수 있습니다. 시계 해상도도 Decimal 객체에 저장할 수 있습니다.
선택적 timestamp 인자를 다음 항목에 추가합니다.
- os 모듈: fstat(), fstatat(), lstat(), stat() (stat 구조체의 st_atime, st_ctime 및 st_mtime 필드), sched_rr_get_interval(), times(), wait3() 및 wait4()
- resource 모듈: getrusage()의 ru_utime 및 ru_stime 필드
- signal 모듈: getitimer(), setitimer()
- time 모듈: clock(), clock_gettime(), clock_getres(), monotonic(), time() 및 wallclock()
timestamp 인자 값은 float 또는 Decimal일 수 있으며, 하위 호환성을 위해 float가 여전히 기본값입니다. 다음 함수는 입력으로 Decimal을 지원합니다.
- datetime 모듈: date.fromtimestamp(), datetime.fromtimestamp() 및 datetime.utcfromtimestamp()
- os 모듈: futimes(), futimesat(), lutimes(), utime()
- select 모듈: epoll.poll(), kqueue.control(), select()
- signal 모듈: setitimer(), sigtimedwait()
- time 모듈: ctime(), gmtime(), localtime(), sleep()
os.stat_float_times() 함수는 더 이상 사용되지 않으므로, 대신 int()를 사용하여 명시적으로 형 변환하십시오.
Note
decimal 모듈은 Python으로 구현되어 있어 float보다 느리지만, CPython에 포함할 준비가 거의 완료된 새로운 C 구현이 있습니다.
하위 호환성
기본 timestamp 형식(float)은 변경되지 않으므로 하위 호환성이나 성능에 미치는 영향은 없습니다. 새로운 timestamp 형식인 decimal.Decimal은 명시적으로 요청한 경우에만 반환됩니다.
이의 제기: 시계의 정확도
컴퓨터 시계와 운영 체제는 부정확하며 실제로 나노초 정확도를 제공하지 못합니다. 나노초는 CPU 명령 몇 개를 실행하는 데 걸리는 시간입니다. 실시간 운영 체제에서도 나노초 정밀도의 측정값은 상위 수준 애플리케이션에서 처리를 시작할 때 이미 쓸모없게 됩니다. CPU에서 캐시 미스가 한 번만 발생해도 정밀도는 무의미해집니다.
Note
Linux는 실제로 나노초 정밀도로 시간을 측정할 수 있지만, 시계를 나노초 정확도로 UTC에 동기화할 수는 없습니다.
대안: timestamp 형식
임의의 해상도 또는 나노초 해상도의 timestamp를 지원하기 위해 다음 형식이 고려되었습니다.
- decimal.Decimal
- 나노초 수
- 128비트 부동소수점
- datetime.datetime
- datetime.timedelta
- 정수 튜플
- timespec 구조체
기준:
- timestamp에 산술 연산을 수행할 수 있어야 합니다.
- timestamp를 비교할 수 있어야 합니다.
- 정밀도를 잃지 않는 임의의 해상도 또는 최소한 1나노초의 해상도
- 하위 호환성을 위해 새로운 timestamp를 float로 변환할 수 있어야 합니다.
1나노초의 해상도면 현재의 모든 C 함수를 지원하기에 충분합니다.
운영 체제에서 사용하는 최상의 해상도는 1나노초입니다. 실제로 대부분의 시계 정확도는 나노초보다 마이크로초에 더 가깝습니다. 따라서 1나노초의 고정 해상도를 사용하는 것이 합리적으로 보입니다.
나노초 수(int)
나노초 해상도면 현재의 모든 C 함수에 충분하므로, 타임스탬프는 부동소수점 수가 아닌 정수인 나노초 수로 간단히 나타낼 수 있습니다.
나노초 수 형식은 객체 유형만 확인해서는 나노초 수와 초 수를 구분할 수 없으므로 이 형식을 위한 새로운 특수 함수를 추가해야 한다는 이유로 거부되었습니다.
128비트 부동소수점 수
새로운 IEEE 754-2008 4배 정밀도 이진 부동소수점 수 유형을 추가하십시오. IEEE 754-2008 4배 정밀도 부동소수점 수는 부호 비트 1개, 지수 비트 15개, 가수 비트 112개를 가집니다. 128비트 부동소수점 수는 GCC(4.3), Clang 및 ICC 컴파일러에서 지원됩니다.
Python은 이식 가능해야 하므로 일부 플랫폼에서만 사용할 수 있는 유형에 의존할 수 없습니다. 예를 들어 Visual C++ 2008은 128비트 부동소수점 수를 지원하지 않지만, 공식 Windows 실행 파일을 빌드하는 데 사용됩니다. 또 다른 예로 GCC 4.3은 x86의 32비트 모드에서 __float128을 지원하지 않습니다(GCC 4.4는 지원합니다).
라이선스 문제도 있습니다. GCC는 128비트 부동소수점 수에 MPFR 라이브러리를 사용하며, 이 라이브러리는 GNU LGPL 라이선스로 배포됩니다. 이 라이선스는 Python 라이선스와 호환되지 않습니다.
Note
Intel CPU의 x87 부동소수점 장치는 80비트 부동소수점 수를 지원합니다. 이 형식은 SSE 명령어 집합에서 지원되지 않으며, 특히 x86_64에서는 현재 부동소수점 수보다 SSE 명령어 집합이 선호됩니다. 다른 CPU 제조업체는 80비트 부동소수점 수를 지원하지 않습니다.
datetime.datetime
datetime.datetime 유형은 이 유형이 타임스탬프를 포함한다는 점이 명확한 반면 int, float 및 Decimal은 원시 수이므로 타임스탬프에 자연스러운 선택입니다. 절대 타임스탬프이므로 잘 정의되어 있습니다. 연도, 월, 일, 시, 분 및 초에 직접 접근할 수 있습니다. 타임스탬프를 문자열로 형식화하는 메서드(예: datetime.datetime.strftime)와 같이 시간과 관련된 메서드를 제공합니다.
가장 큰 문제는 os.stat(), time.time() 및 time.clock_gettime(time.CLOCK_GETTIME)을 제외한 모든 시간 함수가 지정되지 않은 시작점과 시간대 정보를 가지지 않으므로 datetime.datetime으로 변환할 수 없다는 점입니다.
datetime.datetime에는 시간대와 관련된 문제도 있습니다. 예를 들어 시간대가 없는 datetime 객체(unaware)와 시간대가 있는 datetime 객체(aware)는 비교할 수 없습니다. 일광 절약 시간(DST)에서 표준 시간으로 전환되는 중복된 시간대의 일광 절약 시간(DST)과 관련해서도 순서 지정 문제가 있습니다.
datetime.datetime은 os.times() 또는 time.clock()과 같이 시작점이 지정되지 않은 함수를 사용할 수 없으므로 거부되었습니다.
time.time() 및 time.clock_gettime(time.CLOCK_GETTIME)의 경우 다음을 사용하여 현재 시간을 datetime.datetime 객체로 가져오는 것이 이미 가능합니다.:
datetime.datetime.now(datetime.timezone.utc)
os.stat()의 경우 UTC 시간대의 decimal.Decimal 타임스탬프에서 datetime.datetime 객체를 간단히 만들 수 있습니다.:
datetime.datetime.fromtimestamp(value, datetime.timezone.utc)
Note
datetime.datetime은 마이크로초 해상도만 지원하지만 나노초를 지원하도록 개선할 수 있습니다.
datetime.timedelta
datetime.timedelta는 이 타입이 타임스탬프를 포함한다는 점이 명확한 반면, int, float 및 Decimal은 원시 숫자이므로 상대 타임스탬프에 자연스러운 선택입니다. 시작점이 알려져 있을 때 datetime.datetime와 함께 사용하여 절대 타임스탬프를 얻을 수 있습니다.
datetime.timedelta는 float로 강제 변환할 수 없고 해상도가 고정되어 있으므로 채택되지 않았습니다. 새로운 표준 타임스탬프 타입 하나면 충분하며, datetime.timedelta보다 Decimal을 선호합니다. datetime.timedelta를 float로 변환하려면 datetime.timedelta.total_seconds() 메서드를 명시적으로 호출해야 합니다.
Note
datetime.timedelta는 마이크로초 해상도만 지원하지만 나노초를 지원하도록 확장할 수 있습니다.
정수 튜플
Python에서 C 함수를 노출하려면, C 언어가 정수 필드를 포함하는 구조체(예: timeval 및 timespec 구조체)를 사용하므로 타임스탬프를 저장하는 데 정수 튜플이 자연스러운 선택입니다. 정수만 사용하면 정밀도 손실을 방지할 수 있습니다(Python은 임의 길이의 정수를 지원합니다). 정수 튜플을 생성하고 파싱하는 작업은 간단하고 빠릅니다.
튜플의 정확한 형식에 따라 정밀도는 임의적이거나 고정될 수 있습니다. 1나노초와 같은 임의의 한계보다 정밀도 손실이 작아지도록 정밀도를 선택할 수 있습니다.
다양한 형식이 제안되었습니다.
- A: (numerator, denominator)
- value = numerator / denominator
- resolution = 1 / denominator
- denominator > 0
- B: (seconds, numerator, denominator)
- value = seconds + numerator / denominator
- resolution = 1 / denominator
- 0 <= numerator < denominator
- denominator > 0
- C: (intpart, floatpart, base, exponent)
- value = intpart + floatpart / baseexponent
- resolution = 1 / base exponent
- 0 <= floatpart < base exponent
- base > 0
- exponent >= 0
- D: (intpart, floatpart, exponent)
- value = intpart + floatpart / 10exponent
- resolution = 1 / 10 exponent
- 0 <= floatpart < 10 exponent
- 지수 >= 0
- E: (sec, nsec)
- value = sec + nsec × 10-9
- resolution = 10 -9 (나노초)
- 0 <= nsec < 10 9
- 형식을 제외한 모든 형식은 임의의 해상도를 지원합니다.
클록 주파수가 임의의 값이고 10의 거듭제곱으로 표현할 수 없는 경우, (D) 형식은 정확한 값을 저장하지 못할 수 있습니다(정밀도가 손실될 수 있습니다). (C) 형식에도 비슷한 문제가 있지만, 이러한 경우 base=frequency 및 exponent=1을 사용할 수 있습니다.
기본값이 2이면 (C), (D), (E) 형식은 float로 변환하는 최적화를 허용하고, 기본값이 10이면 decimal.Decimal로 변환하는 최적화를 허용합니다.
- 형식은 단순한 분수입니다. 임의의 정밀도를 지원하고, 단순하며(필드가 두 개뿐입니다), 부동 소수점 값을 얻는 데 간단한 나눗셈만 필요하고, 이미 float.as_integer_ratio()에서 사용되고 있습니다.
구현을 단순화하기 위해(특히 정수 오버플로를 방지하는 C 구현에서) 분모보다 큰 분자를 허용할 수 있습니다. 튜플은 나중에 정규화할 수 있습니다.
정수 튜플은 산술 연산을 지원하지 않기 때문에 거부되었습니다.
Note
Windows에서 QueryPerformanceCounter() 클록은 임의의 수인 프로세서 주파수를 사용하므로 2의 거듭제곱이 아닐 수 있습니다.
10. 주파수는 QueryPerformanceFrequency()를 사용하여 읽을 수 있습니다.
timespec 구조체
timespec은 나노초 해상도로 타임스탬프를 저장하는 데 사용되는 C 구조체입니다. Python에서는 동일한 구조를 가진 타입을 사용할 수 있습니다: (seconds, nanoseconds). 편의를 위해 timespec의 산술 연산이 지원됩니다.
덧셈, 뺄셈 및 float로의 강제 변환을 지원하는 불완전한 timespec 타입의 예:
class timespec(tuple):
def __new__(cls, sec, nsec):
if not isinstance(sec, int):
raise TypeError
if not isinstance(nsec, int):
raise TypeError
asec, nsec = divmod(nsec, 10 ** 9)
sec += asec
obj = tuple.__new__(cls, (sec, nsec))
obj.sec = sec
obj.nsec = nsec
return obj
def __float__(self):
return self.sec + self.nsec * 1e-9
def total_nanoseconds(self):
return self.sec * 10 ** 9 + self.nsec
def __add__(self, other):
if not isinstance(other, timespec):
raise TypeError
ns_sum = self.total_nanoseconds() + other.total_nanoseconds()
return timespec(*divmod(ns_sum, 10 ** 9))
def __sub__(self, other):
if not isinstance(other, timespec):
raise TypeError
ns_diff = self.total_nanoseconds() - other.total_nanoseconds()
return timespec(*divmod(ns_diff, 10 ** 9))
def __str__(self):
if self.sec < 0 and self.nsec:
sec = abs(1 + self.sec)
nsec = 10**9 - self.nsec
return '-%i.%09u' % (sec, nsec)
else:
return '%i.%09u' % (self.sec, self.nsec)
def __repr__(self):
return '<timespec(%s, %s)>' % (self.sec, self.nsec)
timespec 타입은 정수 튜플의 (E) 형식과 유사하지만, 산술 연산과 float로의 강제 변환을 지원합니다.
timespec 타입은 나노초 해상도만 지원하고 각 산술 연산을 구현해야 하는 반면, Decimal 타입은 이미 구현되어 충분히 테스트되었기 때문에 거부되었습니다.
대안: API 설계
반환 타입을 지정하는 문자열 인자를 추가합니다.
타임스탬프를 반환하는 함수에 문자열 인자를 추가합니다. 예: time.time(format=”datetime”). 문자열은 타입보다 확장성이 높습니다. 정수 튜플처럼 타입이 없는 형식을 요청할 수 있기 때문입니다.
이 API는 객체를 인스턴스화하기 위해 모듈을 암시적으로 가져와야 했기 때문에 거부되었습니다(예: datetime.datetime을 생성하려면 import datetime이 필요합니다). 모듈을 가져오면 예외가 발생하거나 속도가 느려질 수 있으며, 이러한 동작은 예상 밖이고 당혹스럽습니다.
타임스탬프 타입을 변경하는 전역 플래그를 추가합니다.
os.stat_float_times()와 유사한 os.stat_decimal_times() 같은 전역 플래그를 추가하여 타임스탬프 타입을 전역적으로 설정할 수 있습니다.
Decimal 대신 float를 예상하는 라이브러리와 애플리케이션에서 전역 플래그로 인해 문제가 발생할 수 있습니다. Decimal은 float와 완전히 호환되지 않습니다. 예를 들어 float+Decimal은 TypeError를 발생시킵니다. os.stat_float_times()의 경우는 다릅니다. int는 float로 강제 변환할 수 있고 int+float는 float가 되기 때문입니다.
타임스탬프를 생성할 프로토콜을 추가하십시오
타임스탬프를 생성하는 방법을 하드 코딩하는 대신, 분수에서 타임스탬프를 생성하는 새 프로토콜을 추가할 수 있습니다.
예를 들어 time.time(timestamp=type)은 클래스 메서드 type.__fromfraction__(numerator, denominator)을 호출하여 지정된 타입의 타임스탬프 객체를 생성할 수 있습니다. 타입이 프로토콜을 지원하지 않으면 대체 방법으로 type(numerator) / type(denominator)를 사용합니다.
변형된 방법으로는 타임스탬프를 생성하는 “converter” 콜백을 사용할 수 있습니다. float 타임스탬프를 생성하는 예:
def timestamp_to_float(numerator, denominator):
return float(numerator) / float(denominator)
공통 변환기는 time, datetime 및 기타 모듈에서 제공하거나, 특정 “hires” 모듈에서 제공할 수도 있습니다. 사용자는 자체 변환기를 정의할 수 있습니다.
이러한 프로토콜에는 한계가 있습니다. 타임스탬프 구조를 한 번 결정해야 하며 나중에 변경할 수 없습니다. 예를 들어 시간대를 추가하거나 타임스탬프의 절대적인 시작점을 추가하면 API가 손상됩니다.
요구 사항을 고려하면 이 프로토콜 제안은 과도한 것으로 여겨졌지만, 제안된 특정 구문(time.time(timestamp=type))을 사용하면 설득력 있는 사용 사례가 발견될 경우 나중에 이를 도입할 수 있습니다.
Note
분수 대신 다른 형식을 사용할 수도 있습니다. 예를 들어 정수 튜플 섹션을 참조하십시오.
os.stat에 새 필드를 추가하십시오
나노초 해상도로 파일의 생성 시간, 수정 시간 및 접근 시간을 얻으려면 os.stat() 구조체에 세 개의 필드를 추가할 수 있습니다.
새 필드는 나노초 해상도의 타임스탬프(예: Decimal)이거나 각 타임스탬프의 나노초 부분(int)일 수 있습니다.
새 필드가 나노초 해상도의 타임스탬프라면 추가 필드를 채우는 데 시간이 오래 걸립니다. 파일이 존재하는지만 확인하기 위해 os.stat()을 호출하는 경우에도 os.stat()에 대한 모든 호출이 느려집니다. 이러한 필드를 선택 사항으로 만들기 위해 os.stat()에 매개변수를 추가할 수 있지만, 구조체의 필드 수가 가변적이게 됩니다.
새 필드가 소수 부분(나노초)만 포함한다면 os.stat()은 효율적으로 동작합니다. 이러한 필드는 항상 존재하므로 운영 체제가 1초 미만의 해상도를 지원하지 않으면 0으로 설정됩니다. 타임스탬프를 초와 나노초라는 두 부분으로 나누는 방식은 timespec 타입 및 정수 튜플과 유사하므로 동일한 단점이 있습니다.
os.stat() 구조체에 새 필드를 추가해도 다른 모듈(예: time 모듈)의 나노초 문제는 해결되지 않습니다.
불리언 인자를 추가하십시오
새 타입 하나(Decimal)만 필요하므로 간단한 불리언 플래그를 추가할 수 있습니다. 예: time.time(decimal=True) 또는 time.time(hires=True).
이러한 플래그를 사용하려면 숨겨진 임포트를 수행해야 하며, 이는 나쁜 관행으로 간주됩니다.
불리언 인자 API는 “pythonic”하지 않다는 이유로 거부되었습니다. 불리언 매개변수(플래그)보다 매개변수 값에 따라 반환 타입을 변경하는 방식이 선호됩니다.
새 함수를 추가하십시오
각 유형에 새 함수를 추가합니다. 예시는 다음과 같습니다.
- time.clock_decimal()
- time.time_decimal()
- os.stat_decimal()
- os.stat_timespec()
- 기타
타임스탬프를 생성하는 각 함수에 새 함수를 추가하면 코드가 많이 중복되고 유지 관리가 매우 어려워집니다.
새 hires 모듈을 추가합니다.
“hires”라는 새 모듈을 추가하고 time 모듈과 동일한 API를 제공하되, 높은 해상도의 타임스탬프(예: decimal.Decimal)를 반환하도록 합니다. 새 모듈을 추가하면 time이나 os 같은 저수준 모듈을 decimal 모듈에 연결하지 않아도 됩니다.
이 아이디어는 time 모듈 코드의 대부분을 중복해야 하고 유지 관리가 매우 어려우며, time 모듈 이외의 모듈에서도 타임스탬프를 사용하기 때문에 거부되었습니다. 예시는 다음과 같습니다: signal.sigtimedwait(), select.select(), resource.getrusage(), os.stat() 등입니다. 각 모듈의 코드를 중복하는 것은 허용할 수 없습니다.
링크
Python:
- Issue #7652: Merge C version of decimal into py3k (cdecimal)
- Issue #11457: os.stat(): add new fields to get timestamps as Decimal objects with nanosecond resolution
- Issue #13882: PEP 410: Use decimal.Decimal type for timestamps
- [Python-Dev] Store timestamps as decimal.Decimal objects
기타 언어:
- Ruby (1.9.3)의 Time class는 피코초(10-12)를 지원합니다.
- .NET framework의 DateTime type: 0001년 1월 1일 자정 12:00:00부터 경과한 100나노초 간격의 수입니다. DateTime.Ticks는 부호 있는 64비트 정수를 사용합니다.
- Java (1.5)의 System.nanoTime(): 시작점이 지정되지 않은 벽시계 시간을 나노초 수로 나타내며, 부호 있는 64비트 정수(long)를 사용합니다.
- Perl의 Time::Hiref module: 부동 소수점을 사용하므로 Python의 부동 소수점 타임스탬프와 마찬가지로 나노초 해상도에서 정밀도 손실 문제가 발생합니다.
Copyright
This document has been placed in the public domain.