2006년 3월 11일 printf와 친구들 (3)
한동안 정신이 없어서 글을 못 썼으므로 반성의 의미로 3편을 만들어서 올린다.
Python
파이썬은 "format string" % (arg, arg, args...)
형태의 interpolation operator를 지원한다. 물론 다들 짐작하셨겠지만 이건 C의 sprintf(buf, "format string", arg, arg, args...)
와 거의 비슷하고, %로 시작하는 내용도 엇비슷하다. 하지만 언어가 언어다 보니까 거기에 맞는 포매팅 문법이 많이 추가되었다는 게 특징이다. (즉 펄이 C 등과의 호환성을 유지한다면, 파이썬은 호환성은 대충 신경만 쓰고 언어에 맞게 뜯어 고친 흔적이 많다.)
파이썬은 기본적으로 ANSI C의 printf와 비슷한 문법을 사용한다. 하지만 포인터를 직접적으로 다룰 수 있는 언어에서나 볼 법한 %n
은 지원하지 않는다. 문법적으로 특이한 것은 %(mapping_key)03d
와 같이 괄호로 묶은 내용을 연관 배열의 키로 지정할 수 있다는 점이다. 즉,
>>> print '%(language)s has %(#)03d quote types.' % {'language': "Python", "#": 2}
Python has 002 quote types.
이런 게 가능하다. ({}
안에 있는 것이 연관 배열이다.) 근데 개인적으로 난감한 것은, 이 연관 배열 키 지정하는 게 문자열(-_-)만 된다는 점이다. 물론 따지고 보면 이 명령의 사용법은 원래 다음과 같은 것을 지원하려는 것이었을 테지만,
>>> a = 'hello'; b = 42; c = (3,1,4,1,5,9,2)
>>> print '%(a)s, world! %(c)s is first 7 digits of pi (but how about %(b)d?)' % locals()
hello, world! (3,1,4,1,5,9,2) is first 7 digits of pi (but how about 42?)
그러니까, 변수들을 담고 있는 게 다름 아닌 연관 배열-_-이기 때문에 이런 짓을 할 수 있다는 것이다. 하지만 내 생각에는 포매팅 문법으로 이런 거 하느니 ()에다가 필요한 것만 담아 놓는 거랑 별반 다를 게 없다고 본다.
그 외에 몇 가지 특징을 살펴 보면,
%r
문법은 받은 인자를repr
함수를 써서 문자열로 바꾼다. (%s
는str
이다.str
과repr
의 차이는repr
은 객체의 "내부 구조"를 알기 쉬운 형태로, 그리고 가급적이면 그 객체를 생성하는 올바른 파이썬 코드가 되도록 문자열이 생성된다는 것이다.)%hd
,%ld
,%Ld
같은 건 허용은 되지만h
,l
,L
등은 모두 무시된다.- 이걸 feature라고 해야 할 지 알 수가 없는데, 사실 연관 배열은 위에서 제시한 방법 말고 다른 방법으로 쓰기가 참 힘들다.
%*d
같은 문법도 사용하면 에러가-_- 난다.
Ruby
루비의 Kernel::sprintf
메소드는 펄의 영향을 어느 정도 받았기 때문에 펄과 비슷한 형태의 문법을 사용한다. (단 %v
는 없다.) 그 외에 다음과 같은 특징이 있다.
- 파이썬의
%r
에 해당하는%p
(inspect
메소드를 호출함)가 있다. - 파이썬과 루비는 모두 다 임의 자릿수 정수(파이썬은
long
, 루비는Bignum
)를 지원하지만%x
의 출력 결과는 다르다. -2**100이라는 숫자를 출력한다면 파이썬은 "-10000000000000000000000000", 루비는 "..f0000000000000000000000000"라고 나타낸다.
덤으로 루비도 파이썬같이 %
연산자를 지원하는데, "format" % args
는 Kernel::sprintf("format", *args)
와 같다. 물론 sprintf
에 들어 가는 인자이기 때문에 args
에 연관 배열이 올 수는 없다.
Lua
루아의 string.format
함수도 나름대로 printf를 구현하긴 하는데 ANSI C 문법에서 %*d
같이 *
를 쓸 수 없다는 것과 파이썬의 %r
, 루비의 %p
에 해당하는 %q
(문자열을 자동으로 quote하기)가 있다는 것 말고 설명할 게 없어서 건너 뛰기로 하겠다. -_-;
PHP
(까먹고 PHP를 안 썼다는 지적을 받아서 나중에 추가했음)
PHP의 printf
는 %v
지원 안 하는 것만 빼면 역시 펄과 비슷하다. 한 가지 다른 점이라면, %
뒤에 'char
라고 하면 자리 채움에 사용할 문자를 지정할 수 있다는 것이다. 즉, 다음과 같은 게 가능하다.
printf("%'_10d", 3141592); // ___3141592라고 출력됨
.NET
수많은 %
에 질리셨을 것 같아서 슬슬 %
안 쓰는 언어들로 넘어 가야 할 것 같다.
%
로 시작하는 printf 형태의 문법에는 몇 가지 문제가 있었는데,
- 잘못 쓰기가 너무 쉽다(error-prone).
%
부터 어디까지가 포매팅 문법의 끝인지 알아 차리려면 말 그대로 순서대로 글자를 스캐닝-_-해야 한다는 문제가 있다. 나중에 추가된 %1$d 같은 문법들까지 들어 가면 이게 도대체 포매팅 문법인지 암호인지 알 수가 없다. - 어떤 인자를 가지고 포매팅을 할 지 선택하는 게 쉽지 않다. 아까 말했지만
%1$d
같은 경우 보기도 좋지 않고*
문법과 결합하면 그야말로 악몽이 되어 버린다. (한 번%3$*1$.*2$f
같은 문법을 생각해 보시면 되겠다. -_-) - 결정적으로 사용자가 문자열을 포매팅하는 별도의 방법을 지원하지 않는다. 거의 대부분 이미 지정된 내부 객체로만 변환할 수 있는 정도 뿐이고 사용자가 필요한 옵션을 지정하기는 힘들다. :( 물론 glibc 같은 경우 아직 안 지정된 글자에 사용자가 지정한 함수를 연결시키는 (예를 들어서
%v
에 무슨 함수... 이런 식으로) 것도 되긴 하지만 한계가 있다.
.NET 프레임워크의 String.Format
메소드는 문자열을 포매팅하는 새로운 방법을 제공한다. (물론 앞에서 말한 몇 가지 문제점을 해결하긴 했지만, 이게 완벽한 건 아니다. 예를 들어서 %*d
같은 문법은 별도로 지원되지 않는다.) 새로운 포매팅 문법은 다음과 같이 생겼다.
{index[,alignment][:options]}
index
는 사용할 인자의 번호(첫 인자가 1)이고, alignment
는 printf의 길이 지정하는 부분과 같으며, 마지막으로 options
는 포매팅 옵션을 문자열 형태로 지정해 준다. {
나 }
를 그대로 쓰려면 {{
나 }}
를 써야 한다.
여기서 options
가 추가되었다는 게 가장 중요한데, 예측하셨듯이 객체 별로 포매팅을 어떻게 할 건지 결정하는 옵션 부분이다. 자기가 만든 클래스가 이런 기능을 사용하게 하려면 IFormattable
인터페이스의 ToString
메소드를 구현해서 쓰면 되는 것이다. options
가 생략되면 "G
"(general)라는 문자열이 기본으로 들어 간다.
options
부분에 뭐가 들어 가느냐는 물론 객체마다 다르다. (개인적으로는 %c
같이 "변환"을 지정할 수 없다는 게 살짝 아쉽다.) 수치형의 경우 다음과 같은 "기본" 옵션들이 제공된다.
C
(currency): 주어진 숫자를 "$123,456,789.00" 형태로 포매팅한다. 물론 이 결과는 현재 culture(C의 로캘 같은 거)에 따라 달라진다.D
(decimal),E
(exponential),F
(floating point),G
(general),X
(hexadecimal): printf의%d
,%e
,%f
,%g
,%x
와 같다. 특히E
,G
,X
의 경우 대소문자에 따라서 포매팅에 어떤 문자를 쓸 지 결정된다.N
(number): 콤마로 구별된(이 역시 culture에 따라 결정됨) 숫자를 출력한다.P
(percent): 주어진 숫자가 비율을 나타낸다고 보고 백분율로 나타낸다. 예를 들어서 0.527은 "52.7%"처럼 변환된다.R
(round-trip): 출력된 내용을 다시 숫자로 변환했을 때 정확히 같은 수치가 나오도록 최대한 많은 유효자릿수로 출력한다.
그리고 이런 "기본" 옵션 말고 세세한 모양을 지정할 수 있는 다음 문자들이 있다. (엑셀 같은 거 써 보신 분은 각 셀에 숫자 등이 출력될 방법을 지정하는 걸 보셨을 것이다. 그거랑 비슷하다.)
0
,#
: 숫자가 출력될 위치를 지정한다. (예를 들어서 "### ##"라고 하면 1234는 " 12 34"라고 출력될 것이다.)0
과#
의 차이는 01234같이 필요 없는 0이 0 그대로 출력될 건가 공백으로 출력될 건가의 차이다..
: 소숫점의 위치를 지정한다. 하나 이상 나오면 그 다음부터는 무시된다.,
: 0이나 # 사이에 있는 콤마는 천단위 구분자(1,234,567 같은)의 위치를 지정한다. 소숫점 앞이나 문자열 맨 끄트머리에 나오는 콤마는 그 갯수만큼 원래 값을 1000으로 나누는 역할을 한다. ("####,"라고 하면 1234567은 "1235"라고 출력된다. 근데 왜 하필 1000이란 말인가? -_-)%
: 백분율 기호가 나올 위치를 지정한다.E+0
,E-0
,E0
등등...: 과학적 표기법에서 사용되는 "e+123" 같은 내용이 어디 나오는 지 지정한다. 좀 더 정확하게 말하면, "E", "e"가 문자열 중간에 나오면 그 뒷부분은 모두 지수(exponent) 부분이 된다.'...'
,"..."
: 따옴표에 둘러 싸인 내용은 그대로 출력된다. (사실 해석되지 않은 문자도 모두 그냥 출력된다);
: 세미콜론은 "양수일 때 문자열; 음수일 때 문자열; 0일 때 문자열" 식으로 포매팅 문자열을 나누는 데 사용된다.
이런 문법의 장점은 전체 몇 자리, 소숫점 몇 자리 식으로 쓰는 것보다 훨씬 자유도가 높다는 것이겠고, 단점이라면 문법을 쓰기가 좀 귀찮-_-다는 것이겠다. 아무튼 언어 만들면서 새로운 포매팅 방법 같은 걸 만들어야 한다면 고려해 볼 만할 듯하다.
다음 4편이 (까먹은 게 없다면) 마지막인데, 마지막으로 다룰 두 종류의 내용이 엄청나게 방대하기 때문에 어떻게 설명해야 할 지 참 난감해진다. -_-; 아무튼 조만간 써서 올리겠다.
덤: 그러고 보니 이번 편은 생각보다 너무 길어졌다. -_-
전체 글 목록은 다음과 같다.
- 2006-02-25: printf와 친구들 (1)
- 2006-02-28: printf와 친구들 (2)
- 2006-03-11: printf와 친구들 (3)
4편은 결국 올리지 못 했다. 4편에서 계획하고 있던 글은 커먼 리습의 format
에 대한 얘기였지만, 이게 사실 굉장히 복잡하기 때문에 (pretty-print와 연결되어 있기도 하고) 쓸 엄두를 내지 못 하고 결국 잊어버린 것 같다. (2010-03-25)