[파이썬 튜토리얼] 문자열 포매팅
f-string, placeholder와 format 메소드, numbered placeholder, named placeholder, 중괄호 escaping, f-string debug support, format specifier 등
Table of contents
Level 1
터미널로 입력된 문자열의 앞에, input was: 라는 내용을 붙여 다시 출력해야 한다고 하자. 앞의 내용을 되짚어보기 위해, 입력된 값을 그대로 출력해주는 코드를 먼저 작성했다.
x = input()
print(x)
입력
hello
결과
hello
hello가 입력되었을 때 input was: hello라는 내용을 출력하기 위해서는, 터미널에서 입력된 내용을 담고 있는 변수 x의 앞에 문자열을 붙여줘야 한다. 다음처럼 문자열 내에 변수의 이름을 넣는 것만으로는 해결할 수 없다.
x = input()
print('input was: x')
입력
hello
결과
input was: x
이번 단원에서 소개할 문자열 포매팅을 통해 이 문제를 풀어낼 수 있다. 문자열 포매팅은 '이 자리에 무언가 값이 채워질 것임'을 명시하는 placeholder(자리 표시자)를 문자열 내에 포함시키고, 그 placeholder를 대체할 값을 명시하는 것이 기본적인 흐름이다.
파이썬은 문자열 포매팅을 지원하기 위한 기능을 지속적으로 추가해 왔다. 그러므로 문자열 포매팅을 위한 문법은 여러가지가 있다. 가장 최근에 추가된 f-string을 시작으로, 그보다 더 예전에 사용하던 방식들을 최신순으로 설명할 것이다. 다만 현재 Python 3.7이 최소 버전으로 권고되기 때문에, 가장 처음 소개할 f-string에 대해서만 읽고 지나가도 된다.
소프트웨어의 업데이트가 더 이상 이루어지지 않는 시점을 End Of Life(EOL)이라고 말한다. Windows XP나 Android 8(Oreo)을 예로 들 수 있다. 보안 업데이트에 한해 관리를 유지해주는 경우가 있어서, 보통은 보안 업데이트까지 지원 종료되는 시점을 EOL로 치곤 한다. endoflife.date에 잘 정리되어 있다. Python 3.6까지의 버전은 모두 EOL을 넘겼다. EOL을 넘긴 버전은 되도록 사용하지 않는 것이 좋다.
f-string
Python 3.6 버전에는 f-string이라는 기능이 추가되었다. 문자열을 시작하는 따옴표 앞에 f를 붙이고, 문자열 내에 포함시키고자 하는 내용을 중괄호로 감싸주면 된다.
x = 1
y = 2
print(f'x is {x}, y is {y}')
결과
x is 1, y is 2
f 뒤에 문자열(string)이 이어지기 때문에 f-string이라고 부른다. f-string임을 명시하는 문자 f는 대소문자에 관계 없으나, PEP 8은 소문자로 사용하는 것을 권고한다.
x = 1
y = 2
print(f'x is {x}, y is {y}')
print(F'x is {x}, y is {y}')
결과
x is 1, y is 2
x is 1, y is 2
이 내용은 PEP 498 - Literal String Interpolation에 명시되어 있다. Python 3.6 이상을 사용하고 있다면, f-string을 사용하는 것을 추천한다. 이전 세대의 포매팅 방식들보다 가독성과 자유도가 높고, 이와 비슷한 모양의 포매팅 방식이 다른 프로그래밍 언어에서도 꽤 사용되기 때문이다.
placeholder 표현식과 format
아직 class에 대한 내용을 다루지 않았기 때문에, 문자열의 format 메소드에 대해 메소드 대신 기능이라는 단어를 사용했다.
f-string이 없는 Python 3.6 미만 버전에서는, 문자열이 제공하는 format이라는 기능을 사용할 수 있다. 다음 순서대로 코드를 작성하면 된다.
- 문자열 내에 {}를 두어 placeholder를 표현한다.
- 문자열 뒤에 .format()을 붙이고, placeholder의 자리를 채울 값들을 소괄호 안에 순서대로 명시한다.
x = 1
y = 2
print('x is {}, y is {}'.format(x, y))
결과
x is 1, y is 2
'x is {}, y is {}'에 명시된 두 placeholder에 각각 x와 y에 해당하는 값이 대체되었다. format() 내에 명시한 순서대로 placeholder를 대체하는 것이다. 값이 placeholder의 개수보다 많이 전달되는 것은 괜찮지만, 부족하면 에러가 발생한다.
x = 1
y = 2
print('x is {}, y is {}'.format(x, y, 3, 4))
print('x is {}, y is {}'.format(x))
결과
x is 1, y is 2
Traceback (most recent call last):
File "example.py", line 5, in <module>
print('x is {}, y is {}'.format(x))
IndexError: Replacement index 1 out of range for positional args tuple
numbered placeholder
placeholder 표현식에는 {3}과 같이 순번을 명시할 수 있다. 이는 format() 내에 명시된 값들 중 해당 순서의 값을 placeholder에 사용하게 만든다.
순번은 0번부터 시작한다. {0}에는 format()에 첫 번째로 명시된 값을, {1}에는 두 번째로 명시된 값을 사용하는 식이다.
print('first value is {0}, second value is {1}'.format(1, 2))
결과
first value is 1, second value is 2
앞에서 먼저 설명했던, 순번이 설정되지 않은 placeholder 표현식({})에는 순서가 자동으로 부여된다. 0으로 시작해, 순서대로 1씩 증가하는 값이 사용된다. 예로 'x is {}, y is {}'라는 포맷 문자열은, 'x is {0}, y is {1}'과 동일한 의미다.
x = 1
y = 2
print('x is {}, y is {}'.format(x, y))
print('x is {0}, y is {1}'.format(x, y))
결과
x is 1, y is 2
x is 1, y is 2
순번을 순서대로 지정하지 않아도 되고, 동일한 순번을 가진 placeholder가 여러 번 등장해도 괜찮다.
x = 1
y = 2
print('x is {0}, y is {1}. x({0}) * y({1}) = {2}'.format(x, y, x * y))
결과
x is 1, y is 2. x(1) * y(2) = 2
named placeholder
numbered placeholder가 순번을 명시할 수 있었던 것처럼, named placeholder는 {x} 처럼 이름을 명시할 수 있다. 이 경우, placeholder에 사용된 이름에 맞게 format()에 값을 명시해 주어야 한다. 이름=값 형태로 작성하면 된다.
x = 1
y = 2
print('x is {a}, y is {b}'.format(a=x, b=y))
결과
x is 1, y is 2
조언
- Python 3.6 버전 이상을 사용하는 파이썬 프로젝트에선 f-string 외의 문자열 포매팅 방식이 선호되지 않는다. 따라서 f-string만 잘 사용하면 된다. f-string을 제외한 다른 방식들은, 코드를 해석할 줄만 알면 넘어가도 무관하다. 튜토리얼에서는 문자열 포매팅이 필요한 경우 f-string만 사용할 것이다.
연습문제
결과 예상하기 1
다음 코드의 실행 결과를 예상해보자.
a = 0
b = 1
print(f'{a + 1} {b - 1}')
print(a)
print(b)
결과 예상하기 2
다음 코드의 실행 결과를 예상해보자.
x = input()
print('input value is {}'.format(x))
입력
3
결과 예상하기 3
다음 코드의 실행 결과를 예상해보자.
a = 0
b = 1
print('{1} {0}'.format(a, b))
결과 예상하기 4
다음 코드의 실행 결과를 예상해보자.
print('{a} {abc} {a}'.format(a=1, b=3, abc=999))
Level 2
string interpolation
f-string처럼 문자열 내에 표현식을 내장(embed)시키는 것을 string interpolation이라고 한다. 각자 문법은 조금씩 다르지만, 많은 프로그래밍 언어들이 string interpolation을 지원한다. 다음은 JavaScript의 예다.
var x = 1;
var y = 2;
console.log(`x is ${x}, y is ${y}`);
결과
x is 1, y is 2
포매팅 후에 중괄호를 남기기
파이썬은 문자열을 포매팅해야할 때, 문자열 내에 중괄호가 등장하면 그 종류(여는 괄호, 닫는 괄호)에 관계 없이 placeholder로 해석하려고 한다. 따라서 다음과 같은 에러를 마주칠 때가 있다.
print(f'answer of 3*4} {3 * 4}')
결과
File "example.py", line 1
print(f"answer of 3*4} {3*4}")
^
SyntaxError: f-string: single '}' is not allowed
중괄호가 placeholder로 해석되지 않게 하려면, 동일한 종류의 중괄호를 한 번 더 붙여주면 된다.
print(f'answer of 3*4}} {3 * 4}')
결과
answer of 3*4} 12
f-string의 debug support
Python 3.8부터, f-string에 {name=}과 같은 표현식을 사용할 수 있게 되었다. {name=}은 name={name}와 동일한 효과를 갖는다.
x = 0
print(f'{x=}')
결과
x=0
등호 좌우의 공백은 포매팅 결과에 그대로 반영된다.
x = 0
print(f'{x =}')
print(f'{x= }')
print(f'{x = }')
결과
x =0
x= 0
x = 0
%
C언어의 printf, Go언어의 fmt.Printf 등에서는 %d, %s와 같이 % 기호에 타입을 나타내는 알파벳을 이어붙여 placeholder를 명시한다. 파이썬도 이러한 방식의 문자열 포매팅을 지원한다.
nickname = 'planb'
print('My nickname is %s.' % nickname)
결과
My nickname is planb.
format이나 f-string이 추가된 뒤로 이 방법은 선호되지 않아 'Level 1'에서 제외했다.
Level 3
placeholder에 속성 접근식 담기
placeholder 내에 표현식을 포함시킬 수 있다. 속성 접근, Mapping 참조, Indexing에 한정되지만 꽤 유용하다.
class Dummy:
x = 10
obj = Dummy()
print('x is {.x}'.format(obj))
print('x is {0.x}'.format(obj))
print('x is {obj.x}'.format(obj=obj))
print('value is {[value]}'.format({'value': 0}))
print('value is {data[value]}'.format(data={'value': 0}))
print('first item is {[0]}'.format(['a', 'b', 'c']))
print('first item is {data[0]}'.format(data=['a', 'b', 'c']))
결과
x is 10
x is 10
x is 10
value is 0
value is 0
first item is a
first item is a
Mapping을 참조하는 경우, key에 따옴표를 감싸지 않는 것에 주의해야 한다. {data["value"]}가 아니라 {data[value]}처럼 작성해야 한다.
format specifier
placeholder의 표현식 뒤에 콜론(:)을 붙여서, 표현식의 값이 표시되는 방식을 지정할 수 있다. 예를 들어,
- 15를 0015처럼, 값이 네자릿수가 될 때까지 앞에 0을 붙인다.
- 15를 +15처럼, 부호가 드러나게 한다.
- 0.6215를 62.2%처럼, 백분율로 표현하고 소수점 첫째 자리까지 반올림한다.
- 43을 2b처럼, 16진수로 변환한다.
다음 예제는 이 4가지를 표현한 것이다.
print(f'{15:0=4}')
print(f'{15:+}')
print(f'{0.6215:.1%}')
print(f'{43:x}')
결과
0015
+15
62.2%
2b
여기에 사용된 0=4, +f, .1%, x 부분이 format specifier다. 그러나, format specifier를 잘 모르는 상황에서는 그 모양만 보고 결과물을 예상하기는 어렵다. 문법이 다음처럼 매우 복잡하기 때문이다. 때문에 format specifier를 mini language라고 부르기도 한다.
[[fill]align][sign][#][0][minimumwidth][.precision][type]
맨 뒤에 있는 type으로 시작해, 앞으로 가면서 하나씩 알아보도록 하겠다.
[type]
type은 10진수 값을 2진수로 표현하는 등, 데이터 표시 방식을 설정하는 데에 사용할 수 있다. 다음은 정수 타입의 값을 대상으로 하는 specifier들을 나열한 예제다. 대화형 인터프리터 형태로 구성했다.
>>> f'{78:b}'
'1001110'
>>> f'{78:o}'
'116'
>>> f'{78:x}'
'4e'
>>> f'{78:X}'
'4E'
>>> f'{78:c}'
'N'
- b : 2진수로 변환
- o : 8진수로 변환
- x : 16진수로 변환, 소문자 사용
- X : 16진수로 변환, 대문자 사용
- c : 숫자에 해당하는 유니코드 문자로 변환
다음은 숫자 타입의 값을 대상으로, 실수 표현 방식을 변경하는 specifier들을 나열한 예제다. 대화형 인터프리터 형태로 구성했다.
>>> f'{0.00003:e}'
'3.000000e-05'
>>> f'{float("inf"):e}'
'inf'
>>> f'{float("nan"):e}'
'nan'
>>> f'{0.00003:E}'
'3.000000E-05'
>>> f'{float("inf"):E}'
'INF'
>>> f'{float("nan"):E}'
'NAN'
>>> f'{0.00003:f}'
'0.000030'
>>> f'{0.00003:F}'
'0.000030'
>>> f'{0.00003:g}'
'3e-05'
>>> f'{0.00003:G}'
'3E-05'
>>> f'{0.00003:%}'
'0.003000%'
- e : 지수 표기법으로 실수를 표현한다.
- E : e 타입처럼 지수 표기법으로 실수를 표현한다. 지수를 e 대신 E로, nan을 NAN, inf를 INF로 표현한다.
- f : 고정 소수점으로 실수를 표현한다.
- F : f 타입처럼 고정 소수점으로 실수를 표현한다. nan을 NAN, inf를 INF로 표현한다.
- g, G : 대상의 크기와 뒤에서 설명할 precision(정밀도)에 따라, e 타입과 f 타입 중 하나를 사용해 포매팅한다. precision을 다룬 뒤 재차 설명한다.
- % : 대상에 100을 곱한 뒤, f 타입과 동일한 포매팅을 수행하고 뒤에 percent sign(%)을 붙인다. 백분율 표현을 위해 사용한다.
이 specifier들은 실수 타입의 결과를 만든다. 따라서 정수를 대상으로 사용하는 경우, float 함수를 거쳐 타입 캐스팅한 뒤 포매팅을 적용된다.
print(f'{15:e}')
print(f'{15:f}')
print(f'{15:%}')
결과
1.500000e+01
15.000000
1500.000000%
다음은 숫자 타입의 값을 대상으로, thousands separator를 적용하는 specifier들을 나열한 예제다.
print(f'{78000:n}')
print(f'{78000:,}')
print(f'{78000:_}')
결과
78000
78,000
78_000
- n : 실행 환경의 locale 정보를 근거로 thousands separator를 적용한다(예제의 결과에는 드러나지 않음). 대상이 실수인 경우 그 전에 g type과 동일한 포매팅을 수행한다.
- , : , 기호를 thousands separator로 사용한다. Python 3.1부터 사용할 수 있다.
- _ : _ 기호를 thousands separator로 사용한다. Python 3.6부터 사용할 수 있다.
locale 모듈로 런타임에 locale 정보를 설정해, n type의 동작을 실험해볼 수 있다.
import locale
# locale 설정 전
print(f'{78000:n}')
# 숫자 포매팅에 대한 범주에 locale 설정
locale.setlocale(locale.LC_NUMERIC, 'ko_KR')
# locale 설정 후
print(f'{78000:n}')
결과
78000
78,000
문자열에 사용할 수 있는 s 타입이 있으나 무의미하다. 애초에 기본값이 s 타입이고, 그 외에 사용할 수 있는 것이 없기 때문이다.
print(f'{"abc":s}')
print(f'{"abc"}')
결과
abc
abc
[.precision]
precision은 문자열 또는 실수 타입을 대상으로 사용할 수 있다. 문자열에 대해, precision은 문자열의 최대 길이를 제한한다. 문자열 타입의 변수 s가 있을 때, f'{s:.2}'는 s[:2]와 동일한 의미다.
print(f'{"abc":.0}')
print(f'{"abc":.2}')
print(f'{"abc":.5}')
결과
ab
abc
실수에 대해, precision은 소수점 아래 자릿수를 제한한다. 반올림이 적용된다.
print(f'{0.113:.2}')
print(f'{0.113:.1}')
print(f'{0.17:.1}')
결과
0.11
0.1
0.2
다음은 지금까지 알아본 precision, type을 모두 사용한 예다.
print(f'{0.01394:.2%}')
결과
1.39%
[minimumwidth]
minimumwidth는 대상이 문자열 내에서 차지할 길이를 나타낸다. 다음은 'abc'라는 문자열이 길이 8만큼의 자리를 차지하게 만드는 포매팅의 예다. 자리를 차지하는 개념을 쉽게 알 수 있도록 앞뒤에 dot(.) 기호를 붙였다.
print(f'.{"abc":8}.')
결과
.abc .
문자열 'abc'는 minimumwidth에 따라 길이 8만큼의 자리를 차지해야 하므로, 본인의 길이를 제외한 5칸을 공백으로 채웠다. 숫자의 경우 문자열과 다르게 오른쪽으로 정렬된다.
print(f'.{0.01:8}.')
결과
. 0.01.
문자열과 숫자의 기본 정렬 방향이 다른 이유는 뒤에서 알아볼 [[fill]align]을 통해 알 수 있다. 여기서 간단하게 먼저 이야기하면, 문자열은 기본적으로 좌측 정렬이, 숫자는 기본적으로 우측 정렬이 적용된다.
다음은 지금까지 알아본 minimumwidth, precision, type을 모두 사용한 예다.
print(f'{0.01394:8.2%}')
결과
1.39%
[0]
숫자 타입의 값에 minimumwidth가 사용되는 경우, minimumwidth의 앞에 0을 붙여주면 zero padding을 할 수 있다.
print(f'{15:08}')
결과
00000015
이는 뒤에서 설명할 [[fill]align]과 어느 정도 비슷하다. [[fill]align]은 문자열이 차지할 width 내에서의 정렬 방향(align)과 빈 자리를 채울 값(fill)을 나타낸다. 다음은 {15:08}을 [[fill]align]으로 표현한 간단한 예다.
print(f'{15:0>8}')
결과
00000015
[[fill]align]과 다르게 [0]은 부호를 인식해 자연스럽게 zero padding을 수행한다.
print(f'{-15:0>8}')
print(f'{-15:08}')
결과
00000-15
-0000015
다음은 지금까지 알아본 0, minimumwidth, precision, type을 모두 사용한 예다.
print(f'{-0.01394:08.2%}')
결과
-001.39%
[#]
정수 타입의 값을 대상으로 사용할 수 있는 #은 type이 b, o, x일 때 문자열의 앞에 각각 0b, 0o, 0x를 붙이는 역할을 한다.
print(f'{123:#b}')
print(f'{123:#o}')
print(f'{123:#x}')
결과
0b1111011
0o173
0x7b
다음은 지금까지 알아본 #, 0, minimumwidth, type을 사용한 예다. #은 정수 타입에 대해서만 사용할 수 있으므로, precision과는 혼용이 불가능해 제외했다.
print(f'{12:#08x}')
결과
0x00000c
[sign]
sign은 대상이 숫자 타입인 경우에만 사용할 수 있고, 유효한 값으로 +, -, (공백)이 있다. 기본값은 -다.
print(f'{15:+}')
print(f'{-15:+}')
print(f'{15:-}')
print(f'{-15:-}')
print(f'{15: }')
print(f'{-15: }')
결과
+15
-15
15
-15
15
-15
- + : 대상의 부호를 항상 표시
- - : 대상이 음수일 때만 부호 표시
- (공백) : 양수일 때 한 칸 공백을 삽입하고, 음수일 때 부호 표시
다음은 지금까지 알아본 sign, 0, minimumwidth, precision, type을 모두 사용한 예다. 타입 관련(#과 precision을 함께 사용할 수 없는) 문제로 #을 제외했다.
print(f'{0.01394:+08.2%}')
결과
+001.39%
[[fill]align]
format specifier의 가장 앞에는 [fill]align이 포함될 수 있다. align은 표현식이 차지해야 할 자리 내에서, 값이 문자열의 어느 방향으로 정렬되어야 하는지를 기호로 나타낸다.
align에는 사용할 수 있는 값이 <, >, ^, =로 정해져 있다. 각각의 의미는 예제를 통해 알아보도록 하자. 예제의 format specifier 마지막에 명시된 8은 minimumwidth로 해석된다. align이 되는 것을 확인하려면 대상이 충분한 공간을 차지해야 하기 때문에 추가했다.
print(f'.{"abc":<8}.')
print(f'.{"abc":>8}.')
print(f'.{"abc":^8}.')
print(f'.{15:=8}.')
결과
.abc .
. abc.
. abc .
. 15.
< 기호는 왼쪽으로 붙이도록, > 기호는 오른쪽으로 붙이도록, ^ 기호는 가운데로 정렬하도록, = 기호는 숫자 타입의 대상에 한해 오른쪽으로 붙이도록 만든다.
fill은 빈 자리를 어느 값으로 채울지를 나타낸다. [[fill]align]이라는 문법에 맞게, 단독으로 사용될 수 없으며 항상 align을 후행해야 한다.
print(f'{"abc":x<8}')
print(f'{"abc":x>8}')
print(f'{"abc":x^8}')
print(f'{15:x=8}')
결과
abcxxxxx
xxxxxabc
xxabcxxx
xxxxxx15
align의 기본값은 대상이 숫자 타입일 때 >, 그 외일 때 <가 사용된다. 따라서 숫자 타입을 대상으로 :8과 :>8은 동일한 의미이며, 그 외의 타입을 대상으로 :8과 :<8은 동일한 의미다.
print(f'.{15:8}.')
print(f'.{15:>8}.')
print(f'.{"abc":8}.')
print(f'.{"abc":<8}.')
결과
. 15.
. 15.
.abc .
.abc .