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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 공지
이 PEP는 거부되었습니다. 다음을 참조하십시오: https://mail.python.org/pipermail/python-dev/2012-February/116837.html
초록
Python이 나노초 해상도를 사용하는 새 함수들을 정밀도 손실 없이 지원할 수 있도록 Decimal이 고해상도 타임스탬프의 공식 타입이 됩니다.
근거
Python 2.3은 1초 미만의 해상도를 지원하기 위해 float 타임스탬프를 도입했습니다. os.stat()은 Python 2.5부터 기본적으로 float 타임스탬프를 사용합니다. Python 3.3은 나노초 해상도를 지원하는 함수들을 도입했습니다:
- os 모듈: futimens(), utimensat()
- time 모듈: clock_gettime(), clock_getres(), monotonic(), wallclock()
os.stat()은 나노초 타임스탬프를 읽지만 타임스탬프를 float로 반환합니다.
Python float 타입은 IEEE 754 표준의 binary64 형식을 사용합니다. 1나노초(10-9) 해상도에서 float 타임스탬프는 224초(194일: Epoch 타임스탬프로는 1970-07-14)보다 큰 값에 대해 정밀도를 잃습니다.
나노초 타임스탬프를 지원하는 파일 시스템(예: ext4, btrfs, NTFS, …)에서 정확한 수정 시각을 설정하려면 나노초 해상도가 필요합니다. 또한 수정 시각을 비교하여 한 파일이 다른 파일보다 최신인지 확인하는 데에도 도움이 됩니다. 사용 사례: shutil.copystat()을 사용한 파일 수정 시각 복사, tarfile 모듈을 사용한 TAR 아카이브 생성, mailbox 모듈을 사용한 메일함 관리 등이 있습니다.
더 나은 해상도가 필요해질 때 API를 변경하지 않아도 되도록, (나노초와 같은) 고정 해상도보다는 임의의 해상도를 선호합니다. 예를 들어 NTP 프로토콜은 232분의 1초 단위(약 2.3 × 10-10초)를 사용하는 반면, NTP 프로토콜 버전 4는 264분의 1초 단위(5.4 × 10-20초)를 사용합니다.
Note
1마이크로초(10-6) 해상도에서 float 타임스탬프는 233초(272년: Epoch 타임스탬프로는 2242-03-16)보다 큰 값에 대해 정밀도를 잃습니다. 100나노초(10-7, Windows에서 사용하는 해상도) 해상도에서 float 타임스탬프는 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 구현이 있습니다.
하위 호환성
기본 타임스탬프 타입(float)은 변경되지 않으므로 하위 호환성에도 성능에도 영향이 없습니다. 새로운 타임스탬프 타입인 decimal.Decimal은 명시적으로 요청한 경우에만 반환됩니다.
반론: 시계의 정확도
컴퓨터 시계와 운영 체제는 부정확하며, 실제로는 나노초 정확도를 제공하지 못합니다. 나노초는 CPU 명령어 두어 개를 실행하는 데 걸리는 시간입니다. 실시간 운영 체제에서조차 나노초 정밀도의 측정값은 상위 수준 애플리케이션이 처리하기 시작하는 시점에 이미 쓸모없어집니다. CPU에서 캐시 미스가 단 한 번만 발생해도 그 정밀도는 무의미해집니다.
Note
Linux는 시계를 나노초 정확도로 UTC에 동기화된 상태로 유지하지는 못하지만, 실제로 나노초 정밀도로 시간을 측정할 수 있습니다.
대안: 타임스탬프 타입
임의의 해상도 또는 나노초 해상도를 갖는 타임스탬프를 지원하기 위해 다음 타입들이 검토되었습니다:
- decimal.Decimal
- 나노초 수
- 128비트 float
- datetime.datetime
- datetime.timedelta
- 정수의 튜플
- timespec 구조체
기준:
- 타임스탬프에 대해 산술 연산을 할 수 있어야 합니다
- 타임스탬프는 비교 가능해야 합니다
- 임의의 해상도, 또는 최소한 정밀도 손실 없는 1나노초의 해상도
- 하위 호환성을 위해 새로운 타임스탬프를 float로 강제 변환할 수 있어야 합니다
1나노초 해상도면 현재의 모든 C 함수를 지원하기에 충분합니다.
운영 체제가 사용하는 가장 높은 해상도는 1나노초입니다. 실제로 대부분의 시계 정확도는 나노초보다는 마이크로초에 가깝습니다. 따라서 1나노초의 고정 해상도를 사용하는 것이 합리적으로 보입니다.
나노초 수 (int)
나노초 해상도면 현재의 모든 C 함수에 충분하므로, 타임스탬프는 float가 아닌 정수인 나노초 수로 간단히 나타낼 수 있습니다.
나노초 수 형식은 객체 타입을 확인하는 것만으로는 나노초 수와 초 수를 구별할 수 없어서 이 형식을 위한 새로운 전용 함수를 추가해야 하므로 거부되었습니다.
128비트 float
새로운 IEEE 754-2008 4배 정밀도(quad-precision) 이진 부동 소수점 타입을 추가합니다. IEEE 754-2008 4배 정밀도 부동 소수점은 부호 비트 1개, 지수 15비트, 가수 112비트로 구성됩니다. 128비트 float는 GCC (4.3), Clang, ICC 컴파일러에서 지원됩니다.
Python은 이식성이 있어야 하므로 일부 플랫폼에서만 사용할 수 있는 타입에 의존할 수 없습니다. 예를 들어 Visual C++ 2008은 128비트 float를 지원하지 않지만, 공식 Windows 실행 파일을 빌드하는 데 사용됩니다. 또 다른 예로, GCC 4.3은 x86의 32비트 모드에서 __float128을 지원하지 않습니다(GCC 4.4는 지원합니다).
라이선스 문제도 있습니다. GCC는 128비트 float에 MPFR 라이브러리를 사용하는데, 이 라이브러리는 GNU LGPL 라이선스로 배포됩니다. 이 라이선스는 Python 라이선스와 호환되지 않습니다.
Note
Intel CPU의 x87 부동 소수점 장치는 80비트 float를 지원합니다. 이 형식은 SSE 명령어 집합에서 지원되지 않는데, 현재는 특히 x86_64에서 float 연산에 SSE가 더 선호됩니다. 다른 CPU 제조사는 80비트 float를 지원하지 않습니다.
datetime.datetime
int, float, Decimal은 원시 숫자인 반면 datetime.datetime 타입은 타임스탬프를 담고 있다는 것이 분명하므로, 타임스탬프를 위한 자연스러운 선택입니다. 이 타입은 절대 타임스탬프이므로 명확하게 정의됩니다. 연, 월, 일, 시, 분, 초에 직접 접근할 수 있습니다. 타임스탬프를 문자열로 서식화하는 메서드(예: datetime.datetime.strftime)와 같은 시간 관련 메서드를 제공합니다.
주된 문제는 os.stat(), time.time(), time.clock_gettime(time.CLOCK_GETTIME)을 제외한 모든 시간 함수가 시작점이 명시되어 있지 않고 시간대 정보도 없어서 datetime.datetime으로 변환할 수 없다는 점입니다.
datetime.datetime에는 시간대와 관련된 문제도 있습니다. 예를 들어 시간대가 없는(unaware) datetime 객체와 시간대가 있는(aware) datetime은 비교할 수 없습니다. 또한 일광 절약 시간(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는 마이크로초 해상도만 지원하지만, 나노초를 지원하도록 개선할 수 있습니다.
정수 튜플
C 함수를 Python에 노출할 때, 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
- exponent >= 0
- E: (sec, nsec)
- value = sec + nsec × 10-9
- resolution = 10 -9 (나노초)
- 0 <= nsec < 10 9
형식 (E)를 제외한 모든 형식은 임의의 해상도를 지원합니다.
클록 주파수가 임의의 값이어서 10의 거듭제곱으로 표현할 수 없는 경우, 형식 (D)는 정확한 값을 저장하지 못할 수 있습니다(정밀도가 손실될 수 있습니다). 형식 (C)에도 비슷한 문제가 있지만, 이 경우에는 base=frequency, exponent=1을 사용할 수 있습니다.
형식 (C), (D), (E)는 base가 2이면 float으로, base가 10이면 decimal.Decimal로 변환하는 과정을 최적화할 수 있습니다.
형식 (A)는 단순한 분수입니다. 이 형식은 임의의 정밀도를 지원하고, 단순하며(필드가 두 개뿐입니다), 부동 소수점 값을 얻는 데 간단한 나눗셈 한 번만 필요하고, 이미 float.as_integer_ratio()에서 사용하고 있습니다.
구현을 단순하게 하기 위해(특히 C 구현에서 정수 오버플로를 피하기 위해) 분자가 분모보다 큰 값도 허용할 수 있습니다. 튜플은 나중에 정규화할 수 있습니다.
정수 튜플은 산술 연산을 지원하지 않으므로 기각되었습니다.
Note
Windows에서 QueryPerformanceCounter() 클록은 프로세서의 주파수를 사용하는데, 이 주파수는 임의의 수이므로 2나 10의 거듭제곱이 아닐 수 있습니다. 주파수는 QueryPerformanceFrequency()로 읽을 수 있습니다.
timespec 구조체
timespec은 나노초 해상도로 타임스탬프를 저장하는 데 사용하는 C 구조체입니다. Python은 같은 구조를 가진 타입인 (초, 나노초)를 사용할 수 있습니다. 편의를 위해 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()은 효율적입니다. 이 필드들은 항상 존재하므로, 운영 체제가 초 미만 해상도를 지원하지 않으면 0으로 설정됩니다. 타임스탬프를 초와 나노초 두 부분으로 나누는 방식은 timespec 타입 및 정수 튜플과 유사하므로 같은 단점을 가집니다.
os.stat() 구조체에 새 필드를 추가해도 다른 모듈(예: time 모듈)의 나노초 문제는 해결되지 않습니다.
불리언 인자 추가
새로운 타입은 하나(Decimal)만 필요하므로 간단한 불리언 플래그를 추가할 수 있습니다. 예: time.time(decimal=True) 또는 time.time(hires=True).
이러한 플래그를 사용하려면 숨겨진 임포트를 수행해야 하는데, 이는 나쁜 관행으로 여겨집니다.
불리언 인자 API는 “파이썬답지” 않기 때문에 거부되었습니다. 불리언 매개변수(플래그)보다는 매개변수 값으로 반환 타입을 바꾸는 방식이 선호됩니다.
새 함수 추가
타입마다 새 함수를 추가합니다. 예를 들면 다음과 같습니다.
- time.clock_decimal()
- time.time_decimal()
- os.stat_decimal()
- os.stat_timespec()
- 등
타임스탬프를 생성하는 함수마다 새 함수를 추가하면 코드가 많이 중복되고 유지 보수가 매우 번거로워집니다.
새 hires 모듈 추가
time 모듈과 같은 API를 제공하되 고해상도 타임스탬프(예: decimal.Decimal)를 반환하는 “hires”라는 새 모듈을 추가합니다. 새 모듈을 추가하면 time이나 os 같은 저수준 모듈을 decimal 모듈에 연결하지 않아도 됩니다.
이 아이디어는 time 모듈 코드 대부분을 중복해야 하고, 유지 보수가 번거로우며, time 모듈 외의 다른 모듈에서도 타임스탬프를 사용하기 때문에 거부되었습니다. 예를 들면 signal.sigtimedwait(), select.select(), resource.getrusage(), os.stat() 등이 있습니다. 각 모듈의 코드를 중복하는 것은 받아들일 수 없습니다.
링크
Python:
- Issue #7652: C 버전 decimal을 py3k에 병합 (cdecimal)
- Issue #11457: os.stat(): 나노초 해상도의 Decimal 객체로 타임스탬프를 얻는 새 필드 추가
- Issue #13882: PEP 410: 타임스탬프에 decimal.Decimal 타입 사용
- [Python-Dev] 타임스탬프를 decimal.Decimal 객체로 저장
다른 언어:
- Ruby (1.9.3)의 Time 클래스는 피코초(10-12)를 지원합니다.
- .NET 프레임워크의 DateTime 타입: 0001년 1월 1일 자정 12:00:00 이후 경과한 100나노초 간격의 수입니다. DateTime.Ticks는 부호 있는 64비트 정수를 사용합니다.
- Java (1.5)의 System.nanoTime(): 시작 시점이 지정되지 않은 벽시계 시간을 나노초 단위의 수로 나타내며, 부호 있는 64비트 정수(long)를 사용합니다.
- Perl의 Time::Hiref 모듈: float를 사용하므로 Python float 타임스탬프와 마찬가지로 나노초 해상도에서 정밀도 손실 문제가 있습니다.
Copyright
This document has been placed in the public domain.