内部からのPython。 オブジェクト 頭

1. はじめに

2. オブジェクト。

3. オブジェクト。 しっぽ

4. プロセス構造



Pythonの内部を引き続き理解しています。 前回 、Pythonが単純なプログラムをどのように消化するかを学びました。 今日、そのオブジェクトシステムのデバイスの研究を開始します。



前回のエピソードで書いたように(偶然にも成功したことが判明しました。皆さんのおかげで、あなたの意見やコメントは文字通り私を動かします!)-今日の投稿はPython 3.xでのオブジェクトの実装に当てられています 。 最初は簡単なトピックだと思いました。 しかし、投稿を書く前に読んでいたすべてのコードを読んだとしても、Pythonオブジェクトシステムは...ええと、「シンプル」とはほとんど言えません(完全に理解したとは言えません)。 しかし、私はオブジェクトの実装が最初から良いトピックであるとさらに確信しました。 次の投稿では、それがどれほど重要かを見ていきます。 同時に、Pythonのベテランであっても、それを完全に理解している人はほとんどいないと思います。 オブジェクトは他のすべてのPythonと緩やかに接続されています(投稿を書くとき、。/ Pythonを少し見て、。 ./Objects



./Include



)。 オブジェクトの実装は、他のすべてとまったく接続されていないかのように考える方が簡単に思えました。 オブジェクトサブシステムを作成するためのユニバーサルC APIであるかのように。 おそらく、あなたがこのように考えることもより簡単になるでしょう。これは、これらの構造を管理するための構造と機能のセットにすぎないことを忘れないでください。



Pythonのすべてはオブジェクトです:数値、辞書、カスタムクラスと組み込みクラス、スタックフレーム、コードオブジェクト。 メモリ位置へのポインタをオブジェクトと見なすためには、構造./Include/object.h



定義されている少なくとも2つのフィールドが必要です: PyObject







 typedef struct _object { Py_ssize_t ob_refcnt; struct _typeobject *ob_type; } PyObject;
      
      





多くのオブジェクトは必要なフィールドを追加することでこの構造を拡張しますが、これらの2つのフィールドは参照カウントタイプ (リンクを追跡するために特別なデバッグビルドに追加されます)



参照カウンタは、他のオブジェクトがこのオブジェクトを参照する回数を示す数値です。 コード>>> a = b = c = object()



では、空のオブジェクトが初期化され、 b



c



3つの異なる名前に関連付けられていc



。 各名前はオブジェクトへの新しいリンクを作成しますが、オブジェクトは一度作成されます。 オブジェクトを新しい名前でリンクするか、オブジェクトをリストに追加すると、新しいリンクが作成されますが、新しいオブジェクトは作成されません! このテーマについてはまだ多くのことが言われていますが、これはオブジェクトシステムよりもガベージコレクションに関連しています。 ここでこの質問を解析するのではなく、それについて別の投稿を書くほうがいいでしょう。 しかし、このトピックを去る前に、マクロ./Include/object.h



を理解するのが簡単になったと言います。最初の部分で出会った./Include/object.h



ですPy_DECREF



デクリメントするだけob_refcnt



ob_refcnt



値が0の場合はリソースを解放します) 。 とりあえず、参照カウントを終了しましょう。



オブジェクトのへのポインター、Pythonオブジェクトモデルの中心概念であるob_type



を解析することに変わりはありob_type



(覚えておいてください:3番目のPythonでは、 クラスは本質的に同じです。 歴史的な理由から、これらの用語使用はコンテキストに依存します)。 各オブジェクトには、オブジェクトの存続期間中に変更されないタイプが1つしかありません(タイプは非常にまれな状況で変更される可能性があります。このタスクにはAPIがありません。タイプを変更するオブジェクトを扱った場合、この記事はほとんど読みません)。 おそらくもっと重要なのは、オブジェクトのタイプ(およびオブジェクトタイプのみ )によって、それで何ができるかが決まることです(この段落の後のスポイラーの例)。 最初の部分から覚えているように、減算操作を実行すると、オペランドのタイプに関係なく、同じ関数が呼び出されます(整数、整数と小数、または辞書から例外を減算するなどの完全な不条理)。



コードを表示
 # ,   , ,      >>> class Foo(object): ... "I don't have __call__, so I can't be called" ... >>> class Bar(object): ... __call__ = lambda *a, **kw: 42 ... >>> foo = Foo() >>> bar = Bar() >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Foo' object is not callable >>> bar() 42 #   __call__? >>> foo.__call__ = lambda *a, **kw: 42 >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Foo' object is not callable #      Foo? >>> Foo.__call__ = lambda *a, **kw: 42 >>> foo() 42 >>>
      
      





最初は奇妙に思えます。 単一の関数で渡されるオブジェクトの種類をどのようにサポートできますか? void *



ポインターを取得できます(実際、データに対しても不透明なPyObject *



ポインターを取得しPyObject *



)が、受信した引数の処理方法をどのように決定しますか? 答えはオブジェクトのタイプにあります。 タイプはオブジェクトでもあります(参照カウントと独自のタイプがあり、ほとんどのタイプのタイプはtype



)が、2つのメインフィールドに加えて、他の多くのフィールドが含まれています。 ここでその分野の構造定義と記述を研究してください 。 定義自体は./Include/object.h



ます。 記事を読んでいるときに彼を参照することをお勧めします。 タイプのオブジェクトのフィールドの多くはスロットと呼ばれ、Python C-API関数がこのタイプのオブジェクトで呼び出されたときに実行される関数(または関連する関数を示す構造)を示します。 そして、 PyNumber_Subtract



は異なる型の引数で動作するように見えますが、実際には、オペランドの型は逆参照され、この型に固有の減算関数が呼び出されます。 したがって、C-API関数は汎用的ではありません。 タイプと詳細からの抽象に依存しており、どのデータでも動作するようです( TypeError



例外のスローも動作します)。



詳細を見てみましょう。 PyNumber_Subtract



は、2つの引数./Objects/abstract.c



の汎用関数を呼び出しますnb_subtract



スロットで作業する必要があることを示します(他の操作にも同様のスロットがあります。たとえば、 nb_negative



を無効にするsq_length



またはシーケンスの長さを決定するsq_length



)。 binary_op



は、すべての作業を行う関数binary_op1



でエラーチェックを行うラッパーです。 ./Objects/abstract.c



(この関数のコードを読む-多くのことに目を向ける)は、 BINARY_SUBTRACT



操作のオペランドをv



およびw



として受け入れ、数値を実装する関数へのポインターを含む構造体であるv->ob_type->tp_as_number



を逆参照しようとしますプロトコル。 binary_op1



は、 tp_as_number->nb_subtract



で、オペランドが減分され減算されたものとして互換性がないと判断した場合、減算を実行するか、特別なPy_NotImplemented



値を返すC関数を見つけることを期待します(これはTypeError



例外をスローします)。



オブジェクトの動作を変更したい場合は、 PyTypeObject



構造体をオーバーライドし、希望する方法でスロットを埋めるC拡張機能を記述できます。 Pythonで新しい型を作成するとき( >>> class Foo(list): pass



create a new type 、クラスと型は同じものです)、構造を手動で記述せず、スロットを埋めません。 しかし、なぜこれらの型は組み込みのものと同じように振る舞うのですか? そうです、継承のために、型付けが重要な役割を果たします。 Pythonには、 list



dict



などの組み込み型が既にあります。 前述のように、これらのタイプには、対応するスロットを埋める特定の機能があります。これにより、オブジェクトに望ましい動作が与えられます。たとえば、値のシーケンスの可変性や値へのキーのマッピングです。 Pythonで(他のオブジェクトと同様に)ヒープ上に新しい型を作成すると、新しいC構造が動的に決定され、そのスロットは継承された基本的な型に従って埋められます( 複数の継承についてどうですか? 他のエピソードで )。 なぜなら スロットがコピーされ、新しく認識されたサブタイプとベーシックの機能はほぼ同じです。 Pythonには、機能のない基本タイプ( object



(CのPyBaseObject_Type



))があり、ほとんどすべてのスロットがゼロになり、何も継承せずに拡張できます。



したがって、Pythonで型を作成することはできず、常に他の何かから継承します(明示的な継承なしでクラスを定義すると、 object



から暗黙的に継承されobject



。Python2.xでは、この場合「クラシック」クラスが作成されます、それらは考慮されません)。 当然、常にすべてを継承する必要はありません。 上記のスニペットに示すように、Pythonで直接作成された型の動作を変更できます。 Bar



クラスで特別な __call__ メソッドを定義することにより 、このクラスのインスタンスを呼び出し可能にしました。



クラスの作成中のどこかで、この__call__



メソッドに気づき、それをtp_call



スロットに関連付けます。 ./Objects/typeobject.c



複雑で重要な機能-これがすべて起こる場所です。 この関数の詳細については次の投稿で説明します。新しいタイプが作成された後、戻る前に、ほとんど最後の行に注目しますfixup_slot_dispatchers(type);



。 この関数は、新しいタイプで定義されたすべての正しい名前のメソッドを実行し、メソッドの名前に基づいて、タイプ構造内の必要なスロットに関連付けます(ただし、これらのメソッドはどこに格納されますか?)。



別の不可解な点:型を作成したの型の__call__



メソッドの定義は、メソッド定義の前にインスタンス化された場合でも、この型のインスタンスをどのように呼び出し可能にしますか? 簡単でシンプルな、私の友人。 覚えているように、タイプはオブジェクトであり、タイプはtype



(頭痛がある場合は、以下を実行してください: >>> class Foo(list): pass ; type(Foo)



)。 したがって、クラスで何かをするとき(classの代わりにtypeという単語を書くこともできますが、別のコンテキストで「type」を使用するため、しばらくの間typeをクラスと呼びましょう)、たとえば、call、subtractまたは、属性を定義すると、 ob_type



オブジェクトのob_type



フィールドが逆参照され、クラスtype



type



あることが検出されます。 次に、 type->tp_setattro



スロットを使用して属性を設定します。 つまり、クラスには個別の属性設定機能を持たせることができます。 そして、そのような特定の関数(facebookでフリーズしたい場合、ここにそのページがありますtype_setattro



)は、 fixup_slot_dispatchers



が新しい属性を決定した後にすべての問題を解決するために使用する同じ関数( update_one_slot



)をfixup_slot_dispatchers



ます。 新しい詳細が明らかになりました!



これはおそらくPythonオブジェクトの紹介を終了する場所です。 旅行を楽しんでくれて、まだ私と一緒にいることを願っています。 この投稿を書くのは思ったよりもはるかに困難であったことを認めなければなりません(そして、 #python-dev



で夜遅くにAntoine PitrouMark Dickinsの助けがなければ、おそらくあきらめます!)。 まだ多くの興味深いことが残っています。バイナリ演算で使用されるオペランドスロットはどれですか。 多重継承ではどうなりますか?また、それに関連付けられた気味が悪いほど小さな詳細はどうでしょうか? メタクラスはどうですか? そして、 __slots__



弱いリンク ? 埋め込みオブジェクトで何が起こっていますか? 辞書リストセット、およびそれらのいとこどのように機能しますか? そして最後に、 この奇跡はどうですか?



 >>> a = object() >>> class C(object): pass ... >>> b = C() >>> a.foo = 5 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'object' object has no attribute 'foo' >>> b.foo = 5 >>>
      
      





object



を継承するC



インスタンスb



に任意の属性を追加するだけで、同じobject



インスタンスa



で同じことができないのはどうして? b



__dict__



がありますがa



はありません。 はい、そうです。 しかし、この新しい(そして完全に非自明な!)機能は、継承しない場合、どこから来たのでしょうか?



ハ! 私はそのような質問に非常に満足しています! 回答はになりますが、次のエピソードでは。






好奇心の強い人のための短い文献リスト:





いい夢を。






覚えて! 興味のある人と会えるの いつでも嬉しいです。



All Articles