web-dev-qa-db-ja.com

リストが__len__について尋ねるのはなぜですか?

class Foo:
    def __getitem__(self, item):
        print('getitem', item)
        if item == 6:
            raise IndexError
        return item**2
    def __len__(self):
        print('len')
        return 3

class Bar:
    def __iter__(self):
        print('iter')
        return iter([3, 5, 42, 69])
    def __len__(self):
        print('len')
        return 3

デモ:

>>> list(Foo())
len
getitem 0
getitem 1
getitem 2
getitem 3
getitem 4
getitem 5
getitem 6
[0, 1, 4, 9, 16, 25]
>>> list(Bar())
iter
len
[3, 5, 42, 69]

list__len__を呼び出すのはなぜですか?明らかなことには結果を使用していないようです。 forループはそれを行いません。これは イテレータプロトコル のどこにも言及されていません。これは__iter____next__について話しているだけです。

これはPython事前にリスト用のスペースを予約するのですか、それともそのような賢いものですか?

(Linux上のCPython 3.6.0)

34
wim

__length_hint__を導入し、動機に関する洞察を提供する PEP 424の根拠セクション を参照してください。

__length_hint__によって推定されるように、予想されるサイズに基づいてリストを事前に割り当てることができることは、重要な最適化になる可能性があります。 CPythonは、純粋にこの最適化が存在するために、PyPyよりも高速にコードを実行することが確認されています。

それに加えて、ドキュメント object.__length_hint__の場合 は、これが純粋に最適化機能であることを確認します。

operator.length_hint()を実装するために呼び出されます。オブジェクトの推定長さを返す必要があります(実際の長さよりも長い場合も短い場合もあります)。長さは整数>= 0でなければなりません。 このメソッドは純粋に最適化であり、正確性のために必要になることはありません

したがって、__length_hint__は、いくつかの優れた最適化をもたらす可能性があるため、ここにあります。

PyObject_LengthHint最初にobject.__len__から値を取得しようとします(定義されている場合) 次に、object.__length_hint__が使用可能かどうかを確認しようとします。どちらも存在しない場合は、リストのデフォルト値8を返します。

Eliが回答で述べたようにlist_initから呼び出されるlistextendは、このPEPに従って変更され、__len__または__length_hint__のいずれかを定義するすべてのものに対してこの最適化を提供します。

もちろん、これから恩恵を受けるのはlistだけではありません bytesオブジェクトはそうします

>>> bytes(Foo())
len
getitem 0
...
b'\x00\x01\x04\t\x10\x19'

したがって bytearrayオブジェクトを実行しますが、extendオブジェクトを実行する場合のみ

>>> bytearray().extend(Foo())
len
getitem 0
...

およびTupleオブジェクトを作成します 中間シーケンスto 自分自身にデータを入力します:

>>> Tuple(Foo())
len
getitem 0
...
(0, 1, 4, 9, 16, 25)

誰かがさまよっている場合、なぜ正確に'iter'が出力されるのかbefore'len' in class Barであり、class Fooのように後ではありません。

これは、手元のオブジェクトが__iter__Python 最初に呼び出すイテレータを取得する を定義している場合、それによってprint('iter')も実行されるためです。 __getitem__の使用にフォールバックした場合、同じことは起こりません。

listは、メモリの初期スライスをその内容に割り当てるリストオブジェクトコンストラクタです。リストコンストラクターは、コンストラクターに渡されたオブジェクトの長さのヒントまたは長さをチェックすることにより、メモリの最初のスライスに適したサイズを見つけようとします。 Python ソースはこちらPyObject_LengthHint の呼び出しを参照してください。この場所はリストコンストラクターから呼び出されます--- list_init

オブジェクトに__len__または__length_hint__がない場合は、問題ありません--a デフォルト値8 が使用されます。再割り当てのために効率が低下する可能性があります。

31
Eli Bendersky

:答えを用意しました [SO]:__ len__が呼び出され、__ getitem __? で反復すると結果が使用されないのはなぜですか?私が書いている間に(まさにこの質問なので)複製したので、そこに投稿することはできなくなりました。すでに持っていたので、ここに投稿することにしました(少し調整して)。

これは、物事を少し明確にするコードの修正バージョンです。

code00.py

_#!/usr/bin/env python3

import sys


class Foo:
    def __getitem__(self, item):
        print("{0:s}.{1:s}: {2:d}".format(self.__class__.__name__, "getitem", item))
        if item == 6:
            raise IndexError
        return item ** 2


class Bar:
    def __iter__(self):
        print("{0:s}.{1:s}".format(self.__class__.__name__, "iter"))
        return iter([3, 5, 42, 69])

    def __len__(self):
        result = 3
        print("{0:s}.{1:s}: {2:d}".format(self.__class__.__name__, "len", result))
        return result


def main():
    print("Start ...\n")
    for class_obj in [Foo, Bar]:
        inst_obj = class_obj()
        print("Created {0:s} instance".format(class_obj.__name__))
        list_obj = list(inst_obj)
        print("Converted instance to list")
        print("{0:s}: {1:}\n".format(class_obj.__name__, list_obj))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main()
    print("\nDone.")
_

出力

_[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q041474829]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32

Start ...

Created Foo instance
Foo.getitem: 0
Foo.getitem: 1
Foo.getitem: 2
Foo.getitem: 3
Foo.getitem: 4
Foo.getitem: 5
Foo.getitem: 6
Converted instance to list
Foo: [0, 1, 4, 9, 16, 25]

Created Bar instance
Bar.iter
Bar.len: 3
Converted instance to list
Bar: [3, 5, 42, 69]


Done.
_

ご覧のとおり、リストの作成時に__ len __が呼び出されます。ブラウジング [GitHub]:python/cpython-(マスター)cpython/Objects/listobject.c

  • list ___ init __(これは初期化子です:__ init __tp_initメンバーPyList_Type))呼び出しlist ___ init ___ impl
  • list ___ init ___ impl呼び出しlist_extend
  • list_extend呼び出しPyObject_LengthHintn = PyObject_LengthHint(iterable, 8);
  • PyObject_LengthHintabstract.c内)、チェックを行います:

    _Py_ssize_t
    PyObject_LengthHint(PyObject *o, Py_ssize_t defaultvalue)
    
        // ...
    
        if (_PyObject_HasLen(o)) {
            res = PyObject_Length(o);
    
        // ...
    _

つまり、これは__len __を定義する反復可能オブジェクトに対して機能する最適化機能です。

これは、イテラブルに多数の要素があり、それらが一度に割り当てられるため、リストの拡大メカニズムをスキップする場合に特に便利です(まだ適用されるかどうかはチェックしませんでしたが、ある時点では適用されていました): "いっぱいになるとスペースが約12.5%増加します "(David M. Beazleyによる)。リストが(他の)リストまたはタプルから作成された場合に非常に役立ちます。
たとえば、10要素を使用して反復可能(__ len __を定義しない)からリストを作成します、すべてを一度に割り当てる代わりに、〜41log1.125(1000 / 8) )操作(割り当て、データシフト、割り当て解除)は、新しいリストがいっぱいになると(ソースの反復可能な要素で)増加するためにのみ必要です。

言うまでもなく、「最新の」反復可能オブジェクトの場合、改善は適用されなくなります。

0
CristiFati