파이썬 코드에서 문자열은 자주 사용된다. 메세지를 표시하거나, 데이터를 쓰거나, Exception에 자세히 기록하거나, 디버깅을 할 때 등등.
형식화, formatting, 포맷팅
형식화(formatting)는 미리 정의된 문자열에 데이터 값을 끼워 넣어 사람이 보기 좋은 문자열로 저장하는 과정이다. 파이썬에서는 언어에 내장된 기능과 표준 라이브러리를 통해 네 가지 방식으로 형식화를 할 수 있다. 그러나 한 가지 방법을 제외한 나머지는 모두 치명적인 단점이 존재한다.
% 형식화
파이썬에서 문자열을 형식화하는 가장 일반적인 방법은 % 형식화 연산자를 사용하는 것이다. 이 연산자 왼쪽에 들어가는 미리 정의된 텍스트 템플릿을 형식 문자열이라 한다. 템플릿에 끼워 넣을 값들은 연산자의 오른쪽에 단일 값이나 tuple로 지정한다. 예를 들면 다음과 같다.
a = 0b10111011
b = 0xc5f
print('이진수 : %d, 십육진수 : %d' % (a, b))
>>>
이진수 : 187, 십육진수 : 3167
형식 문자열은 연산자 왼쪽에 값을 끼워 넣기 위해 %d같은 형식 지정자를 사용한다. 이 문법은 C의 printf 함수에서 비롯되었기 때문에 아마 C언어를 했던 사람이라면 익숙한 형태일 것이다. (이는 비단 다른 프로그래밍 언어도 다르지 않다.) 파이썬은 %s, %x, %f 등 C언어의 printf에 사용할 수 있는 대부분의 형식 지정자를 지원하며 소수점 위치나 패딩, 채워 넣기, 좌우 정렬 등도 제공한다. 그래서 다른 언어를 접했다가 파이썬을 사용하는 사람들은 익숙함에 이끌려 대부분 이러한 C 스타일 형식 문자열을 사용한다.
하지만 해당 형식 문자열을 사용할 때 발생하는 세 가지 문제점이 있다.
첫 번째 문제점
형식화 식에서 오른쪽에 있는 튜플 내 데이터 값의 순서를 바꾸거나 값의 타입을 바꾸면 타입 변환이 불가능해 오류가 발생할 수 있다는 점이다. 즉, 위치를 바꿀 경우 오류가 발생할 수 있다. 예를 들어 다음과 같은 간단한 형식 문자열은 잘 작동한다.
key = 'my_var'
value = 1.234
formatted = '%-10s = %.2f' % (key, value)
print(formatted)
>>>
my_var = 1.23
하지만 key와 value의 위치를 바꾸면 실행 시점에 예외가 발생한다.
formatted = '%-10s = %.2f' % (value, key)
>>>
Traceback (most recent call last):
TypeError: must be real number, not str
마찬가지로 오른쪽에 있는 파라미터의 순서를 유지하고 형식 문자열의 순서를 바꿔도 오류가 발생한다.
formatted = '%-10f = %.2s' % (key, value)
>>>
Traceback (most recent call last):
TypeError: must be real number, not str
이러한 오류를 피하려면 % 연산자의 좌우가 서로 잘 맞는지 늘 검사해야 한다. 코드를 변경할 때마다 사람이 직접 검사해야 하므로 실수가 나오기 쉽다.
두 번째 문제점
형식화를 하기 전에 값을 살짝 변경해야 한다면 식을 읽기가 매우 어려워진다는 점이다. 다음 예제를 보면 각 과일의 무게를 변경하지 않고 열거했다.
fruit = [('apple', 1.25), ('banana', 2.5), ('pear', 3.6)]
for i, (item, weight) in enumerate(fruit):
print('#%d: %-10s = %.2f' % (i, item, weight))
>>>
#0: apple = 1
#1: banana = 2
#2: pear = 3
이제 여기서 출력된 메세지를 조금 더 가독성이 좋도록, 쓸모가 있도록 바꾸고 싶다 생각해 여러 메소드를 추가할 수 있다. 허나 이렇게 하면 형식화 식에 있는 tuple의 길이가 너무 길어져 가독성이 나빠진다. (물론 이 정도는 상관 없다고 생각할 수도 있으나 더 길어질 경우를 상정해보자.)
for i, (item, weight) in enumerate(fruit):
print('#%d: %-10s = %d' % (i + 1, item.title(), round(weight)))
>>>
#1: Apple = 1
#2: Banana = 2
#3: Pear = 4
세 번째 문제점
형식화 문자열에서 같은 값을 여러 번 사용하고 싶다면 튜플에서 같은 값을 여러 번 반복해야 한다는 점이다.
template = '%s는 음식을 좋아해. %s가 요리하는 모습을 봐요.'
name = 'reo'
formatted = template % (name, name)
print(formatted)
>>>
reo는 음식을 좋아해. reo가 요리하는 모습을 봐요.
이런 식으로 여러 값을 반복해야 하면 값을 살짝 변경해야 할 경우에 실수하기도 쉽고 코딩하기도 성가시다. 예를 들어 다음 예제에서는 title()메서드를 빼먹지 않고 여러 번 호출했지만 어느 한 name에 대해서는 title() 메서드를 호출하고 다른 참조에 대해서는 호출을 잊어버릴 수도 있다.
name = 'reo'
formatted = template % (name.title(), name.title())
print(formatted)
>>>
Reo는 음식을 좋아해. Reo가 요리하는 모습을 봐요.
세 번째 문제점까지 살펴봤다. 네 번째 문제점은 후에 다시 언급한다.
이런 문제들을 해결하기 위해 파이썬의 % 연산자에는 튜플 대신 딕셔너리를 사용해 형식화하는 기능이 추가됐다. 딕셔너리의 키는 형식 지정자에 있는 키(예 : %(key)s)와 매칭된다. 다음 예제에서는 이 기능을 사용해 출력에 아무런 영향을 주지 않으며 형식화 식의 오른쪽에 있는 값의 순서를 바꿔 보았다. 이렇게 앞에서 설명한 첫 번째 문제점을 해결할 수 있다.
key = 'my_var'
value = 1.234
old_way = '%-10s = %.2f' % (key, value)
new_way = '%(key)-10s = %(value).2f' % {'key' : key, 'value' : value}
reordered = '%(key)-10s = %(value).2f' % {'value' : value, 'key' : key}
assert old_way == new_way == reordered
형식화 식에 딕셔너리를 사용하면 여러 형식 지정자에 같은 키를 지정할 수 있어 같은 값을 반복하지 않아도 되므로 세 번째 문제도 해결된다.
name = 'reo'
template = '%s는 음식을 좋아해. %s가 요리하는 모습을 봐요.'
before = template % (name, name) # 튜플
template = '%(name)s는 음식을 좋아해. %(name)s가 요리하는 모습을 봐요.'
after = template % {'name' : name}
assert before == after
하지만 딕셔너리 형식 문자열을 사용하면 다른 문제가 더 심해지거나 새로운 문제가 생긴다. 바로 두 번째 문제점이 더 심각해지는 점이다. 각 키를 최소 두 번을 반복하게 되고, 키에 해당하는 값이 변수에 들어 있다면 변수 이름까지 세 번 이상 같은 이름을 반복해서 사용하게 될 수도 있다. 즉 너무 문장이 번잡스러워져 나중에 코드를 해석하기도 힘들어진다.
내장 함수 format과 str.format
파이썬 3부터는 %을 사용하는 오래된 C 스타일 형식화 문자열보다 더 표현력이 좋은 고급 문자열 형식화 기능이 도입됐다. 이 기능은 format 내장 함수를 통해 모든 파이썬 값에 사용할 수 있다. 예를 들어 다음 코드는 새로운 옵션(천 단위 구분자를 표시할 때 쓰는 ,와 중앙에 값을 표시하는 ^)을 사용해 값을 형식화한다.
a = 1234.5678
formatted = format(a, ',.2f')
print(formatted)
b = 'my 문자열'
formatted = format(b, '^20s')
print('*', formatted, '*')
>>>
1,234.57
* my 문자열 *
str 타입에 새로 추가된 format 메서드를 호출하면 여러 값에 대해 한꺼번에 여러 기능들을 적용할 수 있다. 또한 %d같은 C 스타일 형식화 지정자를 사용하기보다 {} 위치 지정자를 사용함으로써 더 편해진다. 기본적으로 위치 지정자는 format 메서드에 전달된 인자 중 순서상 같은 위치에 있는 인자를 가리킨다.
key = 'my_var'
value = 1.234
formatted = '{} = {}'.format(key, value)
print(formatted)
>>>
my_var = 1.234
각 위치 지정자에는 콜론 뒤에 형식 지정자를 붙여 문자열에 값을 넣을 때 어떤 형식으로 변환할지 정할 수 있다.
key = 'my_var'
value = 1.234
formatted = '{:<10} = {:.2f}'.format(key, value)
print(formatted)
>>>
my_var = 1.23
위치 지정자를 적용한 결과는 그 위치에 해당하는 값과 : 뒤에 있는 형식 지정자를 foramt 내장 함수에 전달해 얻은 결과와 같다. 따라서 앞에 예의 {:.2f}는 format(value, '.2f')와 같다.
잠시 사족을 붙이면 C 스타일 형식화 문자열에서 % 문자를 표현하고 싶으면 %가 형식 지정자로 해석되지 않도록 %%로 이스케이프해야 한다. 마찬가지로 str.format을 사용할 때는 중괄호를 이스케이프 해야 한다.
print('%.2f%%' % 12.5)
print('{} replaces {{}}'.format(1.23))
>>>
12.50%
1.23 replaces {}
위치 지정자에 절단된 인자의 순서를 표현하는 위치 인덱스를 전달할 수도 있다. 이렇게 하면 인자의 순서를 바꾸지 않아도 값의 출력 순서를 변경할 수 있어, 앞서 설명했던 C 스타일 형식 문자열의 첫 번째 문제점을 해결할 수 있다.
key = 'my_var'
value = 1.234
formatted = '{1} = {0}'.format(key, value)
print(formatted)
>>>
1.234 = my_var
또한 같은 위치 인덱스를 여러 번 사용할 수도 있다. 이렇게 하면 format에 넘기는 인자에 값을 여러번 반복할 필요가 없어져 C 스타일 형식 문자열의 세 번째 문제점도 해결된다.
name = 'reo'
formatted = '{0}은 음식을 좋아해. {0}가 요리하는 모습을 봐요.'.format(name)
print(formatted)
>>>
reo은 음식을 좋아해. reo가 요리하는 모습을 봐요.
허나 아쉽게도 새로운 format 메서도도 앞서 설명한 두 번째 문제점은 해결하지 못한다. 따라서 형식화를 하기 전에 값을 조금 변경해야 하면 코드 읽기가 어려워진다. 또한 네 번째 문제점도 해결하지 못한다.
인터폴레이션을 통한 형식 문자열
이러한 문제들을 완전히 해결하기 위해 파이썬 3.6부터 인터폴레이션을 통한 형식 문자열(f-문자열)이 도입됐다. 이 문법에서는 형식 문자열 앞에 f 문자를 붙여야 한다.
f-문자열은 형식 문자열의 표현력을 극대화하고, 앞서 설명한 네 번째 문제점인 형식화 문자열에서 키와 값을 불필요하게 중복 지정해야 하는 경우를 없애준다.
key = 'my_var'
value = 1.234
formatted = f'{key} = {value}'
print(formatted)
>>>
my_var = 1.234
앞 절에서 살펴본 format 함수의 형식 지정자 안에서 콜론 뒤에 사용하는 내장 미니 언어를 f-문자열에서도 사용할 수 있다. 값을 유니코드나 repr 문자열로 변환하는 기능 역시 사용 가능하다.
key = 'my_var'
value = 1.234
formatted = f'{key!r:<10} = {value:.2f}' # !r이 repr문
print(formatted)
>>>
'my_var' = 1.23
f-문자열을 사용한 형식화는 C 스타일보다, str.format보다 항상 더 짧다. 다음 예제에서 비교해보자.
key = 'my_var'
value = 1.234
f_string = f'{key:<10} = {value:.2f}'
c_tuple = '%-10s = %.2f' % (key, value)
str_args = '{:<10} = {:.2f}'.format(key, value)
str_kw = '{key:<10} = {value:.2f}'.format(key=key, value=value)
c_dict = '%(key)-10s = %(value).2f' % {'key': key, 'value': value}
assert c_tuple == c_dict == f_string
assert str_args == str_kw == f_string
f-string을 사용하면 위치 지정자 중괄호 안에 완전한 파이썬 식을 넣을 수 있어 값을 변경하고 싶을 때에도 간결한 구문으로 표기할 수 있으므로 앞서 설명한 두 번째 문제점을 해결한다.
fruit = [('apple', 1.25), ('banana', 2.5), ('pear', 3.6)]
for i, (item, weight) in enumerate(fruit):
old_style = '#%d: %-10s = %d' % (i + 1, item.title(), round(weight))
new_style = '#{}: {:<10s} = {}'.format(i + 1, item.title(), round(weight))
f_string = f'#{i+1}: {item.title():<10s} = {round(weight)}'
assert old_style == new_style == f_string
참고
- 파이썬 코딩의 기술 - 똑똑하게 코딩하는 법, 브렛 슬라킨 저/오현석 역
'◎ Python > 코딩의 기술 (책)' 카테고리의 다른 글
[코딩의 기술] 6. 인덱스보다는 대입을 사용해 데이터를 언패킹하자 (1) | 2022.11.30 |
---|---|
[코딩의 기술] 5. 복잡한 식을 쓰는 대신 도우미(헬퍼) 함수를 작성하자 (0) | 2022.11.29 |
[코딩의 기술] 3. bytes와 str의 차이를 알자 (0) | 2022.11.27 |
[코딩의 기술] 2. PEP 8 스타일 가이드를 따르자 (2) | 2022.09.15 |
[코딩의 기술] 1. 사용 중인 파이썬의 버전을 알자 (1) | 2022.09.14 |
자기계발 블로그