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

Posted

技术标签:

【中文标题】在 Python 3 中获取未绑定方法对象的定义类【英文标题】:Get defining class of unbound method object in Python 3 【发布时间】:2011-04-05 02:18:14 【问题描述】:

假设我想为类中定义的方法制作一个装饰器。我希望该装饰器在被调用时能够在定义该方法的类上设置一个属性(以便将其注册到服务于特定目的的方法列表中)。

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

编辑:我刚刚找到this question。这使得看起来最好的解决方案就是像我一样避免它。我仍然想知道为什么它被删除了。

【问题讨论】:

这里记录了未绑定方法的删除:docs.python.org/py3k/whatsnew/… Guido van Rossum 删除未绑定方法的理由可以在这里找到:mail.python.org/pipermail/python-dev/2005-January/050625.html,该帖子中提到的博客在这里:artima.com/weblogs/viewpost.jsp?thread=86641 感谢大家。这正是我想要的。 您链接到的问题中描述的解决方案(使用装饰器仅标记函数,然后使用类装饰器在事后修改标记的函数)具有一些不错的优势。它是明确的,它不依赖于任何棘手或不知名的东西,它保证可以在任何版本的 Python 中工作(嗯,你需要装饰器……但即使没有它们,spam = deco(spam) 也可以),它的灵活性适用于广泛一系列相似但不相同的问题,…… @PavelPatrin 你说得对,看起来它只能用作decorator(Foo.bar),而不是@decorator。我可能做了一些稍微不同的事情,但是已经六年了,无论如何它从来没有在我想要的地方工作过,所以谁知道呢。 【参考方案1】:

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

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

TL;DR

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

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

对于绑定方法,它只是简单地遍历MRO,其方式与accepted answer to an equivalent question for Python 2 中类似。 对于“未绑定方法”,它依赖于解析其qualified name,即available only from Python 3.3,并且非常鲁莽(如果不需要此功能,最好删除此代码块并返回None )。

一些有用的 cmets 引发了额外的更改,如下面的编辑部分所述,产生了以下改进:

对通过描述符定义的、未被归类为普通方法或函数的方法(例如 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

一个小请求

如果您决定使用此实现并遇到任何警告,请发表评论并描述发生的情况。


完整版

“未绑定方法”是常规函数

首先,值得注意的是以下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.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__ 属性是类本身,而不是它的对象。因此,在 __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__ 属性(由@ 引入987654334@),如果存在:

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

但是,inspect.ismethoddescriptor 会为各自的实例方法对象返回False,即set().unionint().__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)

【讨论】:

这将@staticmethod 标识为具有此基类的方法。他们真的不是。一旦你有了这个类,你应该检查不是 isinstance(cls.__dict__[meth.__name__], staticmethod)。您是否还应该检查 classmethod 是模棱两可的。 (使用 dict 的目的是避开由 get 完成的替换)。在这种情况下,我认为您不需要扫描 mro,因为定义实际上将位于 qualname 指示的任何类中,但我并不完全确定。 (这种见解来自新的“inspect.getattr_static”方法实现) 另外,回复:编辑。 set().union 是绑定的。它有一个自我。所以你可以使用它的类型作为声明类。并且 set.union 有一个 objclass @jobermark,感谢您的 cmets,但我不太了解。您能否提供一个示例,对于 @staticmethod@classmethod,返回的类不正确?此外,关于set().unionint().__add__,我宁愿避免尝试获取其“self 对象”,除非我确定我正在处理绑定方法。 该函数采用未绑定的方法... set().__add__ 是绑定的方法。而 set.__add__ 现在在 Python 3.7 中有一个 objclass。所以评论中的这种特殊情况已经消失了。 @studioj,我实际上指的是io.BytesIO().__enter__set().union,它们的类型为builtin_function_or_method,并且没有__func__ 属性。感谢您提出这个问题,促使我在回答中澄清这一点。【参考方案2】:

您似乎缺少的一点是,在 Python 3 中,“未绑定方法”类型已完全消失——一个方法,除非它被绑定,否则只是一个函数,没有奇怪的“类型检查”未绑定方法用来表演的。这使语言更简单!

也就是说……

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

瞧——一个不那么微妙的概念和需要担心的区别。这样的简化是 Python 3 wrt Python 2 的核心优势,它(多年来)积累了如此多的微妙之处,以至于它面临着真正失去其作为 simple 的地位的危险(如果不断向其中添加功能) 语言。在 Python 3 中,简单性回归了返回!-)

【讨论】:

这是有道理的。我什至没有意识到像第一个参数类型检查这样的方法还有额外的功能——我只是认为它们是带有一些额外属性的普通函数。有了这个,我可以完全理解删除了。 @Tim,是的,检查第一个参数是 .im_class 的实例(当然包括其子类)是为什么 im_class 被保留在首位(同样包裹在函数周围的间接层以生成未绑定的方法,因为无论如何都需要包裹以生成绑定的方法)。底层函数始终是(并且仍然适用于绑定方法).im_func,顺便说一句,从来没有方法对象本身。 "有了 Python 3,简单又回来了!"听起来它会成为一个很好的保险杠贴纸(或笔记本电脑贴纸)。 有人介意解释一下这是如何回答蒂姆的问题的吗?谢谢! @GershomMaes,如果问题是“我如何获得独角兽的定义类”,你真的看不出“独角兽不存在”是如何回答的吗?问题是“我如何在 Python 3 中获得未绑定方法的定义类”,显然“Python 3 中的未绑定方法不存在”以完全相同的方式回答它:显然,您不能“获得定义类“(或任何其他特征)的东西就是不存在的!-)【参考方案3】:

从 python 3.6 开始,您可以使用定义 __set_name__ 方法的装饰器来完成您所描述的内容。 The documentation 声明 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

请注意,这个问题与Can a Python decorator of an instance method access the class? 非常相似,因此我对the answer I provided there 的回答也是如此。

【讨论】:

小心这种方法,因为装饰器类 (particular_purpose) 将返回 itselfnot 函数,这意味着任何装饰器“上面”它会错误地接收到这个对象并可能发生故障。如果您要标记然后在类中进行后处理功能,我建议使用类装饰器(如图所示here)。【参考方案4】:

python 3.6 的小扩展(python 2.7 运行良好)https://***.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

我发现doctest需要进行以下调整

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

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

【讨论】:

以上是关于在 Python 3 中获取未绑定方法对象的定义类的主要内容,如果未能解决你的问题,请参考以下文章

术语:用户定义的函数对象属性?

python----03(面向对象进阶02)

将未绑定的python函数存储在类对象中

类方法:绑定或无绑定

python基础类的定义与使用

如果我只知道 python 中的绑定方法,如何获取类的名称? [复制]