Python 3.6 alphaでリテラル形式の文字列(f-strings)が遅いのはなぜですか?(3.6で修正済み)
Python 3.6 alphaビルドをPython Githubリポジトリからダウンロードしました。私のお気に入りの新機能の1つは、文字列フォーマットです。それは次のように使用できます。そう:
>>> x = 2
>>> f"x is {x}"
"x is 2"
これは、format
インスタンスでstr
関数を使用するのと同じことを行うようです。ただし、私が気づいたことの1つは、このリテラル文字列の書式設定は、実際にformat
を呼び出す場合に比べて非常に遅いことです。各メソッドについてtimeit
は次のように言っています。
>>> x = 2
>>> timeit.timeit(lambda: f"X is {x}")
0.8658502227130764
>>> timeit.timeit(lambda: "X is {}".format(x))
0.5500578542015617
文字列をtimeit
の引数として使用すると、私の結果はまだパターンを示しています:
>>> timeit.timeit('x = 2; f"X is {x}"')
0.5786435347381484
>>> timeit.timeit('x = 2; "X is {}".format(x)')
0.4145195760771685
ご覧のとおり、format
を使用すると、ほぼ半分の時間がかかります。必要な構文が少ないため、リテラルメソッドの方が高速になると思います。リテラルメソッドが非常に遅くなる原因となっている舞台裏で何が起こっていますか?
注:この回答は、Python 3.6 alphaリリース用に書かれました。A .6に追加された新しいオペコード.0b1 f-stringのパフォーマンスが大幅に向上しました。
_f"..."
_構文は、_{...}
_式の周りのリテラル文字列部分のstr.join()
操作に効果的に変換され、object.__format__()
メソッドを介して渡された式自体の結果(_:..
_フォーマット指定を渡します)。分解するとこれがわかります。
_>>> import dis
>>> dis.dis(compile('f"X is {x}"', '', 'exec'))
1 0 LOAD_CONST 0 ('')
3 LOAD_ATTR 0 (join)
6 LOAD_CONST 1 ('X is ')
9 LOAD_NAME 1 (x)
12 FORMAT_VALUE 0
15 BUILD_LIST 2
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 POP_TOP
22 LOAD_CONST 2 (None)
25 RETURN_VALUE
>>> dis.dis(compile('"X is {}".format(x)', '', 'exec'))
1 0 LOAD_CONST 0 ('X is {}')
3 LOAD_ATTR 0 (format)
6 LOAD_NAME 1 (x)
9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
12 POP_TOP
13 LOAD_CONST 1 (None)
16 RETURN_VALUE
_
その結果の_BUILD_LIST
_およびLOAD_ATTR .. (join)
オペコードに注意してください。新しい_FORMAT_VALUE
_は、スタックの最上位とフォーマット値(コンパイル時に解析される)を取り、これらをobject.__format__()
呼び出しで結合します。
したがって、_f"X is {x}"
_の例は次のように翻訳されます。
_''.join(["X is ", x.__format__('')])
_
リストオブジェクトを作成し、str.join()
メソッドを呼び出すにはPythonが必要です。
str.format()
呼び出しもメソッド呼び出しであり、解析後もx.__format__('')
への呼び出しが関係していますが、決定的に、リストの作成はありませんここに含まれます。 str.format()
メソッドを高速化するのはこの違いです。
Python 3.6はアルファビルドとしてのみリリースされていることに注意してください。この実装はまだ簡単に変更できます。 PEP 494 –Python 3.6リリーススケジュールを参照) タイムテーブル、および Pythonの問題#27078 (この質問への回答として開かれた)で、パフォーマンスをさらに改善する方法についての議論フォーマットされた文字列リテラル。
3.6ベータ1以前は、フォーマット文字列f'x is {x}'
は''.join(['x is ', x.__format__('')])
と同等にコンパイルされていました。結果のコードは、いくつかの理由で非効率でした。
- 文字列フラグメントのシーケンスを構築しました...
- ...そしてこのシーケンスはタプルではなくリストでした! (リストよりタプルを作成する方がわずかに高速です)。
- 空の文字列をスタックにプッシュしました
- 空の文字列で
join
メソッドを検索しました - これは、
__format__('')
が常にself
を返す裸のUnicodeオブジェクト、または引数として__format__('')
がstr(self)
を返す整数オブジェクトに対して__format__
を呼び出しました。 __format__
メソッドにはスロットがありません。
ただし、より複雑で長い文字列の場合、リテラル形式の文字列は対応する'...'.format(...)
呼び出しよりも高速でした。後者の場合、文字列は形式設定されるたびに解釈されるためです。
この質問自体が issue 27078 の主な動機でしたPythonバイトコードオペコードを文字列に変換します(オペコードは1つのオペランド-フラグメントの数を取得しますスタック上。フラグメントは逆の順序でプッシュされます。つまり、最後の部分が最上位のアイテムです。SerhiyStorchakaがこの新しいオペコードを実装し、C = Pythonにマージして、Python =ベータ1バージョン以降3.6(つまりPython 3.6.0 final)。
結果として、リテラル形式の文字列はstring.format
よりもmuch高速になります。また、単にstr
を補間している場合は、Python 3.6の古いスタイルのフォーマットよりも高速であることがよくあります。またはint
オブジェクト:
>>> timeit.timeit("x = 2; 'X is {}'.format(x)")
0.32464265200542286
>>> timeit.timeit("x = 2; 'X is %s' % x")
0.2260766440012958
>>> timeit.timeit("x = 2; f'X is {x}'")
0.14437875000294298
f'X is {x}'
は次のようにコンパイルされます
>>> dis.dis("f'X is {x}'")
1 0 LOAD_CONST 0 ('X is ')
2 LOAD_NAME 0 (x)
4 FORMAT_VALUE 0
6 BUILD_STRING 2
8 RETURN_VALUE
新しいBUILD_STRING
とFORMAT_VALUE
コードの最適化により、非効率の6つの原因のうち最初の5つが完全に排除されます。 __format__
メソッドはまだスロット化されていないため、クラスでの辞書ルックアップが必要であるため、呼び出しは__str__
を呼び出すよりも必然的に遅くなりますが、一般的なケースでは呼び出しを完全に回避できるようになりましたフォーマット指定子なしのint
またはstr
インスタンス(サブクラスではない!)のフォーマット.