一个在交流群里讨论过两轮的问题,答案竟然跟一个 PEP 有关

用户头像
Python猫
关注
发布于: 2020 年 09 月 05 日
一个在交流群里讨论过两轮的问题,答案竟然跟一个 PEP 有关



Python 中有没有办法通过类方法找到其所属的类?



这个问题看起来不容易理解,我可以给出一个例子:



class Test:
  @xxx
  def foo(self):
      pass

现在有一个类和一个类方法,其中类方法上有一个装饰器。



我们的问题就是要在装饰器代码中动态地获得 Test 这个类(类名+类对象)。



去年 11 月份的时候,我在微信读者群里提出了这个问题,当时引起了小范围的讨论。



没想到在今年上个月的时候,群里又有人提了同样的问题(我在讨论结束后才看到),而且最终都找到了 stackoverflow 上一个同样的问题:





stackoverflow 上的问题提得很明确:Get defining class of unbound method object in Python 3 。但是 unbound method 的叫法已经不常见了,详细的讨论也就不展开了,感兴趣的同学可以去查阅。



这个问题的关键是要使用在 Python 3.3 中引入的__qualname__ 属性,通过它可以获取上层类的名称。



铺垫了这么多,开始进入本文的正题了:__qualname__ 属性是什么东西?为什么 Python 3 要特别引入它呢?



下文是 PEP-3155 的翻译摘录,清楚地说明了这个属性的来龙去脉。



完整内容可在 Github 仓库查看:https://github.com/chinesehuazhou/peps-cn/blob/master/StandardsTrack/3155--%E7%B1%BB%E5%92%8C%E6%96%B9%E6%B3%95%E7%9A%84%E7%89%B9%E5%AE%9A%E5%90%8D%E7%A7%B0.md



-------------------摘录开始--------------------



原理



一直以来,对于嵌套类的自省,Python 的支持很不够。给定一个类对象,根本不可能知道它是在某个类中定义的,还是在顶层模块中定义的;而且,如果是前者,也不可能知道它具体是在哪个类中定义的。虽然嵌套类通常被认为是不太好的用法,但这不应该成为不支持内层自省的理由。



Python 3 因为丢弃了以前的未绑定方法(unbound method),而受到了侮辱性的伤害。



在 Python 2 中,给出以下定义:



class C:
  def f():
      pass

你可以从C.f 对象中获得其所属的类:



>>> C.f.im_class
<class '__main__.C'>

这种用法在 Python 3 中已经没有了:



>>> C.f.im_class
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'im_class'
>>> dir(C.f)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__get__', '__getattribute__',
'__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__',
'__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__']

这就限制了用户可以使用的自省能力。当将程序移植到 Python 3 时,它可能会产生一些实际的问题,例如在 Twisted 的核心代码中,就多次使用到了这种自省方法。此外,这还限制了对 pickle 序列化的支持



提议



本 PEP 提议在函数和类中添加 __qualname__ 属性。



对于顶层的函数和类,__qualname__ 属性等于__name__ 属性。对于嵌套的类、方法和嵌套函数,__qualname__ 属性包含一个点式路径(dotted path),通向顶层模块下的对象。函数的局部命名空间在点式路径中由名为 <locals> 的组件表示。



函数和类的 repr() 和 str() 被修改为使用__qualname__ 而不再是__name__。



嵌套类的示例



>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

嵌套函数的示例



>>> def f():
...   def g(): pass
...   return g
...
>>> f.__qualname__
'f'
>>> f().__qualname__
'f.<locals>.g'

不足之处



对于嵌套函数(以及在函数内部定义的类),由于无法从外部获得函数的命名空间,因此点式路径无法以动态编程的方式遍历。相比于空的__name__,它对于人类读者还是有些帮助的。



跟__name__属性一样,__qualname__ 属性是静态计算的,不会自动地重新绑定。



讨论



去除模块名称



跟__name__一样,__ qualname__ 不包含模块的名称。这使得它不受制于模块别名和重新绑定,也得以在编译期进行计算。



恢复 unbound 方法



恢复 unbound 方法只能解决此 PEP 解决了的部分问题,而且代价更高(额外的对象类型和额外的间接寻址,不如用额外的属性)。



-------------------摘录结束--------------------



后记



去年我在阅读ddt 库关于参数化测试的源码 时,偶然想到了文章开头的问题,但是没有作进一步的梳理(似乎感兴趣的人也不多)。没想到的是在群里又出现了同样的讨论,这让我意识到这个问题是有价值的。



前几天,我偶然间发现__qualname__ 属性有一个专门的 PEP,所以我就抽空把它翻译出来了——既是一种知识梳理,也是给大家做一个“科普”吧。说不定什么时候,还有人会遇到同样的问题呢,希望对大家有所帮助。



更多的 PEP 中文翻译内容,可在 Github 查阅:https://github.com/chinesehuazhou/peps-cn



发布于: 2020 年 09 月 05 日 阅读数: 24
用户头像

Python猫

关注

Python猫作者,一个Python爱好者 2018.08.07 加入

历史文章会陆续搬运过来,欢迎大家在微信搜索“Python猫”,也可以找到我

评论

发布
暂无评论
一个在交流群里讨论过两轮的问题,答案竟然跟一个 PEP 有关