我认为写一些最擅长猜测定义类的东西是值得的。为了完整起见,此答案还涉及绑定方法。
在最坏的情况下,猜测应该完全失败,函数返回None。但是,在任何情况下,它都不应该引发异常或返回错误的类。
TL;DR
我们函数的最终版本成功地克服了大多数简单的情况,以及一些陷阱。
简而言之,它的实现区分绑定方法和“unbound methods“ (functions),因为在Python 3 中,没有可靠的方法从“未绑定方法”中提取封闭类。
一些有用的 cmets 引发了额外的更改,如下面的编辑部分所述,产生了以下改进:
- 对通过描述符定义的、未被归类为普通方法或函数的方法(例如
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
一个小请求
如果您决定使用此实现并遇到任何警告,请发表评论并描述发生的情况。
完整版
“未绑定方法”是常规函数
首先,值得注意的是以下change 来自Python 3(参见Guido 的动机here):
“未绑定方法”的概念已从语言中删除。当引用方法作为类属性时,您现在会得到一个普通的函数对象。
这使得实际上不可能可靠地提取定义了某个“未绑定方法”的类,除非它绑定到该类(或其子类之一)的对象。
处理绑定方法
所以,让我们首先处理我们有绑定方法的“更简单的情况”。注意绑定方法必须写在Python中,如inspect.ismethod's documentation中所述。
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 也存在这个问题。一种可能的解决方法是遍历类的所有属性,寻找其身份与指定方法相同的属性。
处理“未绑定的方法”
既然我们已经解决了这个问题,我们可以建议一个尝试处理“未绑定方法”的 hack。可以在 this answer 中找到该 hack、其基本原理和一些令人沮丧的词。它依赖于手动解析the __qualname__ attribute、available only from 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 是互斥的,将这两种方法结合到一个解决方案中可以为我们带来以下结果(为即将到来的示例添加了日志记录工具):
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__ 属性是类本身,而不是它的对象。因此,在 __self__ 属性之上进一步访问 __class__ 属性无法按预期工作:
>>> 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'
编辑:
根据Bryce 提出的问题,只需返回它们的__objclass__ 属性(由@ 引入987654334@),如果存在:
if inspect.ismethoddescriptor(meth):
return getattr(meth, '__objclass__', None)
但是,inspect.ismethoddescriptor 会为各自的实例方法对象返回False,即set().union 和int().__add__:
- 由于
int().__add__.__objclass__返回int,为了解决int().__add__的问题,可以放弃上面的if子句。不幸的是,这并没有解决set().union 的问题,因为没有定义__objclass__ 属性。为了避免在这种情况下出现AttributeError 异常,__objclass__ 属性不是直接访问的,而是通过getattr 函数访问的。
编辑:
根据x-yuri 提出的issue,似乎我们的函数无法处理方法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
为了避免在回退到__qualname__ 解析期间访问__func__ 属性时可能出现AttributeError 异常,建议添加一个小的附加补丁。这是必需的,因为虽然__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
编辑:
根据user1956611 提出的suggestion,可以通过引入递归调用来查找创建partial 对象的原始可调用对象来处理partial objects:
if isinstance(meth, functools.partial):
return get_class_that_defined_method(meth.func)