1. 程式人生 > 實用技巧 >一個在交流群裡討論過兩輪的問題,答案竟然跟一個 PEP 有關

一個在交流群裡討論過兩輪的問題,答案竟然跟一個 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