web-dev-qa-db-ja.com

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を使用すると、ほぼ半分の時間がかかります。必要な構文が少ないため、リテラルメソッドの方が高速になると思います。リテラルメソッドが非常に遅くなる原因となっている舞台裏で何が起こっていますか?

33

:この回答は、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 (この質問への回答として開かれた)で、パフォーマンスをさらに改善する方法についての議論フォーマットされた文字列リテラル。

30
Martijn Pieters

3.6ベータ1以前は、フォーマット文字列f'x is {x}'''.join(['x is ', x.__format__('')])と同等にコンパイルされていました。結果のコードは、いくつかの理由で非効率でした。

  1. 文字列フラグメントのシーケンスを構築しました...
  2. ...そしてこのシーケンスはタプルではなくリストでした! (リストよりタプルを作成する方がわずかに高速です)。
  3. 空の文字列をスタックにプッシュしました
  4. 空の文字列でjoinメソッドを検索しました
  5. これは、__format__('')が常にselfを返す裸のUnicodeオブジェクト、または引数として__format__('')str(self)を返す整数オブジェクトに対して__format__を呼び出しました。
  6. __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_STRINGFORMAT_VALUEコードの最適化により、非効率の6つの原因のうち最初の5つが完全に排除されます。 __format__メソッドはまだスロット化されていないため、クラスでの辞書ルックアップが必要であるため、呼び出しは__str__を呼び出すよりも必然的に遅くなりますが、一般的なケースでは呼び出しを完全に回避できるようになりましたフォーマット指定子なしのintまたはstrインスタンス(サブクラスではない!)のフォーマット.

21
Antti Haapala