获取 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。不过,我还是很好奇有没有替代Python 2的功能,如果没有的话,为什么会完全去掉它。
编辑:我刚找到这个问题。这让我觉得最好的解决办法就是像我一样避免使用它。不过,我还是在想为什么会把它去掉。
5 个回答
从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实例方法装饰器是否可以访问类的问题非常相似,因此我在那里提供的答案也是相关的。
我觉得写一些能最好地猜测定义类的东西是值得的。为了完整性,这个回答也涉及到绑定方法。
最糟糕的情况是,猜测应该完全失败,函数返回 None
。但是,无论如何,它不应该引发异常或返回错误的类。
总结
我们函数的最终版本成功处理了大多数简单情况,还有一些小问题。
简单来说,它的实现区分了绑定方法和“未绑定方法”(函数),因为在 Python 3
中,没有可靠的方法可以从“未绑定方法”中提取出包含的类。
- 对于绑定方法,它简单地遍历
MRO
,方式类似于Python 2的等效问题的接受答案。 - 对于“未绑定方法”,它依赖于解析其限定名称,这个特性仅在
Python 3.3
中可用,而且相当冒险(如果这个特性不必要,最好删除这段代码,直接返回None
)。
一些有用的评论促使了额外的修改,具体细节在下面的编辑部分,产生了以下改进:
- 对通过描述符定义的方法的有限处理,这些方法不被归类为普通方法或函数(例如,
set.union
、int.__add__
和int().__add__
)以及对内置方法的处理(例如set().union
和io.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.isfunction
和 inspect.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'>
最后的调整
通过验证返回值是否为类,可以部分修复
x
和Z.y
生成的结果(返回None
)。通过回退到解析函数的
__qualname__
属性,可以修复Z().z
生成的结果(可以通过meth.__func__
提取函数)。由于访问类方法总是返回一个绑定方法,其
__self__
属性是类本身,而不是它的对象,因此Z.class_meth
和Z().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_meth
和 Z().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().union
和 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
,并且该 __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)
你似乎忽略了一个重点,那就是在Python 3中,“未绑定方法”这种类型完全消失了。一个方法,除非被绑定,否则它只是一个函数,没有那些奇怪的“类型检查”未绑定方法的麻烦。这让语言变得更简单了!
举个例子...:
>>> class X:
... def Y(self): pass
...
>>> type(X.Y)
<class 'function'>
看,这样一来,我们就少了一个需要担心的复杂概念和区别。这样的简化是Python 3相对于Python 2的核心优势,因为Python 2在这些年里积累了太多复杂的细节,如果继续添加新功能,它真的有可能失去作为一种简单语言的地位。而在Python 3中,简单性又回来了!-)