在 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 功能的替代品,如果没有,完全删除它的理由是什么。

编辑:我刚发现 这个问题。这使得最好的解决方案似乎就是像我一样避免它。我仍然想知道为什么它被删除了。

原文由 Tim Yates 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 596
2 个回答

您似乎缺少的一点是,在 Python 3 中,“未绑定方法”类型完全消失了——一个方法,直到并且除非它被绑定,只是一个函数,没有用于执行的奇怪的“类型检查”未绑定方法.这使语言更简单!

以机智…:

 >>> class X:
...   def Y(self): pass
...
>>> type(X.Y)
<class 'function'>

瞧——一个不那么微妙的概念和需要担心的区别。这种简化是 Python 3 相对于 Python 2 的核心优势,Python 2(多年来)已经积累了如此多的微妙之处,以至于它处于真正失去其作为 简单 语言的地位的危险中(如果不断向其添加特性)。使用 Python 3,简单又 _回来了_!-)

原文由 Alex Martelli 发布,翻译遵循 CC BY-SA 2.5 许可协议

我认为写一些最擅长猜测定义类的东西是值得的。为了完整起见,此答案还解决了绑定方法。

在最坏的情况下,猜测应该完全失败,函数返回 None 。但是,在任何情况下,它都不应该引发异常或返回不正确的类。

长话短说

我们函数的最终版本成功地克服了大多数简单的情况,也克服了一些陷阱。

简而言之,它的实现区分了绑定方法和 “未绑定方法”(函数) ,因为在 Python 3 中没有可靠的方法从“未绑定方法”中提取封闭类。

一些有用的评论提示进行了其他更改,如以下编辑部分所述,产生了以下改进:

  • 对通过描述符定义的方法的有限处理,这些方法未归类为普通方法或函数(例如, set.unionint.__add__int().__add__ )和内置-在方法中(例如 set().unionio.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 中所做的以下 更改(参见 Guido 动机):

“未绑定方法”的概念已从语言中删除。当引用一个方法作为类属性时,您现在得到一个普通的函数对象。

这使得几乎不可能可靠地提取其中定义了某个“未绑定方法”的类,除非它绑定到该类(或其子类之一)的对象。

处理绑定方法

所以,让我们首先处理我们有绑定方法的“更简单的情况”。请注意,绑定方法必须写在 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 。一种可能的解决方法是遍历类的所有属性,寻找其标识与指定方法相同的属性。

处理“未绑定方法”

现在我们已经解决了这个问题,我们可以建议一个尝试处理“未绑定方法”的 hack。可以在 这个答案 中找到 hack、它的基本原理和一些劝阻的话。它依赖于手动解析 __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.isfunctioninspect.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'>

最后润色

  • xZ.y 生成的结果可以通过在实际返回之前验证返回值是一个类来部分修复(返回 None )。

  • Z().z 生成的结果可以通过回退到解析函数的 __qualname__ 属性来修复(可以通过 meth.__func__ 提取函数)。

  • Z.class_methZ().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'


编辑:

As per the issue raised by Bryce , it’s possible to handle method_descriptor objects, like set.union , and wrapper_descriptor objects, like int.__add__ , merely通过返回它们的 __objclass__ 属性(由 PEP-252 引入),如果存在的话:

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

However, inspect.ismethoddescriptor returns False for the respective instance method objects, ie for set().union and for int().__add__ :

  • 由于 int().__add__.__objclass__ 返回 int ,可以放弃上述 if 子句以解决 int().__add__ 的问题。不幸的是,这没有解决 set().union 的问题,因为没有定义 __objclass__ 属性。为了避免在这种情况下出现 AttributeError 异常,不能直接访问 __objclass__ 属性,而是通过 getattr 函数访问。

编辑:

根据 x-yuri 提出的 问题,我们的函数似乎无法处理方法 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

las,此简单的测试失败了 set().union False bool(set().union.__self__) 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__ -567a0 期间访问 __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 提出的 建议,可以通过引入递归调用来寻找创建 partial 对象的原始可调用对象来处理 partial 对象

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

原文由 Yoel 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题