[* a]が割り当て超過になる原因は何ですか?


136

どうやらlist(a)[x for x in a]オーバーローケートしない、ある時点でオーバーローケートする、常に[*a]オーバーローケートするのでしょうか。

n = 100までのサイズ

以下は、0から12までのサイズnと、3つのメソッドの結果のバイト単位のサイズです。

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

このように計算され、repl.itで再現可能パイソン3.使用して、8

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

だから:これはどのように機能しますか?どのように[*a]割り当てますか?実際、与えられた入力から結果リストを作成するためにどのようなメカニズムを使用していますか?それはイテレータをa使い、次のようなものを使いますかlist.append?ソースコードはどこにありますか?

(画像を生成したデータとコードとのコラボレーション。)

より小さいnにズームイン:

n = 40までのサイズ

より大きいnにズームアウトする:

n = 1000までのサイズ


1
Fwiw、テストケースを拡張すると、リスト内包表記はループを記述して各項目をリストに追加[*a]するように動作extendし、空のリストを使用するように動作するように見えます。
jdehesa

4
それぞれについて生成されたバイトコードを確認すると役立つ場合があります。list(a)完全にCで動作します。反復するときに、内部バッファをノードごとに割り当てることができますa。たくさん[x for x in a]使用LIST_APPENDするだけなので、通常のリストの「少し過剰に割り当て、必要に応じて再割り当て」という通常のパターンに従います。[*a]を使用BUILD_LIST_UNPACKしています。これはどうやったかわからないのですが、どうやら常にオーバーアロケーションしているようです。:)
chepner

2
また、Python 3.7では、list(a)[*a]は同一であり、どちらもと比較して全体的に割り当てられている[x for x in a]ようです。そのため、sys.getsizeofここでは適切なツールではない可能性があります。
chepner

7
@chepner私sys.getsizeofは正しいツールだと思います、それは単にlist(a)全体的な割り当てに使用されたことを示しています。実際、Python 3.8の新機能では、「リストコンストラクターは割り当てられていない[...]」と述べています。
Stefan Pochmann

5
@chepner:3.8で修正されたバグです。コンストラクタは割り当てを行うことを想定していません。
ShadowRanger

回答:


81

[*a] 内部でCと同等の処理を実行しています:

  1. 新しく空にする list
  2. コール newlist.extend(a)
  3. を返しますlist

したがって、テストを次のように拡張すると、

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

オンラインでお試しください!

あなたはのために結果が表示されますgetsizeof([*a])l = []; l.extend(a); getsizeof(l)同じです。

これは通常正しいことです。extend通常、後で追加することを期待している場合、および一般的なアンパックの場合も同様に、複数のものが次々に追加されると想定されています。[*a]通常のケースではありません。Pythonは、list[*a, b, c, *d])に追加される複数のアイテムまたはイテラブルがあると想定しているため、割り当て超過により、一般的なケースで作業が節約されます。

対照的にlist、事前にサイズ設定された単一の反復可能オブジェクト(を使用list())から構築されたものは、使用中に拡大または縮小することはできません。Pythonは最近、既知のサイズの入力に対してもコンストラクターを割り当ててしまうバグを修正しました

用としてlist内包、彼らは繰り返しに効果的に同等だappend時に要素を追加するときは、通常の割り当て超過の成長パターンの最終結果を見ているので、S。

明確にするために、これは言語を保証するものではありません。CPythonがそれを実装する方法です。Python言語仕様は、一般的に、特定の成長パターンと無関心されるlist(保証償却別にO(1) appendS及びpop端からS)。コメントで述べたように、特定の実装は3.9で再び変更されました。それは影響しません一方で[*a]、それはするために使用されるどのような他の例影響を与える可能性があり、「一時的な構築tuple個々の項目のをし、その後extendtuple、」今の複数のアプリケーションになっLIST_APPEND割り当て超過が発生し、どのような数字は計算に入ったときに変更することができ、。


4
@StefanPochmann:私は以前にコードを読んだことがあります(これが私がすでにこれを知っている理由です)。これはのバイトコードハンドラーでありBUILD_LIST_UNPACK_PyList_ExtendCと同等の呼び出しとして使用しますextend(メソッドルックアップではなく直接)。彼らはそれtupleをアンパック付きのビルドのパスと組み合わせました。tuplesは断片的な構築にうまく割り当てられないため、list(割り当て超過のメリットを得るために)常にアンパックし、tuple要求されたときに最後に変換します。
ShadowRanger

4
これは3.9明らかに変更されていることに注意してください。単一バイトコード命令で全体を構築する前にスタックにすべてをロードする代わりにBUILD_LIST、個別のバイトコード(LIST_EXTENDアンパックLIST_APPENDするものごと、単一のアイテム)で構築が行われますlist(これにより、コンパイラは実装のような、オールインワンの命令が許可しなかったことを最適化を実行する[*a, b, *c]ようにLIST_EXTENDLIST_APPENDLIST_EXTEND/ oはラップする必要がwをbワンでtupleの要件を満たすためにBUILD_LIST_UNPACK)。
ShadowRanger

18

他の回答とコメントに基づいて、が起こるかを完全に示します(特にShadowRangerの回答。これは、そのように行われる理由も説明ています)。

分解すると、BUILD_LIST_UNPACK慣れることがわかります。

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

これはで処理さceval.c、空のリストを作成して(でa)拡張します。

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend 使用 list_extend

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

どの通話list_resizeサイズの合計と

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

そして、それは次のように割り当てられます:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

確認してみましょう。上記の式でスポットの予想数を計算し、8を掛けて(ここでは64ビットPythonを使用しているため)、空のリストのバイトサイズ(つまり、リストオブジェクトの一定のオーバーヘッド)を追加して、予想されるバイトサイズを計算します。 :

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

出力:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

実際にはショートカットn = 0であるを除いて一致するため、list_extend実際には次のようにも一致します。

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {

8

これらはCPythonインタープリターの実装の詳細になるため、他のインタープリター間で一貫性がない場合があります。

とはいえ、理解度とlist(a)動作がどこにあるかを確認できます。

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

特に理解のために:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

それらの行のすぐ下にlist_preallocate_exact、を呼び出すときに使用されるものが存在しlist(a)ます。


1
[*a]個々の要素を1つずつ追加するのではありません。独自の専用バイトコードがあり、を介して一括挿入されextendます。
ShadowRanger

Gotcha-私はそれについて十分に掘り下げなかったと思います。上のセクションを削除しました[*a]
ランディ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.