2

Python 2 では、任意の callable をクラスのメソッドに変換することができました。重要なのは、callable が C で実装された CPython 組み込みである場合、これを使用して、それ自体が C レイヤーであるユーザー定義クラスのメソッドを作成し、呼び出されたときにバイト コードを呼び出さないことです。

これは、「ロックのない」同期を提供するために GIL に依存している場合に役立つことがあります。GIL は op コード間でしか交換できないため、コードの特定の部分のすべてのステップを C にプッシュできる場合は、アトミックに動作させることができます。

Python 2 では、次のようなことができます。

import types
from operator import attrgetter
class Foo(object):
    ... This class maintains a member named length storing the length...

    def __len__(self):
        return self.length  # We don't want this, because we're trying to push all work to C

# Instead, we explicitly make an unbound method that uses attrgetter to achieve
# the same result as above __len__, but without no byte code invoked to satisfy it
Foo.__len__ = types.MethodType(attrgetter('length'), None, Foo)

Python 3 では、バインドされていないメソッド型はなくなり、types.MethodType2 つの引数のみを取り、バインドされたメソッドのみを作成します (これは__len____hash__、 などの Python の特殊なメソッドには役に立ちません。特殊なメソッドは、多くの場合、型で直接検索されるため、インスタンスではありません)。

私が見逃しているPy3でこれを達成する方法はありますか?

私が見たもの:

  1. functools.partialmethod(C実装がないように見えるため、要件に失敗し、Python実装と必要以上に汎用的であるため、遅く、テストで約5 usかかりますが、直接の場合は約200〜300 nsかかりますPython 定義またはattrgetterPy2 では、オーバーヘッドが約 20 倍増加します)
  2. attrgetter非データ記述子プロトコルに従おうとするなど(AFAICTでは不可能、a__get__などでモンキーパッチを適用できない)
  3. attrgetterを与えるためにサブクラス化する方法を見つけようとしています__get__が、もちろん、__get__何らかの方法で C レイヤーに委譲する必要があり、今では最初の場所に戻っています。
  4. attrgetter(ユースケースに固有)__slots__最初にメンバーを記述子にするために使用し、その後、データの結果の記述子から、実際の値をバインドして取得する最終ステップをスキップして、呼び出し可能にする何かに変換しようとしますしたがって、実際の値の取得は延期されます

ただし、これらのオプションのいずれについても、何かを見逃していないとは言えません。誰にも解決策はありますか?完全なハッキングが許可されています。私はここで病的なことをしていることを認識しています。柔軟であることが理想的です ( 、 、 などclassの Python 組み込み関数、または Python レイヤーで定義されていないその他の呼び出し可能なオブジェクトから、バインドされていないメソッドのように動作するものを作成できるようにするため)。重要なことに、各インスタンスではなく、クラスにアタッチする必要があります (インスタンスごとのオーバーヘッドを削減し、ほとんどの場合インスタンス ルックアップをバイパスする特別なメソッドを正しく動作させるため)。hexlen

4

1 に答える 1

1

最近、これに対する(おそらくCPythonのみの)解決策が見つかりました。ctypesCPython API を直接呼び出すためのハックであるため、少し醜いですが、機能し、目的のパフォーマンスが得られます。

import ctypes
from operator import attrgetter

make_instance_method = ctypes.pythonapi.PyInstanceMethod_New
make_instance_method.argtypes = (ctypes.py_object,)
make_instance_method.restype = ctypes.py_object

class Foo:
    # ... This class maintains a member named length storing the length...

    # Defines a __len__ method that, at the C level, fetches self.length
    __len__ = make_instance_method(attrgetter('length'))

これは、ある意味では Python 2 バージョンよりも改善されています。これは、バインドされていないメソッドを作成するためにクラスを定義する必要がないため、単純な代入によってクラス本体で定義できるためです (Python 2 バージョンはで明示的にFoo2 回参照する必要があり、 の定義が完了Foo.__len__ = types.MethodType(attrgetter('length'), None, Foo)した後でのみ)。class Foo

一方、CPython 3.7 AFAICT では実際にはパフォーマンス上の利点はありません。少なくとも、ここで置き換えている単純なケースではそうではありませんdef __len__(self): return self.length。実際、のインスタンスで__len__経由でアクセスした場合、を経由で定義した場合、マイクロベンチマークは最大 10% 遅くなります。これはおそらくのアーティファクトですlen(instance)Fooipython %%timeitlen(instance)__len____len__ = make_instance_method(attrgetter('length'))attrgetterCPython が "FastCall" プロトコル (暫定的なサードパーティの使用のためにセミパブリックにされたときに 3.8 で "Vectorcall" と呼ばれていた) に移行しなかったため、それ自体がわずかに高いオーバーヘッドを持っていますが、ユーザー定義関数は既にそれを利用しています。 3.7、およびドット付きまたはドットなしの属性ルックアップ、および単一または複数の属性ルックアップを実行するかどうかを動的に選択する必要があるだけでなく (Vectorcall は__call__、構築時に実行される get に適した実装を選択することで回避できる場合があります)、オーバーヘッドが追加されます。プレーンメソッドが回避すること。より複雑な場合 (たとえば、取得する属性が のようなネストされた属性である場合) には勝つはずですself.contained.lengthattrgetterのオーバーヘッドはほぼ固定されていますが、Python でのネストされた属性ルックアップはより多くのバイト コードを意味しますが、現時点ではあまり役に立ちません。

operator.attrgetter彼らが Vectorcall の最適化に取りかかったことがあれば、この回答を再ベンチマークして更新します。

于 2019-11-14T20:53:24.993 に答える