메아리 저널

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 함수를 써서 문자열로 바꾼다. (%sstr이다. strrepr의 차이는 repr은 객체의 "내부 구조"를 알기 쉬운 형태로, 그리고 가급적이면 그 객체를 생성하는 올바른 파이썬 코드가 되도록 문자열이 생성된다는 것이다.)
  • %hd, %ld, %Ld 같은 건 허용은 되지만 h, l, L 등은 모두 무시된다.
  • 이걸 feature라고 해야 할 지 알 수가 없는데, 사실 연관 배열은 위에서 제시한 방법 말고 다른 방법으로 쓰기가 참 힘들다. %*d 같은 문법도 사용하면 에러가-_- 난다.

Ruby

루비의 Kernel::sprintf 메소드는 펄의 영향을 어느 정도 받았기 때문에 펄과 비슷한 형태의 문법을 사용한다. (단 %v는 없다.) 그 외에 다음과 같은 특징이 있다.

  • 파이썬의 %r에 해당하는 %p(inspect 메소드를 호출함)가 있다.
  • 파이썬과 루비는 모두 다 임의 자릿수 정수(파이썬은 long, 루비는 Bignum)를 지원하지만 %x의 출력 결과는 다르다. -2**100이라는 숫자를 출력한다면 파이썬은 "-10000000000000000000000000", 루비는 "..f0000000000000000000000000"라고 나타낸다.

덤으로 루비도 파이썬같이 % 연산자를 지원하는데, "format" % argsKernel::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편이 (까먹은 게 없다면) 마지막인데, 마지막으로 다룰 두 종류의 내용이 엄청나게 방대하기 때문에 어떻게 설명해야 할 지 참 난감해진다. -_-; 아무튼 조만간 써서 올리겠다.

덤: 그러고 보니 이번 편은 생각보다 너무 길어졌다. -_-

전체 글 목록은 다음과 같다.

4편은 결국 올리지 못 했다. 4편에서 계획하고 있던 글은 커먼 리습의 format에 대한 얘기였지만, 이게 사실 굉장히 복잡하기 때문에 (pretty-print와 연결되어 있기도 하고) 쓸 엄두를 내지 못 하고 결국 잊어버린 것 같다. (2010-03-25)

이 글은 본래 http://tokigun.net/blog/entry.php?blogid=65에 썼던 것을 옮겨 온 것입니다.


(rev 1d46270eb038)