파이썬 f-string formatting은 무적일까?

김기욱·2021년 1월 14일
3
post-thumbnail

무적/불사의 상징 아킬레우스

f-string은 파이썬 3.6버전 이후에 등장한 '비교적(?) 최신의 문자열 포매팅기법'입니다. 포매팅 방식 중에서는 누가뭐래도 가장 Pythonicformatting 방식이기도 합니다. 당연히 기존 문법과 다른 여러 장점들을 가지고 있습니다. 하지만, 문득 이런 생각이 들곤 합니다.

"포매팅을 써야할 상황이 되면 무조건 f-string을 쓰는 것이 좋을까?"
"최신의 기법이 가장 완벽한 대안일까?"

최신의 기법이나 제품이 항상 옳은 답이 아니라는건 여러 사례를 통해 증명되어왔습니다. 일례로 마이크로소프트사의 희대의 망작(?) '윈도우 ME'가 있겠네요. 이제는 2000년대 이후의 학생들은 생소할 이 망작은 윈도우 98과 XP사이에 나왔다가 정말 빠르게 소리소문 없이 사라진 희대의 윈도우 시리즈였습니다.


이제는 추억이되버린..

메모리 누수가 너무 심각해서 실행한지 일정 시간이 지나면 잦은 버퍼현상이 무조건적으로 발생해 재부팅을 필수로 해야했던 운영체제였죠. 버전업이 됬는데 전작보다 램 용량은 더 줄어서 출시한 갤럭시 노트20, 이거는 너무 많아 일일히 사례들기도 힘든 전작만도 못한 영화속편 등... 항상 최신의 기법의 최선의 결과만 가져오는것은 '아니다'라는건 우리는 이미 알고있습니다.

세 가지 문자열 포매팅 🤟


사..삼도류?

파이썬은 세 가지 문자열 포매팅 방식을 지원합니다.

파이썬이 처음 릴리즈 될 때부터 지원되었던 c언어와 유사한 % operator 방식
파이썬3에서 처음 등장했던 str.format 방식
그리고 가장 최근(3.6버전 이후)에 등장한 f-string 방식입니다.

간단한 예제코드와 함께 어떻게 사용될 수 있는지 살펴 보도록 합시다.

name = 'Tom'
age = 30

ex1 = 'Hi! my name is %s and I am %i' % (name, age) #%방식
ex2 = 'Hi! my name is {} and I am {}'.format(name, age)  #str.format
ex3 = f'Hi! my name is {name} and I am {age}' #f-string

print(ex1)
print(ex2)
print(ex3)

# 세 가지 모두 결과 동일
# Hi! my name is Tom and I am 30

%방식은 어떤 type인지 정확히 명시해줘야 합니다.
스트링이면 %s 정수형이면 %i 소수점도 포함된 실수형태면 %f 으로요.

str.format 따로 타입을 명시해주지 않아도 포매팅에 반영을 해줍니다. 하지만 두 방식보다는 f하나만 넣어주고 중괄호를 표시한 자리에 직접 변수를 때려박아 표현하는 f-string이 훨씬 직관적이고 깔끔해보입니다.

특히 이런식으로 독스트링(triple-quotes)을 쓸만큼 포매팅할 문자열의 길이가 길어질수록 f-string의 직관성이 훨씬 돋보입니다.

f'''
{f_name}은 파이썬 3.6버전 이후에 등장한 '비교적(?) 최신의 문자열 {kind}기법'입니다. 
{kind} 방식 중에서는 누가뭐래도 가장 {expression}{kind} 방식이기도 합니다.

당연히 기존 문법과 다른 여러 장점들을 가지고 있습니다. 하지만, 문득 이런 생각이 들곤 합니다.

"{kind}을 써야할 상황이 되면 무조건 {f_name}을 쓰는 것이 좋을까?"
"최신의 기법이 가장 완벽한 대안일까?"
'''

변수명이 무슨 의미였는지 한 번 맞춰보세요...

f-string의 또다른 장점? 🐒


f-string이 최고야..
단순히 직관적이다라는 장점 외에도 f-string은 다른 문자열 포맷팅에 없는 장점들이 존재합니다.

첫째. 정수끼리 산술 연산을 지원합니다.

a = 10
b = 20
sum_result = f'sum result is {a+b}'
print(sum_result)

# 결과
sum result is 30

둘째. f-string선언 후 변수를 나중에 선언하는 형식도 지원합니다.

greeting = f'Hi! I am {name}'
name = 'Peter'
print(greeting)

# 결과
Hi! I am Peter

셋째. 성능적인 이점이 존재합니다.
timeit 모듈을 활용해 실행시간을 측정해보겠습니다.

import timeit

a = timeit.timeit("""name="Tom"
age = 74
'%s is %iyears old' % (name, age) """, number=100000)
b = timeit.timeit("""name="Tom"
age = 74
'{} is {}years old'.format(name, age) """, number=100000)
c = timeit.timeit("""name="Tom"
age = 74
'{name} is {age}years old'""", number=100000)


print(f"operator방식의 소요시간 : {round(a,4)}, str.format의 소요시간 : {round(b,4)}, f-string의 소요시간 : {round(c,4)}")

# 결과
operator방식의 소요시간 : 0.0243, str.format의 소요시간 : 0.0383, f-string의 소요시간 : 0.0015

조금이 아니라 생각보다 꽤 많이 차이가 납니다.f-string은 다른 포매팅 방식들과 달리 상수와 함수를 사용하는것이 아닌 런타임 단계에서 사용되는 표현식이기 때문입니다.

아니 그럼 가시성도 좋고, 성능도 좋고, 더 많은 기능을 지원하는 f-string 그냥 쓰면 되는거 아니야? 라는 질문이 있을텐데, 아닙니다. f-string 역시 단점이 존재합니다.

f-string의 단점 🙊

아킬레우스도 발 뒷꿈치(아킬레우스건)이라는 약점이 존재하듯, f-string 역시 약점이 존재하는 포매팅방식입니다.

첫째. 변수명이 길고 문자열이 짧은 경우

샘플의 경우에는 변수명이 name이나 age같이 무척 짧았습니다.
하지만 반대로 변수명이 길고 문자열이 짧다면 어떨까요?

#str.format
'{}>{}>{}'.format(
    diet_info_list['experiment_result'].get('1day_weight_loss'),
    diet_info_list['experiment_result'].get('3day_weight_loss'),
    diet_info_list['experiment_result'].get('total_weight_loss')
    )

#f-string
f'{diet_info_list['experiment_result'].get('1day_weight_loss')}>{diet_info_list['experiment_result'].get('3day_weight_loss')}>{diet_info_list['experiment_result'].get('total_weight_loss')}'

자료구조가 복잡한 객체의 경우 데이터를 가공할 때 뜻하지 않게 변수명이 무척 길어지는 경우가 발생합니다. 이런 경우 라인 관리가 힘든 f-string의 경우 가독성이 오히려 떨어져 보입니다.

가독성을 위해 독스트링을 써서 문자열을 한 칸씩 내리자니 출력값이 달라져버리죠. 결론적으로 이렇게 변수명이 길어질 때는 str.format을 쓰는게 낫습니다.

두번째. 중괄호( {} )가 중첩되어 있는 문장
예를 들어 다음과 같이 다수의 중괄호가 중복/중첩되어 있는 문장이 있고, 이를 포매팅 해야된다고 생각합시다.

"""{'filters': [
{'filter': {
  'value': {
    'type': 'exact', 'value': 'invalid'}, 
    'operator': 'enum_is'}, 
    'property': 'seq3'}, 
{'filter': {
  'operator': 'enum_is', 
  'value': {'type': 'exact', 'value': {이곳을 포매팅해야됨}}},
  'property': 'h345'}
], 

'operator': 'and'}"""

거두절미하고 결론만 말하겠습니다.

이런 문장에서는 f-string str.format 둘 다 안됩니다. 억지로 하려고하면 방법이야 있지만 복잡하고 무척이나 번거롭습니다. 이런 문장에서 유일하게 가능한 방법은 가장 원시적인 방식인 % formatting입니다.

# 유일한 해결법
value_name = 'valid'

"""{'filters': [
{'filter': {
  'value': {
    'type': 'exact', 'value': 'invalid'}, 
    'operator': 'enum_is'}, 
    'property': 'seq3'}, 
{'filter': {
  'operator': 'enum_is', 
  'value': {'type': 'exact', 'value': %s}},
  'property': 'h345'}
], 

'operator': 'and'}""" % (value_name)

결론 🐵

profile
어려운 것은 없다, 다만 아직 익숙치않을뿐이다.

2개의 댓글

comment-user-thumbnail
2022년 2월 28일

왁.. 제가 얼마 전에 시작한 고민을 이 글 하나로 끝냈습니다..!! 깔끔한 내용정리 감사합니다!!!

답글 달기
comment-user-thumbnail
2023년 11월 30일

글이 많은 도움이 되었습니다. 감사합니다.

답글 달기