クラスで定義されたメソッドのデコレータを作成するとします。そのデコレータが呼び出されたときに、メソッドを定義するクラスに属性を設定できるようにする必要があります (特定の目的を果たすメソッドのリストに登録するため)。
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
「非バインドメソッド」から囲んでいるクラスを抽出する信頼できる方法が存在しないためです。
- 境界メソッドの場合、単純に
MRO
、で行われたのと同様の方法で同等の質問に対する受け入れられた回答Python 2
。 - 「非結合メソッド」の場合、そのメソッドの解析に依存します修飾名、これはからのみ入手可能
Python 3.3
これはかなり無謀です (この機能が不要な場合は、このコード ブロックを削除して、代わりに戻るのが最善でしょうNone
)。
いくつかの有益なコメントにより追加の変更が促され、以下の編集セクションで詳述されているように、次のような改善が実現しました。
- 記述子を介して定義され、通常のメソッドまたは関数として分類されないメソッド ( 、 など) と組み込みメソッド ( や など) の処理は
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_meth
、Z().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
が返されます。つまり、 の場合は、 の場合は が返されます。False
set().union
int().__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__)
False
set().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_method
io.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)