PEP 786 – 정수 필드를 위한 정밀도 및 모듈로 정밀도 플래그 형식 지정자
- Author:
- Jay Berry <email at jb2170.com>
- Sponsor:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 04-Apr-2025
- Python-Version:
- 3.15
- Post-History:
- 14-Feb-2025, 09-Apr-2026
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 PEP 3101의 표준 형식 지정자 .과 z를 정수 필드에 대해 각각 “정밀도”와 “모듈로 정밀도”로 구현할 것을 제안합니다. 기각된 대안 구현들이 두 지정자를 서로 얽힌 방식으로 조합하므로, 이 PEP에서는 두 지정자를 함께 제시합니다.
. (“정밀도”)는 이전 스타일 % 포매팅의 동작과 동일하게, 정수를 지정된 최소 자릿수로 포맷해야 합니다. 이는 'c'를 제외한 모든 정수 표현 타입에 대해 구현되어야 합니다.
z (“모듈로 정밀도”)는 정밀도와 함께 2진, 8진, 16진 표현 타입(2의 거듭제곱인 진법) 중 하나로 정수를 포맷할 때 선택적인 “모듈로” 플래그로 허용되어야 합니다. 이 플래그는 먼저 % 연산자를 사용하여 정수를 range(base ** precision) 범위로 축소합니다. 그 결과 정밀도와 같은 정확한 자릿수를 갖는, 예측 가능한 2의 보수 스타일 포매팅이 이루어집니다.
이 PEP는 “정밀도는 정수 변환에서 무시됩니다”라고 명시한 PEP 3101의 절을 수정합니다.
근거
정수를 2진, 8진, 16진 문자열로 포매팅할 때, 결과 문자열이 최소 자릿수를 보장하기를 바라는 경우가 많습니다. 머신 폭의 범위가 알려진 부호 없는 정수(예를 들어 8비트 바이트)의 경우, 이 최소 자릿수가 곧 결과의 정확한 자릿수가 되는 경우도 많습니다. 이는 이전에 구식 % 포매팅에서 C 프로그래밍 언어의 것과 밀접하게 관련된 . “정밀도” 형식 지정자를 사용하여 구현되었습니다.
>>> "0x%.2x" % 15
'0x0f' # two hex digits, ideal for displaying an unsigned byte
>>> "0o%.3o" % 18
'0o022' # three octal digits, ideal for displaying a umask or file permissions
str.format과 f-문자열에서 사용되는 PEP 3101 신식 포매팅이 처음 도입되었을 때, format specification은 충분히 단순하여 “정밀도”의 동작을 width 형식 지정자로 손쉽게 흉내 낼 수 있었습니다. 그래서 정밀도는 구현되지 않은 채 int 필드에서 금지되었습니다. 그러나 시간이 흐르면서 새로운 형식 지정자가 추가되었고, 이들이 width와 상호 작용하면서 그 동작이 정밀도를 흉내 내는 것에서 눈에 띄게 벗어나게 되었으므로, 정밀도를 독자적인 형식 지정자 .로 다시 받아들이는 것은 충분히 정당합니다.
width 형식 지정자는 포매팅된 정수의 자릿수만이 아니라 치환 필드 전체의 최소 길이를 보장합니다. 예를 들어, 해당 표현 타입의 접두사를 앞에 붙여 주는 훌륭한 # 지정자는 width를 소모합니다:
>>> x = 12
>>> f"0x{x:02x}" # manually specifying '0x' prefix
'0x0c' # two hex digits :)
>>> f"{x:#02x}" # use '#' format specifier to output '0x' automatically
'0xc' # only one hex digit :(
>>> f"{x:#08b}"
'0b001100' # we wanted 8 bits, not 6 :(
접두사의 길이는 항상 2로 알려져 있으므로, 원하는 자릿수에 2를 더해 수동으로 처리할 수 있다고 주장할 수도 있습니다. 그러나 이것이 왜 나쁜 생각인지 보여 주는 다음 예시들을 생각해 보십시오:
- 두 번째 예제를
f"{x:#04x}"로 고치면, 언뜻 보기에는 16진 숫자 네 자리를 만들어 낼 것처럼 보이지만 실제로는 두 자리만 만들어 냅니다. 이는 가독성에 좋지 않습니다. 따라서4는 지나치게 ‘매직 넘버’에 가깝고, 이를 보완하려고f"{x:#0{2+2}x}"처럼 지나치게 명시적으로 쓰는 것은 우스꽝스러워 보입니다. - 앞으로 길이가 2가 아닌 접두사를 가진 타입 지정자가 추가될 수도 있으며, 이 경우 Python 내부의 문자열 포매팅 코드가 이를 자동으로 처리하는 대신 프로그래머가 접두사 길이를 계산해야 합니다.
sign형식 지정자를 사용하면 상황이 더 복잡해지며,' 0x0c'를 만들려면f"{x: #0{1+2+2}x}"가 필요합니다.grouping_option을 도입하면 상황은 훨씬 더 복잡해집니다. 예를 들어 정수를_로 연결된k개의 ‘워드’ 세그먼트로 포맷하는 경우,' 0xdead_beef'를 만들어 내려면x = 3735928559; k = 2; f"{x: #0{1+2+4*k+(k - 1)}_x}"가 필요합니다. 정밀도를 사용하여f"{x: #_.8x}"로 쓰는 편이 분명 더 쉽지 않겠습니까?
이쯤 되면 int 필드에 대한 정밀도 구현이 가져다줄 복잡성 감소가 모든 사용자에게 유익하리라는 것은 분명합니다. 또한 이 제안은 오로지 int 필드만을 위해 요구되는 새로운 특수 사례 동작도 아닙니다. 정밀도 토큰 .은 이미 PEP 3101에 규정된 대로 str 데이터에서는 필드 길이를 잘라 내는 데, float 데이터에서는 소수점 이하 자릿수를 고정하는 데 구현되어 있으며, 예를 들어 f"{0.1+0.2: .4f}"는 ' 0.3000'을 만들어 냅니다. 따라서 이 제안으로 인해 포맷 명세에 새 토큰을 추가할 필요가 없으며, 명세의 간결한 크기가 유지됩니다.
완결성을 위해, 그리고 합리적인 반대 의견도 없으므로, 정밀도가 10진수(밑 10)에서도 동작하도록 할 것을 제안합니다. 명시적으로, PEP 3101에 규정된 정수 표현 형식 중 정밀도를 구현할 수 있는 형식은 'b', 'd', 'o', 'x', 'X', 'n', '' (None)입니다. 허용되지 않는 유일한 표현 형식은 c (‘character’)입니다. 이 형식은 정수를 하나의 유니코드 문자로 포맷하거나 출력할 수 없는 문자라면 적절한 대체 문자로 포맷하기 위한 것이므로, 정밀도를 구현하는 것이 의미가 없습니다. 앞으로 새로운 정수 표현 형식이 추가되는 경우, 예를 들어 필요한 부분만 바꾸어 'X'와 같은 동작(즉 대문자 접두사와 숫자)을 제공할 수 있는 'B'와 'O' 같은 형식이 추가된다면, 추가할 때 정밀도를 구현할지 여부를 적절히 고려해야 합니다. 여기에서 설명한 'B'와 'O'의 경우에는 정밀도를 구현하는 것이 옳을 것입니다. 유효하지 않은 정수 표현 형식에 정밀도를 사용하려고 하면 ValueError가 발생해야 합니다.
음수에 대한 정밀도
지금까지 이 PEP에서는 정밀도를 적용한 음수 포매팅에 대한 이야기를 신중하게 피해 왔으며, 이제 이를 논의하겠습니다.
짧은 결론
우리는 두 가지 동작을 원하며, 이 때문에 후자의 동작을 켜는 플래그 z를 구현하고자 합니다:
z플래그 없이 정밀도를 사용하는 경우, 음의 정수x는 음수 부호와-x를 포맷한 숫자들로 포맷되어야 합니다. 이는 구식%포매팅과 같은 친숙한 동작입니다.예를 들어
f"{-12:#.2x}"는'-0x0c'를 생성해야 하며, 이는"%#.2x" % -12와 동등합니다.z플래그와 함께 정밀도를 사용하는 경우,f"{x:z.{n}{base_char}}"를 포맷할 때 먼저r = x % base ** n을 구한 뒤r을 정밀도 처리에 넘기며, 결과 문자열은f"{r:.{n}{base_char}}"와 동등합니다.r은range(base ** n)안에 있으므로 자릿수는 항상 정확히n이 되고, 그 결과 예측 가능한 2의 보수 방식의 포매팅이 이루어집니다. 이는struct처럼 기계 폭 기반 정수를 다루는 환경에서 최종 사용자에게 유용합니다.예를 들어
f"{-1:z#.2x}"를 포맷할 때,-1은255 = -1 % 256을 통해256을 법으로 축약되며, 결과 문자열은f"{255:#.2x}"와 동등한'0xff'입니다.z플래그는 2의 거듭제곱인 진법에 해당하는 표현 형식, 구체적으로 현재로서는 2진수, 8진수, 16진수에 대해서만 구현되어야 합니다. 정수를 10의 거듭제곱을 법으로 축약하는 것은 계산상 가능하지만, ‘10의 보수?’에 대한 수요가 없으므로 10진수 표현 형식에 대해서는 정밀도가 구현되지 않습니다.z플래그는 음수뿐 아니라 모든 정수에 대해 동작해야 합니다.z라는 문법을 선택한 것 역시 포맷 명세의 적당한 크기를 유지하려는 배려에서 비롯되었습니다.z는 PEP 682에서float와Decimal형의 음의 0을 양의 0으로 정규화하는 플래그로 포맷 명세에 도입되었습니다. 현재int형에 대해서는 구현되어 있지 않으며, 정수에는 ‘음의 0’ 상황이 결코 발생하지 않으므로z를 다시 플래그로 재활용하는 것은 논란의 여지가 없어 보입니다. 눈을 충분히 가늘게 뜨고 보면,z는 2의 보수(two’s complement)의2처럼 보입니다!
긴 인트로스펙션
먼저 2의 보수에서 부호 있는 정수의 이진 표현에 대한 몇 가지 관찰을 제시합니다. 이를 통해 음수를 포매팅하는 몇 가지 대안적인 방식에 이르게 됩니다.
부호 있는 수의 이진 표현은 선행 숫자를 접두사로 덧붙여 언제나 확장할 수 있다는 점에 주목하십시오:
45 (8-bit) 00101101
45 (9-bit) 000101101
-19 (8-bit) 11101101
-19 (9-bit) 111101101
음이 아닌 수의 경우 이는 자명합니다. 음수의 경우, n비트 표현의 기존 선행 열의 값이 -2 ** (n-1)에서 +2 ** (n-1)로 바뀌고, 값이 -2 ** n인 새로운 n+1번째 열이 앞에 붙으므로 전체 합이 변하지 않기 때문입니다.
이는 C의 printf가 하는 방식으로, 2의 거듭제곱을 자릿수로 사용합니다:
printf("%#hhb\n", -19); // 0b11101101
printf("%#hho\n", -19); // 0355
printf("%#hhx\n", -19); // 0xed
printf("%#b\n", -19); // 0b11111111111111111111111111101101
printf("%#o\n", -19); // 037777777755
printf("%#x\n", -19); // 0xffffffed
반대로, 부호 있는 수의 이진 표현을 음이 아니면 선행 0이 하나만, 음수이면 선행 1이 하나만 남도록 손실 없이 잘라낼 수 있다는 것도 분명할 것입니다:
45 (8-bit) 00101101
45 (7-bit) 0101101
-19 (8-bit) 11101101
-19 (7-bit) 1101101
이 예시들에서 숫자를 하나 더 잘라낸다면 둘 다 101101이 됩니다. 45와 -19는 2 ** 6 = 64를 법으로 합동이므로, 6개의 이진 숫자만 사용할 때는 둘을 구별할 수 없습니다. 따라서 부호 있는 정수 x를 최종 사용자에게 보여 주는 이진 문자열로 손실 없이 모호하지 않게 표현하기 위해, 우리는 n개의 숫자를 사용하는 사실상의 ‘최소 폭’ 표현 관례를 따릅니다. 여기서 n은 x가 range(-2 ** (n-1), 2 ** (n-1)) 안에 있도록 하는 가장 작은 정수입니다.
8진수와 16진수 문자열을 렌더링하려면 ‘최소 너비’ 표현 규칙의 정의를 충분히 모호하지 않도록 확장해야 합니다. 383의 최소 너비 이진 문자열은 0101111111이고, -129의 최소 너비 이진 문자열은 전자의 접미사인 101111111입니다. 16진수 문자열 포맷팅을 순진하고 잘못된 방식으로 구현하면 두 이진 표현을 모두 000101111111로 패딩하여 둘 다 '0x17f'로 렌더링할 것입니다. 이진 표현을 (16진수) 자릿수로 나눌 수 있도록 밑의 비트 수(16진법에서는 4비트)로 나누어떨어지는 이진 자릿수(12)를 원한 것은 옳았지만, 패딩한 것은 잘못이었습니다. 앞에서 살펴본 것처럼 대신 확장했어야 합니다. 383은 000101111111로, -129는 111101111111로 확장되며, 따라서 383은 '0x17f'로, -129는 0xf7f로 렌더링됩니다.
따라서 ‘최소 너비’ 표현 규칙을 일반화하여 정의하면 다음과 같습니다. 정수 x를 base 진법으로 렌더링할 때 n개의 자릿수를 생성하며, 여기서 n은 x가 range(-base ** n / 2, base ** n / 2)에 속하도록 하는 가장 작은 정수입니다.
이는 기각된 대안으로 이어집니다.
기각된 대안
z의 동작
2의 보수 방식 포맷팅 플래그인 z를 어떻게 구현해야 하는지에 대해서는 의견이 크게 두 진영으로 나뉘었으며, 무손실 표현과 손실 표현을 두고 의견이 갈렸습니다. 무손실 진영은 정수에 대응하는 포맷된 문자열이 모두 서로 달라야 하며, 최소 너비 표현 규칙으로 고유성이 보존되어야 한다고 생각합니다. z가 활성화된 경우에도 정밀도는 z가 없을 때와 마찬가지로 요청된 최소 자릿수일 뿐이어야 한다는 것입니다. 손실 진영은 z가 활성화된 경우 정밀도에 따라 먼저 모듈러 산술로 정수를 축소해야 한다고 생각합니다. 그러면 요청된 자릿수가 정확히 생성되며, 이는 최소 너비 표현 문자열을 왼쪽에서 잘라내는 것과 같습니다.
다음 절에서 우리는 전자의 진영, 즉 무손실 포매팅에는 사용 사례가 없으므로 거부된 아이디어라는 결론을 내리고자 하며, 이에 따라 이 PEP는 후자인 손실 동작을 제안합니다.
최소 너비 표현 규칙
이 아이디어는 오로지 무손실 동작 때문에 치열하게 검토되었지만, 모든 후보 사용 사례에서 사용 편의성을 해치는 걸림돌입니다. 문자열 렌더링의 미학에 관한 이러한 인자는 비합리적이거나 개인적 취향의 문제가 아니며, 오히려 정보가 최종 사용자에게 전달되는 방식에서 매우 중요합니다.
정수의 부호 여부를 전달하는 것이 중요한 프로그램에서는 z를 어떤 방식으로 구현하든 사용해서는 안 됩니다. 일반 사용자는 음수 부호 -를 보게 되리라 기대할 것이기 때문입니다. 최소 너비 표현 규칙을 사용하는 대안에서는 음수가 있을 때마다 밑 범위의 상위 절반에 속하는 수의 선행 자릿수(2진수는 1, 8진수는 4-7, 16진수는 8-f)를 불편할 정도로 주의 깊게 살펴야 합니다. 이 사실상의 규칙을 모르는 최종 사용자는 물론이고, 알고 있더라도 프로그램에 이 규칙이 쓰일 것이라고 예상하지 못한 사용자라면 어려움을 겪을 것입니다.
f"{x:z#.2x}"를 사용하여 128과 -128을 포맷팅하면 각각 '0x080'과 '0x80'이 생성됩니다. PEP 작성자는 정상적인 상황에서 '0x80'이 음수 128로 읽힐 가능성이 0%라고 생각합니다. 게다가 양수 128을 '0x080'으로 흉하게 렌더링하는 방식은 부호 유무와 관계없이 바이트를 균일한 간격으로 헥스덤프해야 하는 프로그램에서는 쓸모가 없습니다. 모든 바이트는 '0xNN' 형식으로 렌더링되어야 합니다. 모듈로 정밀도가 부호와 무관하게 바이트를 올바르게 처리하는 방법은 예제 절을 참조하십시오.
그러므로 대우를 취하면, z의 목적은 부호 여부가 중요하지 않은 환경에서 사용되는 것이며, 대개는 고정 레지스터 크기를 가진 2의 보수 하드웨어에서 생기는 모듈러 산술의 관점에서 정수를 다루는 것이 오히려 권장되는 환경에서 사용되는 것입니다. 위 예제에서 128과 -128은 256을 법으로 같으며, 적절한 렌더링은 '0x80'입니다. 일반적으로 z의 목적은 base ** precision을 법으로 합동인 정수를 같은 것으로 취급하는 것입니다. 마찬가지로 255와 -1은 각각 '0x0ff'와 '0xff'가 아니라 둘 다 '0xff'로 렌더링되어야 합니다. 잘라내기는 방해 요소가 아니라 의도한 동작입니다. 형식적으로 말하면, 포맷팅은 Z/(base ** precision)Z의 동치류와 precision개의 자릿수를 가진 문자열 사이의 잘 정의된 전단사 함수여야 합니다.
남은 질문은 사실상 왼쪽이 잘린 문자열에서 생기는 ‘정보 손실’에 대한 우려로서 “이러한 잘라내기를 사용자에게 알릴 방법은 없는가?”입니다. 하드웨어를 인식하는 정수의 경우와 그렇지 않은 경우를 모두 살펴봄으로써, 의도치 않은 정보 손실이 일어나는 경우가 있다는 이 질문의 전제를 받아들이지 않습니다.
하드웨어를 인식하는 정수와 관련해서는 지금까지 바이트의 부호 있는 범위와 부호 없는 범위의 합집합인 range(-128, 256)에 속하는 정수의 예를 다루어 보았습니다. x와 x - 256을 똑같이 포맷팅하는 것의 장점은 분명히 확인되었습니다. z가 쓰일 것으로 예상되는 이러한 맥락에서, 해당 범위를 벗어나는 바이트에 대응하는 잘못된 정수는 프로그래밍 오류일 가능성이 높습니다. 예를 들어 라이브러리가 픽셀 밝기 정수를 257로 설정하고 f"{x:z#.2x}"를 통해 '0x101' 대신 '0x01'을 출력한다면, 그것은 우리의 문제도 아니고 우리 탓도 아닙니다. 문자열 포맷팅은 예외를 발생시켜서는 안 되며, 잘못된 이스케이프 시퀀스 "\y"처럼 SyntaxWarning을 발생시켜서도 안 됩니다. bytes([257])로 그 정수를 직렬화하려고 하면 bytes가 ValueError: bytes must be in range(0, 256)을 발생시킬 것이기 때문입니다. 예외는 코드의 적절한 ‘계층’에서 발생하도록 하십시오. 그쪽이 문자열 포맷팅이 아니라 라이브러리의 결함을 더 잘 드러내기 때문입니다.
하드웨어를 고려하지 않는 정수의 경우, 모듈러 산술을 원하는 효과로 택할 때에만 의도적으로 z를 선택해야 합니다. 같은 이유로 range(-base ** precision / 2, base ** precision) 범위 밖에 있는 정수에 대해서도 SyntaxWarning이나 ValueError를 발생시키지 않습니다.
이로써 모듈로 정밀도로 구현된 z의 손실 있는 동작을 옹호했으며, 손실 없는 동작의 합리적인 사용 사례도 모두 검토했습니다.
마지막으로 고려했다가 기각한 절충안은 z를 .에 의존하는 플래그가 아니라 .와 결합할 수 있는 플래그로 구현하는 것입니다. 구체적으로 설명하면, . 없는 z는 2의 보수 모드를 켜서 포매팅된 정수를 최소 너비 표현으로 출력하고, z 없는 .는 이미 설명한 대로 정밀도, 즉 크기의 최소 자릿수와 필요한 경우 부호를 구현하며, .와 결합한 z는 왼쪽을 잘라 내는 모듈로 정밀도를 켭니다. 이런 미로 같은 조합은 누구에게도 유용해 보이지 않습니다. 최소 너비 표현 관례의 사용 편의성은 이미 부정했으므로 z가 단독으로 쓰이는 일은 드물 것이며, 각각은 최소 자릿수를 출력하는 두 옵션이 결합하면 정확한 자릿수를 출력한다는 동작은 직관에 어긋나 보입니다.
무한 길이 표시
덜 인기 있었던 또 다른 기각된 대안은 음이 아닌 수와 음수 앞에 각각 붙는 0 또는 1의 무한한 접두사를 z가 직접 표시하도록 하는 것이었습니다. 예를 들면 다음과 같습니다:
>>> f"{-1:z#.8b}"
'0b[...1]11111111'
>>> f"{300:z#.8b}"
'0b[...0]100101100'
이는 사실상 최소 너비 표현 관례에 ‘무한’ 접두사를 붙인 것입니다.
C 프로그래밍 언어에서 기계 너비에 의존하는 int 데이터의 2의 보수 정밀도 포매팅은 음수에서 지나치게 긴 접두사를 만들어 내며, 크기가 작은 음수에서도 마찬가지입니다:
printf("%#.2x\n", -19); // 0xffffffed
printf("%#.2llx\n", (long long unsigned int)-19); // 0xffffffffffffffed
최대 기계 너비의 제한이 없다면 이 접두사는 끝없이 이어질 수 있습니다!
Python의 int 타입은 실제로 최대 기계 폭에 제한되지 않습니다. 따라서 무한히 긴 2의 보수 문자열을 출력하지 않으려면, 자기 자신을 포함하는 리스트를 출력할 때 내장 list의 문자열 포매팅이 쓰는 방식과 비슷한 방법을 사용할 수 있습니다:
>>> l = []
>>> l.append(l)
>>> l
[[...]]
>>> y = -1
>>> f"{y:z#.8b}"
'0b[...1]11111111'
이 방식은 초보자에게 비트 단위 이진 연산이 어떻게 동작하는지 가르치는 데 유용했을 수 있습니다. 예를 들어 -1 & x가 언제나 자명하게 x와 같다는 것이나, 어떤 수의 부호를 바꾼 값의 이진 표현을 그 수의 비트 단위 보수에 1을 더해 얻을 수 있다는 것을 보여 줄 수 있습니다:
>>> x = 42
>>> f"{x:z#.8b}"
'0b[...0]00101010'
>>> f"{~x:z#.8b}"
'0b[...1]11010101'
>>> f"{x|~x:z#.8b}"
'0b[...1]11111111'
# x | ~x == -1
# x | ~x == x + ~x because of their disjoint bitwise representations
# thus x + ~x == -1
# thus -x == ~x + 1
>>> y = ~x + 1
>>> f"{y:z#.8b}"
'0b[...1]11010110'
>>> y == -x
True
하지만 사용 사례가 너무 좁고, 모듈로 정밀도가 이보다 훨씬 낫습니다.
일반 사항
- 1의 보수나 다른 이진 표현은 어떻습니까?
2의 보수가 워낙 지배적이어서 다른 표현을 진지하게 고려하는 사람은 없습니다. GCC는 2의 보수만 지원합니다.
- 아무것도 하지 않으면 어떻습니까?
프로그래머들은 정밀도를 흉내 내기 위해 임시방편적인 보정과 함께
width형식 지정자를 계속 어설프게 사용하고 있습니다. 이는 용납할 수 없는 일이며, 이 PEP의 근거는 정밀도의 추가와 구현 방식 선택에 대해 결정적인 인자를 제시합니다..를 사용한 정수 필드 정밀도를 구현하지 않으면.를 향후 다른 용도로 쓸 수 있도록 남겨 둘 수 있습니다. 그러나 PEP 3101 이후 약 20년 동안 어떤 대안도 채택되지 않았으며,.를 다른 용도로 쓰면 옛 방식의%포매팅과 C 프로그래밍 언어 양쪽 모두와 더욱 어긋나게 됩니다.
문법
- 모듈로 정밀도를 적용한 정밀도에
z.대신!를 사용하며, 이는.와 상호 배타적입니다.장점:
!는 모양상.와 관련이 있으며, 말하자면.의 확장입니다. 모듈로 정밀도 플래그를 설정한 정밀도는 실제로 정밀도의 확장입니다.- 영어에서
!는 명령조의 명령문에 자주 쓰입니다. 마찬가지로 모듈로 정밀도는 입력을 포맷할 자릿수를 정확히 지정하는 반면, 정밀도는 최소 자릿수를 지정합니다. 이는 관용적인 용법입니다. !는z.와 달리 기호 하나뿐입니다. 여기에!가.와 상호 배타적이라는 점이 더해져, 모듈로 정밀도를 켜더라도 작성한 코드의 전체 길이가 달라지지 않습니다.- 새로운
!기호를 사용하면z를 앞으로의 다른 용도를 위해 남겨 둘 수 있습니다. 그 용도가 무엇이든 말입니다.
단점:
z.는.에 붙은 플래그로서.를 확장한다는 느낌도 전달하며, 어휘적으로도 왼쪽에서 오른쪽으로 ‘modulo’(z) ‘precision’(.) 순으로 자연스럽게 읽힙니다..와!가 서로 상호 배타적이면, 초보 프로그래머가 포맷 명세 문서를 볼 때 무엇을 골라야 할지 결정하지 못하고 망설이게 될 수 있습니다.!는 단 하나의 용도를 위해 포맷 명세에 또 하나의 요소를 추가하는 것이 됩니다.str,float등 다른 어떤 타입에도 해당 구현이 없을 것입니다.- 또한 PEP 3101에 규정된 대로 포맷 문자열 문법에는 이미
["!" conversion]“명시적 변환 플래그”가 존재합니다. 예를 들어f"{s!r}"에서!r은s에 대해repr을 호출합니다. 형식 지정자[":" format_spec]는 잘 정의된 앞쪽 콜론으로 구분되므로 이는!형식 지정자와 문법적으로 충돌하지 않습니다만, 새로운 모듈로 정밀도 모드에 익숙하지 않은 사용자는!를 포함한 형식 문자열을 대충 훑어보고 다른 동작을 기대할 수 있습니다.
결론:
!는 시각적으로는 매력적이지만, 기존z플래그를 오버로딩하여 달성할 수 있는 단 하나의 용도를 위해 포맷 명세를 복잡하게 만들 것입니다.
하위 호환성
PEP 682를 인용하면 다음과 같습니다:
새로운 포맷 동작은 옵트인 방식이므로 기존 프로그램의 숫자 포맷에는 영향을 주지 않습니다.
다만 누군가가 현재처럼 .가 정수에 대해 ValueError를 발생시키는 동작에 특별히 의존하고 있다면 예외입니다. 그러나 PEP 475를 인용하면 다음과 같습니다:
이 PEP의 저자들은 그러한 애플리케이션이 존재한다고 생각하지 않습니다
예제와 교육
정밀도
Python의 영향권에 있는 문서와 튜토리얼은 치환 필드 전체의 최소 길이가 아니라 최소 자릿수가 필요한 것이 분명한 경우, int 필드를 포매팅하기 위한 기본 형식 지정자로 width 대신 정밀도인 .의 채택을 권장해야 합니다.
정밀도 개념은 C 같은 다른 언어에서도 흔하고 Python의 구식 % 포맷에도 이미 있었으므로 지나치게 많이 다룰 필요는 없지만, 아래와 같은 적당한 수의 예제로 그 용도를 보여 줄 수 있습니다.
>>> def hexdump(b: bytes) -> str:
... return " ".join(f"{c:#.2x}" for c in b)
>>> hexdump(b"GET /\r\n\r\n")
'0x47 0x45 0x54 0x20 0x2f 0x0d 0x0a 0x0d 0x0a'
# observe the CR and LF bytes padded to precision 2
# in this basic HTTP/0.9 request
>>> def unicode_dump(s: str) -> str:
... return " ".join(f"U+{ord(c):.4X}" for c in s)
>>> unicode_dump("USA 🦅")
'U+0055 U+0053 U+0041 U+0020 U+1F985'
# observe the last character's Unicode codepoint has 5 digits;
# precision is only the minimum number of digits
모듈로 정밀도
모듈로 정밀도의 사용을 권장할 만한 분명한 영역은 struct로 패킹하고 언패킹하는 정수처럼 머신 폭을 기준으로 하는 정수를 다룰 때입니다. 부호 있는 정수와 부호 없는 정수를 일관되고 예측 가능하게 2의 보수로 포맷하는 예를 제시합니다.
>>> import struct
>>> my_struct = b"\xff"
>>> (t,) = struct.unpack('b', my_struct) # signed char
>>> print(t, f"{t:#.2x}", f"{t:z#.2x}")
'-1 -0x01 0xff'
>>> (t,) = struct.unpack('B', my_struct) # unsigned char
>>> print(t, f"{t:#.2x}", f"{t:z#.2x}")
'255 0xff 0xff'
# observe in both the signed and unsigned unpacking the modulo-precision flag 'z'
# produces a predictable two's complement formatting
참조 구현
이 PEP를 구현한 풀 리퀘스트는 GitHub에서 확인할 수 있습니다: python/cpython#146437.
감사의 말
다음 분들께 감사드립니다:
- 2의 보수 동작을 처음 제안해 주신 Raymond Hettinger에게 감사드립니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.