获取 Python 3 中未绑定方法对象的定义类

58 投票
5 回答
31713 浏览
提问于 2025-04-16 03:25

假设我想为一个类里面的方法做一个装饰器。我希望这个装饰器在被调用的时候,可以在定义这个方法的类上设置一个属性(这样可以把这个方法注册到一个特定用途的方法列表里)。

在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。不过,我还是很好奇有没有替代Python 2的功能,如果没有的话,为什么会完全去掉它。

编辑:我刚找到这个问题。这让我觉得最好的解决办法就是像我一样避免使用它。不过,我还是在想为什么会把它去掉。

5 个回答

9

从Python 3.6开始,你可以使用一个装饰器来实现你所描述的功能,这个装饰器里需要定义一个叫做 __set_name__ 的方法。文档中提到,当类被创建时,object.__set_name__ 方法会被调用。

下面是一个例子,这个例子装饰了一个方法,目的是“将它注册到一个特定用途的方法列表中”:

>>> class particular_purpose: 
...     def __init__(self, fn): 
...         self.fn = fn 
...      
...     def __set_name__(self, owner, name): 
...         owner._particular_purpose.add(self.fn) 
...          
...         # then replace ourself with the original method 
...         setattr(owner, name, self.fn) 
...  
... class A: 
...     _particular_purpose = set() 
...  
...     @particular_purpose 
...     def hello(self): 
...         return "hello" 
...  
...     @particular_purpose 
...     def world(self): 
...         return "world" 
...  
>>> A._particular_purpose
{<function __main__.A.hello(self)>, <function __main__.A.world(self)>}
>>> a = A() 
>>> for fn in A._particular_purpose: 
...     print(fn(a)) 
...                                                                                                                                     
world
hello

请注意,这个问题和一个关于Python实例方法装饰器是否可以访问类的问题非常相似,因此我在那里提供的答案也是相关的。

124

我觉得写一些能最好地猜测定义类的东西是值得的。为了完整性,这个回答也涉及到绑定方法。

最糟糕的情况是,猜测应该完全失败,函数返回 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 中也存在。一个可能的解决方法是遍历所有类的属性,寻找一个其身份与指定方法相同的属性。

处理“未绑定方法”

现在我们解决了这个问题,我们可以提出一个尝试处理“未绑定方法”的黑科技。这个黑科技、它的原理以及一些劝阻的话可以在这个答案中找到。它依赖于手动解析__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)。

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

  • 由于访问类方法总是返回一个绑定方法,其 __self__ 属性是类本身,而不是它的对象,因此 Z.class_methZ().class_meth 生成的结果是错误的。因此,进一步访问该 __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_methZ().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__ 属性(由PEP-252引入)来处理method_descriptor对象,如set.union,和wrapper_descriptor对象,如int.__add__,如果存在的话:

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

然而,inspect.ismethoddescriptor 对于相应的实例方法对象返回 False,即对于 set().unionint().__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,并且该 __self__ 对象的 __class__ 属性(如果定义的话)也不为 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提出的建议,可以通过引入递归调用来处理partial对象,以寻找创建该 partial 对象时的原始可调用对象:

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

你似乎忽略了一个重点,那就是在Python 3中,“未绑定方法”这种类型完全消失了。一个方法,除非被绑定,否则它只是一个函数,没有那些奇怪的“类型检查”未绑定方法的麻烦。这让语言变得更简单了!

举个例子...:

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

看,这样一来,我们就少了一个需要担心的复杂概念和区别。这样的简化是Python 3相对于Python 2的核心优势,因为Python 2在这些年里积累了太多复杂的细节,如果继续添加新功能,它真的有可能失去作为一种简单语言的地位。而在Python 3中,简单性又回来了!-)

撰写回答