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

Get defining class of unbound method object in Python 3

假设我想为类中定义的方法制作一个修饰器。我希望这个修饰器在被调用时能够在定义方法的类上设置一个属性(以便将它注册到一个特定用途的方法列表中)。

在python2中,im_class方法很好地实现了这一点:

1
2
3
4
def decorator(method):
  cls = method.im_class
  cls.foo = 'bar'
  return method

然而,在python3中,似乎不存在这样的属性(或它的替换)。我想您可以调用type(method.__self__)来获取类,但这对未绑定的方法不起作用,因为在这种情况下__self__ == None

注意:这个问题实际上与我的情况有点不相关,因为我选择了在方法本身上设置一个属性,然后让实例在适当的时候扫描它的所有方法以查找该属性。我(目前)也在使用python2.6。但是,我很好奇是否有版本2功能的替代品,如果没有,完全删除它的理由是什么。

编辑:我刚刚发现这个问题。这让我觉得最好的解决办法就是像我一样避免它。不过,我还是想知道为什么要把它取下来。


我认为写些最适合猜测定义类的东西是值得的。为了完整起见,这个答案还涉及绑定方法。好的。

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

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

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

  • 对于有界方法,它简单地遍历MRO,其方式与在接受的Python 2等价问题的答案中所做的方式类似。
  • 对于"未绑定方法",它依赖于分析其限定名,该限定名只能从Python 3.3获得,而且非常鲁莽(如果不需要此功能,最好删除此代码块,然后返回None)。

对于通过描述符定义的方法,也有部分处理,这些方法没有被归类为普通的方法或函数(例如,set.unionint.__add__int().__add__,但不是set().union)。好的。

结果函数为:好的。

1
2
3
4
5
6
7
8
9
10
11
12
def get_class_that_defined_method(meth):
    if inspect.ismethod(meth):
        for cls in inspect.getmro(meth.__self__.__class__):
           if cls.__dict__.get(meth.__name__) is meth:
                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])
        if isinstance(cls, type):
            return cls
    return getattr(meth, '__objclass__', None)  # handle special descriptor objects

小小的要求

如果您决定使用这个实现,并且遇到任何警告,请评论并描述发生了什么。好的。完整版本"未绑定方法"是正则函数

首先,值得注意的是,在Python 3中所做的以下更改(见guido的动机):好的。

The concept of"unbound methods" has been removed from the language. When referencing a method as a class attribute, you now get a plain function object.

Ok.

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

因此,让我们首先处理"更简单的情况",其中我们有一个绑定方法。注意,绑定方法必须写在Python中,如inspect.ismethod的文档中所述。好的。

1
2
3
4
5
6
7
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 cls.__dict__.get(meth.__name__) is meth:
                return cls
    return None  # not required since None would have been implicitly returned anyway

但是,此解决方案并不完美,而且存在风险,因为可以在运行时分配方法,从而使它们的名称可能与分配给它们的属性的名称不同(请参见下面的示例)。这个问题也存在于Python 2中。一个可能的解决方法是迭代类的所有属性,寻找一个标识为指定方法的属性。好的。处理"未绑定方法"

既然我们已经排除了这一点,我们可以建议一个尝试处理"未绑定方法"的黑客。在这个答案中可以找到黑客,它的基本原理和一些令人沮丧的话。它依赖于手动分析__qualname__属性,该属性仅可从Python 3.3获得,非常不常用,但适用于简单情况:好的。

1
2
3
4
5
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])
    return None  # not required since None would have been implicitly returned anyway

结合两种方法

由于inspect.isfunctioninspect.ismethod是互斥的,将这两种方法结合到一个单一的解决方案中,我们得到了以下结果(在接下来的示例中增加了日志记录设施):好的。

1
2
3
4
5
6
7
8
9
10
11
12
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 cls.__dict__.get(meth.__name__) is meth:
                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])
    print('this is neither a function nor a method')
    return None  # not required since None would have been implicitly returned anyway

执行示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
>>> 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'>

到目前为止,很好,但是…好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
>>> 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
...
>>> 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'>

最后的接触

  • Z.y生成的结果可以部分固定(返回None,方法是在实际返回之前验证返回值是一个类。
  • 通过对函数的__qualname__属性进行分析,可以修复Z().z生成的结果(可以通过meth.__func__提取函数)。
  • Z.class_methZ().class_meth生成的结果不正确,因为访问类方法总是返回一个绑定方法,其__self__属性是类本身,而不是其对象。因此,进一步访问该__self__属性之上的__class__属性并不能按预期工作:好的。

    1
    2
    3
    4
    5
    6
    >>> 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的实例来解决。但是,当针对元类的方法调用我们的函数时,这可能会令人困惑,因此现在我们将保持原样。好的。

以下是最终版本:好的。

1
2
3
4
5
6
7
8
9
10
11
12
def get_class_that_defined_method(meth):
    if inspect.ismethod(meth):
        for cls in inspect.getmro(meth.__self__.__class__):
            if cls.__dict__.get(meth.__name__) is meth:
                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])
        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__属性可以被解析:好的。

1
2
3
4
>>> 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.unionwrapper_descriptor对象,如存在:好的。

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

但是,对于各自的实例方法对象,即对于set().unionint().__add__inspect.ismethoddescriptor返回False:好的。

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

好啊。


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

为了……

1
2
3
4
5
>>> class X:
...   def Y(self): pass
...
>>> type(X.Y)
<class 'function'>

瞧——一个不那么微妙的概念和区别值得担心。这种简化是python 3与python 2的核心优势,多年来,python 3积累了太多的微妙之处,以至于它面临着真正失去简单语言地位的危险(如果不断添加特性的话)。有了Python3,简单性又回来了!-)


由于python 3.6,您可以使用定义__set_name__方法的修饰器来完成您所描述的工作。文档声明在创建类时调用object.__set_name__

下面是一个示例,它修饰了一个方法"以便将其注册到一个具有特定用途的方法列表中":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
>>> 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修饰符可以访问类吗?因此,我的答案和我在那里提供的答案一样。


python 3.6(python 2.7)的一个小扩展可以很好地解决https://stackoverflow.com/a/25959545/4013571的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_class_that_defined_method(meth):
    if inspect.ismethod(meth):
        for cls in inspect.getmro(meth.__self__.__class__):
            if cls.__dict__.get(meth.__name__) is meth:
                return cls
        meth = meth.__func__  # fallback to __qualname__ parsing
    if inspect.isfunction(meth):
        class_name = meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0]
        try:
            cls = getattr(inspect.getmodule(meth), class_name)
        except AttributeError:
            cls = meth.__globals__.get(class_name)
        if isinstance(cls, type):
            return cls
    return None  # not required since None would have been implicitly returned anyway

我发现doctest需要以下调整

1
2
        except AttributeError:
            cls = meth.__globals__.get(class_name)

由于某种原因,当使用nose时,inspect.getmodule(meth)不包含定义类