터미널로 입력된 문자열의 앞에, 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을 넘긴 버전은 되도록 사용하지 않는 것이 좋다.
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을 사용하는 것을 추천한다. 이전 세대의 포매팅 방식들보다 가독성과 자유도가 높고, 이와 비슷한 모양의 포매팅 방식이 다른 프로그래밍 언어에서도 꽤 사용되기 때문이다.
아직 class에 대한 내용을 다루지 않았기 때문에, 문자열의 format 메소드에 대해 메소드 대신 기능이라는 단어를 사용했다.
f-string이 없는 Python 3.6 미만 버전에서는, 문자열이 제공하는 format이라는 기능을 사용할 수 있다. 다음 순서대로 코드를 작성하면 된다.
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
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
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
다음 코드의 실행 결과를 예상해보자.
a = 0
b = 1
print(f'{a + 1} {b - 1}')
print(a)
print(b)
다음 코드의 실행 결과를 예상해보자.
x = input()
print('input value is {}'.format(x))
입력
3
다음 코드의 실행 결과를 예상해보자.
a = 0
b = 1
print('{1} {0}'.format(a, b))
다음 코드의 실행 결과를 예상해보자.
print('{a} {abc} {a}'.format(a=1, b=3, abc=999))
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
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'에서 제외했다.
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]}처럼 작성해야 한다.
placeholder의 표현식 뒤에 콜론(:)을 붙여서, 표현식의 값이 표시되는 방식을 지정할 수 있다. 예를 들어,
다음 예제는 이 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은 10진수 값을 2진수로 표현하는 등, 데이터 표시 방식을 설정하는 데에 사용할 수 있다. 다음은 정수 타입의 값을 대상으로 하는 specifier들을 나열한 예제다. 대화형 인터프리터 형태로 구성했다.
>>> f'{78:b}'
'1001110'
>>> f'{78:o}'
'116'
>>> f'{78:x}'
'4e'
>>> f'{78:X}'
'4E'
>>> f'{78:c}'
'N'
다음은 숫자 타입의 값을 대상으로, 실수 표현 방식을 변경하는 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%'
이 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
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은 문자열의 최대 길이를 제한한다. 문자열 타입의 변수 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는 대상이 문자열 내에서 차지할 길이를 나타낸다. 다음은 '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%
숫자 타입의 값에 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은 대상이 숫자 타입인 경우에만 사용할 수 있고, 유효한 값으로 +, -, ** (공백)이 있다. 기본값은 -**다.
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%
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 .