【str.format】Python文字列フォーマットどれ使えばいいの?【f-strings】

2019-11-05

少し前に業務委託先で「%フォーマット」「str.format()」「f-strings」どれつかってるか?みたいな議論がありました。 表題には書きませんでしたが、%フォーマットもまだ出番はあります。

この記事では自分なりのそれぞれの使い分けのポイントみたいなものを書いてみようと思います。

Slackに書き残した自分用メモが見れなくなったので新規で書いてます。 なんか抜けてるような気がしてるけどきっと気のせい。

string.Template?なんですかそれは?

目次

あまり変なことは書いてないと思いますが、異論は認めます。 プロジェクトで明確な基準がある場合はそちらに従ってください。

logging を使うとき

%フォーマットを使う

logging 機構はstr.format よりも先に生まれました。 新しいフォーマットはサポートされているよう ですが、%フォーマットで行うのが慣例になっているので、特別な事情がない限りは%フォーマットで書くようにしています。

import logging
logging.warning('%s before you %s', 'Look', 'leap!')

なお、この例のように文字列はその場で組立てずにパラメータ(この例では lookleap!)を渡して loggingに組み立てさせるべきです。

パラメータは後からフィルタとして使ったりできます。 ちなみに、Sentryでは組立前の文字列でグルーピングされるようになっていて、エラー通知も減らせます。

このような理由から f-strings などで先に組み立ててしまうのは良くない習慣です。

pyformatで生SQLを発行するとき

%フォーマットを使う

PEP249でSQLのパラメータ指定方法が定められています。

そのうち、 pyformat と呼ばれるフォーマットは %フォーマットでプレースホルダを指定することになっており、 主要なDB Driver(API) ではこれをサポートしています。

以下は Basic module usage — Psycopg 2.8.4.dev0 documentation から持ってきたサンプルです。

>>> cur.execute("""
...     INSERT INTO some_table (an_int, a_date, another_date, a_string)
...     VALUES (%(int)s, %(date)s, %(date)s, %(str)s);
...     """,
...     {'int': 10, 'str': "O'Reilly", 'date': datetime.date(2005, 11, 18)})
>>> cur.execute("SELECT (%s %% 2) = 0 AS even", (10,))  # mod は %% でエスケープ

Python2.6未満を利用する

%フォーマットを使う

書いておきながらこれはないだろうと思ってますが、Python2.6未満では str.format すらないので %フォーマットを使わざるを得ません。

Python3.6未満を利用する可能性がある

str.formatを使う

古いバージョン(3.6未満)のPythonで実行される可能性がある場合 f-strings 構文はシンタックスエラーになってしまうので、用いません。

具体的にはライブラリ提供者はこのことを考慮する必要があるでしょう。

str.format() は Python2.6からサポートされたので、大抵の場合は気にせず使えます。

文字列を遅延評価したい (lazy evaluation)

str.formatを使う

f-strings 構文はその場で文字列を組み立ててしまうため、 ガワだけを作って後から値を組み込みたいケースには適しません。

この場合は str.format() を使うのが適しています。

>>> def render(template):
...     context = {'a': 1, 'b': 2}
...     return template.format(**context)

>>> render('a: {a}, b: {b}')
'a: 1, b: 2'

複雑な制御構文を用いて文字列を組み立てたいのであれば Jinja2 などのテンプレートエンジンを用いるべきでしょう。

str.format()を使うことで簡潔に書ける

str.formatを使う

f-strings よりも str.format のほうが短くなるケースがいくつかあります。

それぞれの「変数を参照するか」「引数を参照するか」という性質の違いがこのような差異を生みます。

変数名が長い (long name variable)

例えばこんなクラスとインスタンスがあったとします。

class Test:
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

testdayo = Test(1, 2, 3)

これを使って文字列を組み立てるとき、 str.format() と f-strings の長さを比べてみましょう。

str.format f-strings
'{t.a}{t.b}{t.c}'.format(t=testdayo)
f'{testdayo.a}{testdayo.b}{testdayo.c}'

このくらいだとあまり違いは出ませんが、変数名がもっと長くなったり 参照する値が増えると、 str.format() のほうが短くなることは割とあります。

リストや辞書を展開する (unpacking)

str.format はメソッドなので、辞書やリストを引数としてアンパッキングして参照できます。

このような変数があったとして

record = {
    'age': 150,
    'firstname': 'teruhiko',
    'lastname': 'teruya',
}

l = [1, 2, 3, 4, 5]

それぞれを比較してみると、 str.format() のほうが簡潔に見えるような気がしませんか?

str.format f-strings
'{firstname} {lastname} ({age})'.format(**record)
'{:04}, {:04}, {:04}, {:04}, {:04}'.format(*l)
f'{record["firstname"]} {record["lastname"]} ({record["age"]})'
f'{l[0]:04}, {l[1]:04}, {l[2]:04}, {l[3]:04}, {l[4]:04}'

それ以外 (the others)

f-stringsを使う

f-strings は JavaScript でいうテンプレートリテラルに位置する構文です。

f から始まる文字列の中に書いた {} 内で変数を参照できます。

大抵の文字列フォーマットはこれで事足りますしシンプルに書けます。 これまでの条件に該当しなければ f-strings を用いるべきと考えています。

ちなみにパフォーマンスの観点で見ると、基本的には str.format() よりも f-strings のほうが多少速いようです。 (listの展開はなぜかformatが速い

とはいえ、そこまでコストの高い処理ではないので 基本的にきれいにかける方を優先してよいと思います。

str.format f-strings
>>> stmt_format1 = """
... class Test:
...     def __init__(self, a, b, c):
...         self.a = a
...         self.b = b
...         self.c = c
...
... testdayo = Test(1, 2, 3)
... '{t.a}{t.b}{t.c}'.format(t=testdayo)
... """
>>> import timeit
>>> timeit.timeit(stmt=stmt_format1, number=1000000)
17.183120172
>>> stmt_fstrings1 = """
... class Test:
...     def __init__(self, a, b, c):
...         self.a = a
...         self.b = b
...         self.c = c
...
... testdayo = Test(1, 2, 3)
... f'{testdayo.a}{testdayo.b}{testdayo.c}'
... """
>>> import timeit
>>> timeit.timeit(stmt=stmt_fstrings1, number=1000000)
14.045747837999997
>>> stmt_format2 = """
... record = {
...     'age': 150,
...     'firstname': 'teruhiko',
...     'lastname': 'teruya',
... }
... '{firstname} {lastname} ({age})'.format(**record)
... """
>>> import timeit
>>> timeit.timeit(stmt=stmt_format2, number=1000000)
1.4482771650000004
>>> stmt_fstrings2 = """
... record = {
...     'age': 150,
...     'firstname': 'teruhiko',
...     'lastname': 'teruya',
... }
... f'{record["firstname"]} {record["lastname"]} ({record["age"]})'
... """
>>> import timeit
>>> timeit.timeit(stmt=stmt_fstrings2, number=1000000)
0.5433545200000012
>>> stmt_format3 = """
... l = [1, 2, 3, 4, 5]
... '{:04}, {:04}, {:04}, {:04}, {:04}'.format(*l)
... """
>>> import timeit
>>> timeit.timeit(stmt=stmt_format3, number=1000000)
2.146872690000002
>>> stmt_fstrings3 = """
... l = [1, 2, 3, 4, 5]
... f'{l[0]:04}, {l[1]:04}, {l[2]:04}, {l[3]:04}, {l[4]:04}'
... """
>>> import timeit
>>> timeit.timeit(stmt=stmt_fstrings3, number=1000000)
2.501091061000004

なお、 f-strings にしても、 str.format にしても文字列が参照する値は予め求めておくほうがお行儀が良いです。

文字列の中に関数が入っていたりすると読みづらく、問題発生時の切り分け難易度があがります。

BAD
print(f'num: {q.count(name=param["name"], date_from=param["date_from"])} records')
GOOD
count = q.count(name=param["name"], date_from=param["date_from"])
print(f'num: {count} records')