特性名稱空間


一個事實是:類別(Class)或實例(Instance)本身的作用是作為特性(Property)的名稱空間(Namespace)。類別或實例本身會擁有一個__dict__特性參考至一個字典物件,其中記錄著類別或實例所擁有的特性。例如:
>>> class Math:
...     PI = 3.14159
...
>>> Math.PI
3.14159
>>> print(Math.__dict__)
{'__dict__': <attribute '__dict__' of 'Math' objects>, '__module__': '__main__',
 'PI': 3.14159, '__weakref__': <attribute '__weakref__' of 'Math' objects>, '__d
oc__': None}
>>> Math.__dict__['PI']
3.14159
>>>


在上例中,Math類別上定義了PI特性,這記錄在Math.__dict__中,你嘗試使用Math.PI,則使用Math.__dict__['PI']來尋找出對應的值。如果你試著透過實例來取得PI:
>>> m = Math()
>>> m.PI
3.14159
>>> print(m.__dict__)
{}
>>>


實際上m所參考的實例,其__dict__中並沒有PI,此時會到Math.__dict__中找看看有無PI。這是Python中尋找特性的順序:如果實例的__dict__中沒有,則到產生實例的類別__dict__中尋找。如果你試著在m實例上設定PI特性:
>>> m.PI
3.14
>>> Math.PI
3.14159
>>> print(m.__dict__)
{'PI': 3.14}
>>> print(Math.__dict__)
{'__dict__': <attribute '__dict__' of 'Math' objects>, '__module__': '__main__',
 'PI': 3.14159, '__weakref__': <attribute '__weakref__' of 'Math' objects>, '__d
oc__': None}
>>>


實際上你並沒有改變Math.__dict__中的PI,而是在實例m.__dict__中新增一個PI,而你嘗試使用實例存取PI時,由於m.__dict__中已經有了,就直接取得該值。

這也說明了,為什麼實例方法的第一個參數會綁定至實例:
>>> class Some:
...     def setx(self, x):
...         self.x = x
...
>>> s = Some()
>>> print(Some.__dict__)
{'__dict__': <attribute '__dict__' of 'Some' objects>, '__weakref__': <attribute
 '__weakref__' of 'Some' objects>, '__module__': '__main__', 'setx': <function s
etx at 0x018FA078>, '__doc__': None}
>>> print(s.__dict__)
{}
>>> s.setx(10)
>>> print(s.__dict__)
{'x': 10}
>>>


類別中所定義的函式,其實就是類別的特性,也就是在類別的__dict__中可以找到該名稱。實例方法的第一個參數self綁定實例,透過self.x來設定特性值,也就是在self.__dict__中添增特性。

由於Python可以動態地為類別添加屬性,即使是未添加屬性前就已建立的物件,在類別動態添加屬性之後, 也可依Python的名稱空間搜尋順序套用上新的屬性,用這種方式,您可以為類別動態地添加方法。例如:
class Some:
def __init__(self, x):
self.x = x

s = Some(1)
Some.service = lambda self, y: print('do service...', self.x + y)
s.service(2) # do service... 3

如果你要刪除物件上的某個特性,則可以使用del。例如:
>>> class Some:
...     pass
...
>>> s = Some()
>>> s.x = 10
>>> print(s.__dict__)
{'x': 10}
>>> del s.x
>>> print(s.__dict__)
{}
>>>

如果你試著在實例上呼叫某個方法,而該實例上沒有該綁定方法時(被@staticmethod或@classmethod修飾的函式),則會試著去類別__dict__中尋找,並以類別呼叫方式來執行函式。例如:
>>> class Some:
...     @staticmethod
...     def service():
...         print('XD')
...
>>> s = Some()
>>> s.service()
XD
>>>


在上例中,嘗試執行s.service(),由於s並沒有service()的綁定方法(因為被@staticmethod修飾),所以嘗試尋找Some.service()執行。

實際上,如果嘗試透過實例取得某個特性,如果實例的__dict__中沒有,則到產 生實例的類別__dict__中尋找,如果類別__dict__仍沒有,則會試著呼叫__getattr__()來傳回,如果沒有定義 __getattr__()方法,則會引發AttributeError,如果有__getattr__(),則看__getattr__()如何處理。例如:
>>> class Some:
...     w = 10
...     def __getattr__(self, name):
...         if name == 'w':
...             return 20
...
>>> s = Some()
>>> s.w
10
>>> s.x
>>> class Some:
...     def __getattr__(self, name):
...         if name == 'w':
...             return 20
...         else:
...             raise AttributeError(name)
...
>>> s = Some()
>>> s.w
20
>>> s.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __getattr__
AttributeError: x
>>>


在類別中的函式執行過程中若有定義實例特性時,具特性名稱是以__開頭,則該名稱會被加工處理。例如:
>>> class Some:
...     def __init__(self):
...         self.__x = 10
...
>>> s = Some()
>>> s.__x

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Some' object has no attribute '__x'
>>> print(s.__dict__)
{'_Some__x': 10}
>>> s._Some__x
10
>>>


實例變數若以__name這樣的名稱,則會自動轉換為「_類別名__name」這樣的名稱儲存在實例的__dict__中,以__開頭的變數名稱,Python沒有真正阻止你存取它,但這提示不希望你直接存取。

如果不想要直接使用實例的__dict__來取得特性字典物件,則可以使用vars(),vars()會代為呼叫實例的__dict__。例如:
>>> class Some:
...     def __init__(self):
...         self.x = 10
...         self.y = 20
...
>>> s = Some()
>>> vars(s)
{'y': 20, 'x': 10}
>>>