简体   繁体   English

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

[英]Get defining class of unbound method object in Python 3

Say I want to make a decorator for methods defined in a class.假设我想为类中定义的方法制作一个装饰器。 I want that decorator, when invoked, to be able to set an attribute on the class defining the method (in order to register it in a list of methods that serve a particular purpose).我希望该装饰器在调用时能够在定义该方法的类上设置一个属性(以便将其注册到用于特定目的的方法列表中)。

In Python 2, the im_class method accomplishes this nicely:在 Python 2 中, im_class方法很好地完成了这一点:

def decorator(method):
  cls = method.im_class
  cls.foo = 'bar'
  return method

However, in Python 3, no such attribute (or a replacement for it) seems to exist.然而,在 Python 3 中,似乎不存在这样的属性(或它的替代品)。 I suppose the idea was that you could call type(method.__self__) to get the class, but this does not work for unbound methods, since __self__ == None in that case.我想这个想法是你可以调用type(method.__self__)来获取类,但这不适用于未绑定的方法,因为在这种情况下__self__ == None

NOTE: This question is actually a bit irrelevant for my case, since I've chosen instead to set an attribute on the method itself and then have the instance scan through all of its methods looking for that attribute at the appropriate time.注意:这个问题实际上与我的情况有点无关,因为我选择在方法本身上设置一个属性,然后让实例在适当的时间扫描其所有方法以查找该属性。 I am also (currently) using Python 2.6.我(目前)也在使用 Python 2.6。 However, I am curious if there is any replacement for the version 2 functionality, and if not, what the rationale was for removing it completely.但是,我很好奇是否有任何替代版本 2 的功能,如果没有,完全删除它的理由是什么。

EDIT : I just found this question .编辑:我刚刚发现这个问题 This makes it seem like the best solution is just to avoid it like I have.这使得看起来最好的解决方案就是像我一样避免它。 I'm still wondering why it was removed though.我仍然想知道为什么它被删除了。

I thought it would be worthwhile writing something that does it best at guessing the defining class.我认为值得写一些最擅长猜测定义类的东西。 For completeness' sake this answer also addresses bound methods.为了完整起见,这个答案还涉及绑定方法。

At worst, guessing should fail altogether, with the function returning None .在最坏的情况下,猜测应该完全失败,函数返回None However, under any circumstances, it shouldn't raise an exception or return an incorrect class.但是,在任何情况下,它都不应该引发异常或返回不正确的类。

TL;DR TL; 博士

The final version of our function successfully overcomes most simple cases, and a few pitfalls as well.我们函数的最终版本成功地克服了大多数简单的情况,以及一些陷阱。

In a nutshell, its implementation differentiates between bound methods and “unbound methods“ (functions) since in Python 3 there is no reliable way to extract the enclosing class from an “unbound method".简而言之,它的实现区分了绑定方法和“未绑定方法”(函数),因为在Python 3中没有可靠的方法从“未绑定方法”中提取封闭类。

Several useful comments prompted additional changes, as detailed in the edits section below, producing the following improvements:一些有用的评论提示了额外的更改,如下面的编辑部分所述,产生了以下改进:

  • Limited handling for methods defined via descriptors, that aren't classified as ordinary methods or functions (for example, set.union , int.__add__ and int().__add__ ) and for built-in methods (for example set().union and io.BytesIO().__enter__ ).对通过描述符定义的方法的有限处理,这些方法不属于普通方法或函数(例如set.unionint.__add__int().__add__ )和内置方法(例如set().unionio.BytesIO().__enter__ )。
  • Handling of functools.partial objects. functools.partial对象的处理。

The resulting function is:结果函数是:

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

A small request一个小小的要求

If you decide to use this implementation, and encounter any caveats, please comment and describe what happened.如果您决定使用此实现,并遇到任何警告,请发表评论并描述发生的情况。


The Full Version完整版

“Unbound methods” are regular functions “未绑定方法”是常规函数

First of all, it's worth noting the following change made in Python 3 (see Guido's motivation here ):首先,值得注意的是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.将方法作为类属性引用时,您现在会得到一个普通的函数对象。

This makes it practically impossible to reliably extract the class in which a certain “unbound method“ was defined unless it's bound to an object of that class (or of one of its subclasses).这使得实际上不可能可靠地提取定义了某个“未绑定方法”的类,除非它绑定到该类(或其子类之一)的对象。

Handling bound methods处理绑定方法

So, let us first handle the “easier case“ in which we have a bound method.因此,让我们首先处理“更简单的情况”,其中我们有一个绑定方法。 Note that the bound method must be written in Python , as described ininspect.ismethod 's documentation .请注意,绑定方法必须用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

However, this solution is not perfect and has its perils, as methods can be assigned in runtime, rendering their name possibly different than that of the attribute that they are assigned to (see example below).然而,这个解决方案并不完美并且有其危险,因为可以在运行时分配方法,使它们的名称可能与分配给它们的属性的名称不同(请参见下面的示例)。 This problem exists also in Python 2 .这个问题也存在于Python 2 A possible workaround would be to iterate over all of the class's attributes, looking for one whose identity is that of the specified method.一种可能的解决方法是遍历类的所有属性,寻找身份与指定方法相同的属性。

Handling “unbound methods“处理“未绑定的方法”

Now that we got that out of the way, we can suggest a hack that tries to handle “unbound methods”.现在我们已经解决了这个问题,我们可以建议一个尝试处理“未绑定方法”的黑客。 The hack, its rationale, and some discouragement words can be found in this answer .可以在这个答案中找到黑客,它的基本原理和一些劝阻词。 It relies on manually parsing the __qualname__ attribute , available only from Python 3.3 , is highly unrecommended, but should work for simple cases:它依赖于手动解析__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

Combining both approaches结合两种方法

Since inspect.isfunction and inspect.ismethod are mutually exclusive, combining both approaches into a single solution gives us the following (with added logging facilities for the upcoming examples):由于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

Execution example执行示例

>>> 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'>

So far, so good, but...到目前为止,一切都很好,但是......

>>> 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'>

Final touches最后润色

  • The outcome generated by x and Zy can be partially fixed (to return None ) by verifying that the returned value is a class, before actually returning it.通过在实际返回之前验证返回的值是一个类,可以部分修复xZy生成的结果(返回None )。

  • The outcome generated by Z().z can be fixed by falling back to parsing the function's __qualname__ attribute (the function can be extracted via meth.__func__ ). Z().z生成的结果可以通过回退到解析函数的__qualname__属性来修复(该函数可以通过meth.__func__提取)。

  • The outcome generated by Z.class_meth and Z().class_meth is incorrect because accessing a class method always returns a bound method, whose __self__ attribute is the class itself, rather than its object. Z.class_methZ().class_meth生成的结果是不正确的,因为访问类方法总是返回一个绑定方法,其__self__属性是类本身,而不是它的对象。 Thus, further accessing the __class__ attribute on top of that __self__ attribute doesn't work as expected:因此,在__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'>

    This can be fixed by checking whether the method's __self__ attribute returns an instance of type .这可以通过检查方法的__self__属性是否返回type的实例来解决。 However, this might be confusing when our function is invoked against methods of a metaclass, so we'll leave it as is for now.然而,当我们的函数被元类的方法调用时,这可能会令人困惑,所以我们暂时保持原样。

Here is the final version:这是最终版本:

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

Surprisingly, this also fixes the outcome of Z.class_meth and Z().class_meth which now correctly return Z .令人惊讶的是,这也修复了Z.class_methZ().class_meth现在正确返回Z This is because the __func__ attribute of a class method returns a regular function whose __qualname__ attribute may be parsed:这是因为类方法的__func__属性返回一个常规函数,其__qualname__属性可以被解析:

>>> Z().class_meth.__func__
<function Z.class_meth at 0x7f13b58d4048>
>>> Z().class_meth.__func__.__qualname__
'Z.class_meth'

EDIT:编辑:

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 by returning their __objclass__ attribute (introduced by PEP-252 ), if such exists:根据Bryce提出的问题,可以处理method_descriptor对象,如set.unionwrapper_descriptor对象,如int.__add__ ,只需返回它们的__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__ :但是, inspect.ismethoddescriptor为各自的实例方法对象返回False ,即对于set().unionint().__add__

  • Since int().__add__.__objclass__ returns int , the above if clause may be relinquished in order to solve the problem for int().__add__ .由于int().__add__.__objclass__返回int ,为了解决int().__add__的问题,可以放弃上面的 if 子句。 Unfortunately, this doesn't address the matter of set().union , for which no __objclass__ attribute is defined.不幸的是,这并没有解决set().union ,为此没有定义__objclass__属性。 In order to avoid an AttributeError exception in such a case, the __objclass__ attribute isn't accessed directly, but rather via the getattr function.为了避免在这种情况下出现AttributeError异常,不直接访问__objclass__属性,而是通过getattr函数访问。

EDIT:编辑:

As per the issue raised by x-yuri , it seems that our function fails to handle the method io.BytesIO().__enter__ since inspect doesn't identify it as a method, but rather as a built-in:根据x-yuri提出的问题,我们的函数似乎无法处理io.BytesIO().__enter__方法,因为inspect并未将其识别为方法,而是将其识别为内置方法:

>>> inspect.ismethod(io.BytesIO().__enter__)
False
>>> inspect.isbuiltin(io.BytesIO().__enter__)
True

This is the same issue encountered above in regard to set().union :这与上面在set().union遇到的问题相同:

>>> inspect.ismethod(set().union)
False
>>> inspect.isbuiltin(set().union)
True

Other than this peculiarity, we can handle such methods as ordinary methods and extract the defining class by traversing the MRO.除了这个特性,我们可以像处理普通方法一样处理这些方法,并通过遍历 MRO 来提取定义类。

However, just to be on the safe side, we shall include an extra layer of protection and verify that the __self__ attribute of such methods, if defined, isn't None and that the __class__ attribute of that __self__ object, if defined, isn't None as well:然而,为了安全起见,我们将包含一个额外的保护层,并验证这些方法的__self__属性(如果已定义)不是None并且该__self__对象的__class__属性(如果已定义)不是“ t 也None

if inspect.ismethod(meth) or (inspect.isbuiltin(meth) and getattr(meth, '__self__', None) and getattr(meth.__self__, '__class__', None)):
    # ordinary method handling

Alas, this simple test fails for set().union because bool(set().union.__self__) evaluates to False since set().union.__self__ returns the empty set.唉,这个简单的测试对set().union失败了,因为bool(set().union.__self__)评估为False因为set().union.__self__返回空集。 Thus, an explicit test against None is required, producing the following fix:因此,需要对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

A minor additional patch is advised in order to avoid a possible AttributeError exception when accessing the __func__ attribute during fallback to __qualname__ parsing.为了避免在回__qualname__解析期间访问__func__属性时可能出现的AttributeError异常,建议添加一个小补丁。 This is required since while the __func__ attribute is guaranteed to exist for an ordinary method, it's not necessarily defined for one of the type builtin_function_or_method , such as io.BytesIO().__enter__ and set().union .这是必需的,因为虽然保证__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

EDIT:编辑:

As per the suggestion put forward by user1956611 , it's possible to handle partial objects by introducing a recursive call to seek out the original callable with which the partial object was created:根据user1956611提出的建议,可以通过引入递归调用来查找创建partial对象的原始可调用对象来处理partial对象

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

The point you appear to be missing is, in Python 3 the "unbound method" type has entirely disappeared -- a method, until and unless it's bound, is just a function, without the weird "type-checking" unbound methods used to perform.您似乎缺少的一点是,在 Python 3 中,“未绑定方法”类型已经完全消失了——一个方法,直到并且除非它被绑定,否则只是一个函数,没有用于执行的奇怪的“类型检查”未绑定方法. This makes the language simpler!这使语言更简单!

To wit...:以机智...:

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

and voila -- one less subtle concept and distinction to worry about.瞧——一个不那么微妙的概念和区别需要担心。 Such simplifications are the core advantage of Python 3 wrt Python 2, which (over the years) had been accumulating so many subtleties that it was in danger (if features kept being added to it) of really losing its status as a simple language.这种简化是 Python 3 与 Python 2 相比的核心优势,Python 2(多年来)积累了如此多的微妙之处,以至于它面临着真正失去其作为简单语言的地位的危险(如果功能不断添加到其中)。 With Python 3, simplicity is back !-)使用 Python 3,简单又回来了!-)

Since python 3.6 you could accomplish what you are describing using a decorator that defines a __set_name__ method.从 python 3.6 开始,您可以使用定义__set_name__方法的装饰器来完成您所描述的__set_name__ The documentation states that object.__set_name__ is called when the class is being created. 该文档指出object.__set_name__在创建类时被调用。

Here is an example that decorates a method "in order to register it in a list of methods that serve a particular purpose":这是一个装饰方法的示例,“以便将其注册到服务于特定目的的方法列表中”:

>>> 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

Note that this question is very similar to Can a Python decorator of an instance method access the class?请注意,这个问题与Can a Python decorator of an instance method access the class非常相似 and therefore my answer as well to the answer I provided there .因此我的回答以及我在那里提供的答案

A small extension for python 3.6 (python 2.7 worked fine) to the great answer of https://stackoverflow.com/a/25959545/4013571 python 3.6的一个小扩展(python 2.7工作正常)对https://stackoverflow.com/a/25959545/4013571的很好的回答

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

I found the following adjustment was required for doctest我发现doctest需要进行以下调整

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

As for some reason, when using nose the inspect.getmodule(meth) didn't contain the defining class由于某种原因,当使用noseinspect.getmodule(meth)不包含定义类

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM