前言

去年我寫過一篇 你用對 hasattr 了嘛? 介紹過被 property 裝飾的方法內部拋錯會引起 hasattr 的結果爲 False。

今天又遇到了一個 AttributeError 向上傳播的問題 (Python 2),一起來看看

問題

先上代碼:

In : class T(object):
...:     @property
...:     def name(self):
...:         print(self.missing_attribute)
...:         return 42
...:     def __getattr__(self, name):
...:         raise AttributeError(name)
...:

In : t = T()

In : t.name
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-afc812d51b9a> in <module>()
----> 1 t.name

<ipython-input-4-a0ba2d3a6446> in __getattr__(self, name)
      5         return 42
      6     def __getattr__(self, name):
----> 7         raise AttributeError(name)
      8

AttributeError: name

In : [k for k in dir(t) if not k.startswith('_')]
Out: ['name']

這個例子需要細品,明明有 name 這個屬性卻拋出了 AttributeError。分析一下:

  1. 通過 __getattr__ 定製屬性查找的邏輯,當用戶試圖獲取一個不存在的屬性時就會拋 AttributeError 錯誤
  2. 獲取 name 屬性的邏輯中包含 print (self.missing_attribute) 這句,由於沒有這個屬性會執行到 __getattr__ 然後拋 AttributeError

一看到這個違背常理的錯誤,我就想起來 hasattr 的那個問題,覺得應該是類似的原因。網上一搜果然之前有人已經給 CPython 提過 issue 了,具體可以看延伸閱讀鏈接。總結一下,這個問題的出現需要有 2 個點都滿足:

__getattr__

相當於方法中的 AttributeError 沒有被處理,傳播到 __getattr__ 了。再看一個例子:

In : class T(object):
...:     @property
...:     def name(self):
...:         raise AttributeError('This message will not be displayed!')
...:         return 'Hello'
...:     def __getattr__(self, name):
...:         return 0
...:

In : t = T()

In : t.name
Out: 0

一旦 name 方法中由於各種原因會拋 AttributeError 錯誤 (別的錯誤不行),就會走 __getattr__ 裏面的邏輯。

怎麼解決?

這不是一個 BUG,但是代碼編寫者不能規避這類問題,怎麼辦呢?最簡單的辦法是使用 __getattribute__ 替代 __getattr__ ,演示一下:

In : class T(object):
...:     @property
...:     def name(self):
...:         raise AttributeError('This message will not be displayed!')
...:         return 'Hello'
...:
...:     def __getattribute__(self, name):
...:         try:
...:             return super(T, self).__getattribute__(name)
...:         except AttributeError as e:
...:             raise e
...:

In : t = T()

In : t.name
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-28-afc812d51b9a> in <module>()
----> 1 t.name

<ipython-input-26-a27dd1330f44> in __getattribute__(self, name)
      9             return super(T, self).__getattribute__(name)
     10         except AttributeError as e:
---> 11             raise e
     12

AttributeError: This message will not be displayed!

In : class T(object):
...:     @property
...:     def name(self):
...:         print(self.missing_attribute)
...:         return 42
...:     def __getattribute__(self, name):
...:         try:
...:             return super(T, self).__getattribute__(name)
...:         except AttributeError as e:
...:             raise e
...:

In : t = T()

In : t.name
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-31-afc812d51b9a> in <module>()
----> 1 t.name

<ipython-input-29-da47b1a1e093> in __getattribute__(self, name)
      8             return super(T, self).__getattribute__(name)
      9         except AttributeError as e:
---> 10             raise e
     11

AttributeError: 'T' object has no attribute 'missing_attribute'

就是這樣~

延伸閱讀

  1. https://bugs.python.org/issue24983
相關文章