Python 3で非バインドメソッドオブジェクトの定義クラスを取得する質問する

Python 3で非バインドメソッドオブジェクトの定義クラスを取得する質問する

クラスで定義されたメソッドのデコレータを作成するとします。そのデコレータが呼び出されたときに、メソッドを定義するクラスに属性を設定できるようにする必要があります (特定の目的を果たすメソッドのリストに登録するため)。

Python 2 では、このim_classメソッドはこれをうまく実現します。

def decorator(method):
  cls = method.im_class
  cls.foo = 'bar'
  return method

ただし、Python 3 では、そのような属性 (またはそれに代わるもの) は存在しないようです。 を呼び出してクラスを取得できるという考えだったと思いますが、その場合、type(method.__self__)これはバインドされていないメソッドでは機能しません。__self__ == None

注記:この質問は、実際には私のケースとは少し無関係です。メソッド自体に属性を設定し、インスタンスがそのすべてのメソッドをスキャンして、適切なタイミングでその属性を探すようにしたからです。また、私は (現在) Python 2.6 を使用しています。ただし、バージョン 2 の機能に代わるものがあるのか​​、また、ない場合はそれを完全に削除した理由は何なのかを知りたいです。

編集: 今見つけたこの質問こうなると、私のようにそれを避けるのが最善の解決策のように思えます。しかし、なぜ削除されたのかはまだわかりません。

ベストアンサー1

定義クラスを推測するのに最も適したものを書く価値があると思いました。完全性のために、この回答ではバインドされたメソッドについても説明します。

最悪の場合、推測は完全に失敗し、関数は を返しますNone。ただし、どのような状況でも、例外が発生したり、間違ったクラスが返されたりすることはありません。

要約

私たちの関数の最終バージョンは、ほとんどの単純なケースといくつかの落とし穴をうまく克服します。

簡単に言えば、その実装は、バインドされたメソッドと「非結合メソッド」(関数)Python 3「非バインドメソッド」から囲んでいるクラスを抽出する信頼できる方法が存在しないためです。

いくつかの有益なコメントにより追加の変更が促され、以下の編集セクションで詳述されているように、次のような改善が実現しました。

  • 記述子を介して定義され、通常のメソッドまたは関数として分類されないメソッド ( 、 など) と組み込みメソッド ( や など) の処理はset.union制限int.__add__int().__add__set().unionますio.BytesIO().__enter__
  • functools.partialオブジェクトの取り扱い。

結果の関数は次のようになります。

def get_class_that_defined_method(meth):
    if isinstance(meth, functools.partial):
        return get_class_that_defined_method(meth.func)
    if inspect.ismethod(meth) or (inspect.isbuiltin(meth) and getattr(meth, '__self__', None) is not None and getattr(meth.__self__, '__class__', None)):
        for cls in inspect.getmro(meth.__self__.__class__):
            if meth.__name__ in cls.__dict__:
                return cls
        meth = getattr(meth, '__func__', meth)  # fallback to __qualname__ parsing
    if inspect.isfunction(meth):
        cls = getattr(inspect.getmodule(meth),
                      meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0],
                      None)
        if isinstance(cls, type):
            return cls
    return getattr(meth, '__objclass__', None)  # handle special descriptor objects

ちょっとしたお願い

この実装を使用することにし、何らかの警告に遭遇した場合は、何が起こったのかをコメントして説明してください。


完全版

「非結合メソッド」は通常の関数である

まず、次の点に留意する価値がある。変化Python 3(グイドの動機を参照)ここ):

「非バインド メソッド」の概念は言語から削除されました。メソッドをクラス属性として参照すると、プレーンな関数オブジェクトが取得されるようになりました。

これにより、特定の「バインドされていないメソッド」がそのクラス (またはそのサブクラスのいずれか) のオブジェクトにバインドされていない限り、そのクラスを確実に抽出することが事実上不可能になります。

バインドされたメソッドの処理

そこで、まずは「より簡単なケース」であるバウンドメソッドを取り上げてみましょう。バウンドメソッドはPython、次のように記述される必要があります。inspect.ismethodのドキュメント

def get_class_that_defined_method(meth):
    # meth must be a bound method
    if inspect.ismethod(meth):
        for cls in inspect.getmro(meth.__self__.__class__):
            if meth.__name__ in cls.__dict__:
                return cls
    return None  # not required since None would have been implicitly returned anyway

ただし、この解決策は完璧ではなく、危険を伴います。メソッドは実行時に割り当てられる可能性があり、その名前は割り当て先の属性の名前と異なる可能性があります (以下の例を参照)。この問題は にも存在しますPython 2。回避策としては、クラスのすべての属性を反復処理して、指定されたメソッドと同じ ID を持つ属性を探すことが考えられます。

「非結合メソッド」の扱い

さて、その話は終わりにして、「非束縛メソッド」を扱うハックを提案しましょう。ハック、その根拠、そしていくつかの注意書きは、この答え手動で解析する必要がある属性__qualname__からのみ入手可能Python 3.3は、あまりお勧めできませんが、すべきのために働く単純事例:

def get_class_that_defined_method(meth):
    if inspect.isfunction(meth):
        return getattr(inspect.getmodule(meth),
                       meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0],
                       None)
    return None  # not required since None would have been implicitly returned anyway

両方のアプローチを組み合わせる

inspect.isfunctionと は相互に排他的であるためinspect.ismethod、両方のアプローチを 1 つのソリューションに組み合わせると、次のようになります (以降の例のためにログ機能が追加されています)。

def get_class_that_defined_method(meth):
    if inspect.ismethod(meth):
        print('this is a method')
        for cls in inspect.getmro(meth.__self__.__class__):
            if meth.__name__ in cls.__dict__:
                return cls
    if inspect.isfunction(meth):
        print('this is a function')
        return getattr(inspect.getmodule(meth),
                       meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0],
                       None)
    print('this is neither a function nor a method')
    return None  # not required since None would have been implicitly returned anyway

実行例

>>> class A:
...     def a(self): pass
... 
>>> class B:
...     def b(self): pass
... 
>>> class C(A, B):
...     def a(self): pass
... 
>>> A.a
<function A.a at 0x7f13b58dfc80>
>>> get_class_that_defined_method(A.a)
this is a function
<class '__main__.A'>
>>>
>>> A().a
<bound method A.a of <__main__.A object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(A().a)
this is a method
<class '__main__.A'>
>>>
>>> C.a
<function C.a at 0x7f13b58dfea0>
>>> get_class_that_defined_method(C.a)
this is a function
<class '__main__.C'>
>>>
>>> C().a
<bound method C.a of <__main__.C object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(C().a)
this is a method
<class '__main__.C'>
>>>
>>> C.b
<function B.b at 0x7f13b58dfe18>
>>> get_class_that_defined_method(C.b)
this is a function
<class '__main__.B'>
>>>
>>> C().b
<bound method C.b of <__main__.C object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(C().b)
this is a method
<class '__main__.B'>

ここまでは順調ですが...

>>> def x(self): pass
... 
>>> class Z:
...     y = x
...     z = (lambda: lambda: 1)()  # this returns the inner function
...     @classmethod
...     def class_meth(cls): pass
...     @staticmethod
...     def static_meth(): pass
...
>>> x
<function x at 0x7f13b58dfa60>
>>> get_class_that_defined_method(x)
this is a function
<function x at 0x7f13b58dfa60>
>>>
>>> Z.y
<function x at 0x7f13b58dfa60>
>>> get_class_that_defined_method(Z.y)
this is a function
<function x at 0x7f13b58dfa60>
>>>
>>> Z().y
<bound method Z.x of <__main__.Z object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(Z().y)
this is a method
this is neither a function nor a method
>>>
>>> Z.z
<function Z.<lambda>.<locals>.<lambda> at 0x7f13b58d40d0>
>>> get_class_that_defined_method(Z.z)
this is a function
<class '__main__.Z'>
>>>
>>> Z().z
<bound method Z.<lambda> of <__main__.Z object at 0x7f13b58ca9e8>>
>>> get_class_that_defined_method(Z().z)
this is a method
this is neither a function nor a method
>>>
>>> Z.class_meth
<bound method type.class_meth of <class '__main__.Z'>>
>>> get_class_that_defined_method(Z.class_meth)
this is a method
this is neither a function nor a method
>>>
>>> Z().class_meth
<bound method type.class_meth of <class '__main__.Z'>>
>>> get_class_that_defined_method(Z().class_meth)
this is a method
this is neither a function nor a method
>>>
>>> Z.static_meth
<function Z.static_meth at 0x7f13b58d4158>
>>> get_class_that_defined_method(Z.static_meth)
this is a function
<class '__main__.Z'>
>>>
>>> Z().static_meth
<function Z.static_meth at 0x7f13b58d4158>
>>> get_class_that_defined_method(Z().static_meth)
this is a function
<class '__main__.Z'>

最後の仕上げ

  • および によって生成される結果はx、実際に を返す前に、返される値が クラスであることを確認することによって、Z.y部分的に修正できます ( を返す)。None

  • によって生成された結果は、Z().z関数の__qualname__属性を解析することで修正できます (関数は を介し​​て抽出できますmeth.__func__)。

  • Z.class_methおよびによって生成される結果Z().class_methは正しくありません。クラス メソッドにアクセスすると、常にバインドされたメソッドが返されます。バインドされたメソッドの属性は、そのクラス自体ではなく、そのオブジェクトであるためです。したがって、その属性の上にある属性__self__にさらにアクセスしても、期待どおりには動作しません。__class____self__

    >>> Z().class_meth
    <bound method type.class_meth of <class '__main__.Z'>>
    >>> Z().class_meth.__self__
    <class '__main__.Z'>
    >>> Z().class_meth.__self__.__class__
    <class 'type'>
    

    これは、メソッドの__self__属性が のインスタンスを返すかどうかを確認することで修正できますtype。ただし、関数がメタクラスのメソッドに対して呼び出される場合は混乱を招く可能性があるため、今のところはそのままにしておきます。

最終バージョンは次のとおりです。

def get_class_that_defined_method(meth):
    if inspect.ismethod(meth):
        for cls in inspect.getmro(meth.__self__.__class__):
            if meth.__name__ in cls.__dict__:
                return cls
        meth = meth.__func__  # fallback to __qualname__ parsing
    if inspect.isfunction(meth):
        cls = getattr(inspect.getmodule(meth),
                      meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0],
                      None)
        if isinstance(cls, type):
            return cls
    return None  # not required since None would have been implicitly returned anyway

驚くべきことに、これにより と の結果も修正されZ.class_methZ().class_methが正しく返されるようになりました。これは、クラス メソッドの属性が、その属性を解析できる通常の関数を返すZためです。__func____qualname__

>>> Z().class_meth.__func__
<function Z.class_meth at 0x7f13b58d4048>
>>> Z().class_meth.__func__.__qualname__
'Z.class_meth'

編集:

提起された問題によれば、ブライスmethod_descriptorのようなオブジェクトset.unionや、wrapper_descriptorのようなオブジェクトをint.__add__、単にそれらの__objclass__属性(導入者ペップ252)、存在する場合:

if inspect.ismethoddescriptor(meth):
    return getattr(meth, '__objclass__', None)

ただし、それぞれのインスタンス メソッド オブジェクトに対してはinspect.ismethoddescriptorが返されます。つまり、 の場合は、 の場合は が返されます。Falseset().unionint().__add__

  • int().__add__.__objclass__は を返すのでint、 の問題を解決するために上記の if 節を放棄することができますint().__add__。残念ながら、これは の問題には対処しませんset().union。 には属性が定義されていません。このような場合に例外__objclass__を回避するために、属性は直接アクセスされず、関数を介してアクセスされます。AttributeError__objclass__getattr

編集:

によると問題育てたx-百合、関数はメソッドをメソッドとしてではなく組み込みとして識別するio.BytesIO().__enter__ため、メソッドを処理できないようです。inspect

>>> inspect.ismethod(io.BytesIO().__enter__)
False
>>> inspect.isbuiltin(io.BytesIO().__enter__)
True

これは、以下に関して上記で発生した問題と同じですset().union

>>> inspect.ismethod(set().union)
False
>>> inspect.isbuiltin(set().union)
True

この特殊性を除けば、このようなメソッドを通常のメソッドとして扱い、MRO を走査することで定義クラスを抽出することができます。

ただし、安全のために、追加の保護レイヤーを追加し、__self__そのようなメソッドの属性が定義されている場合はそれが定義されていないことNone、および__class__その__self__オブジェクトの属性が定義されている場合はそれが定義されていないことを確認しますNone

if inspect.ismethod(meth) or (inspect.isbuiltin(meth) and getattr(meth, '__self__', None) and getattr(meth.__self__, '__class__', None)):
    # ordinary method handling

残念ながら、この単純なテストは、空セットを返すため、に評価されるset().unionため失敗します。したがって、明示的な に対するテストが必要となり、次の修正が生成されます。bool(set().union.__self__)Falseset().union.__self__None

if inspect.ismethod(meth) or (inspect.isbuiltin(meth) and getattr(meth, '__self__', None) is not None and getattr(meth.__self__, '__class__', None)):
    # ordinary method handling

解析へのフォールバック中に属性AttributeErrorにアクセスするときに例外が発生する可能性を回避するために、追加のマイナーパッチが推奨されます。これは、属性が通常のメソッドに対して存在することが保証されている一方で、やなどの型 の 1 つに対して必ずしも定義されているとは限らないために必要です。__func____qualname____func__builtin_function_or_methodio.BytesIO().__enter__set().union

def get_class_that_defined_method(meth):
    if inspect.ismethod(meth) or (inspect.isbuiltin(meth) and getattr(meth, '__self__', None) is not None and getattr(meth.__self__, '__class__', None)):
        for cls in inspect.getmro(meth.__self__.__class__):
            if meth.__name__ in cls.__dict__:
                return cls
        meth = getattr(meth, '__func__', meth)  # fallback to __qualname__ parsing
    if inspect.isfunction(meth):
        cls = getattr(inspect.getmodule(meth),
                      meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0],
                      None)
        if isinstance(cls, type):
            return cls
    return getattr(meth, '__objclass__', None)  # handle special descriptor objects

編集:

によると提案提案したユーザー1956611、扱うことが可能ですpartialオブジェクトオブジェクトが作成された元の呼び出し可能オブジェクトを探すために再帰呼び出しを導入しますpartial

if isinstance(meth, functools.partial):
    return get_class_that_defined_method(meth.func)

おすすめ記事