屬性名稱空間

April 25, 2022

到目前為止,你已經看過幾種可以作為名稱空間的地方了?應該馬上會想到模組是其中之一,而在剛剛知道,類別也可以作為名稱空間使用,除此之外呢?

物件作為名稱空間

除此之外呢?一個可作為名稱空間的對象,都是一個物件,其實每個模組匯入後,都會是一個物件,是 module 類別的實例,每個類別也會是一個物件,是 type 類別的實例。

如果必要的話,一個自定義的類別實例,也可以作為名稱空間。例如:

>>> class Namespace:
...     pass
...
>>> ns = Namespace()
>>> ns.some = 'Just a value'
>>> ns.other = 'Just another value'
>>>

在上面的例子中,ns 參考的物件,不就是作為 someother 的名稱空間嗎?某些程度的意義上,類別的實例,確實是作為屬性的名稱空間。

在一些語言中,本身沒有提供名稱空間的機制,像是 ECMAScript 6 前的 JavaScript,為了管理名稱,就有不少開發者使用物件來實作出類似的機制。

__dict__ 屬性

每個物件本身,都會有個 __dict__ 屬性,當中記錄著類別或實例所擁有的特性。例如:

>>> class Some:
...     def __init__(self, x):
...         self.x = x
...     def add(self, y):
...         return self.x + y
...
>>> s = Some(10)
>>> s.__dict__
{'x': 10}
>>> Some.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function Some.__init__ at 0x000002087EF234C0>, 'add': <function Some.add at 0x000002087EF23550>, '__dict__': <attribute '__dict__' of 'Some' objects>, '__weakref__': <attribute '__weakref__' of 'Some' objects>, '__doc__': None})
>>>

在這邊可以看到,真正屬於實例的屬性其實只有 xSome 中定義的 add,其實是屬於類別,這也可用來解釋,當呼叫 s.add(10) 時,為何效果相當於 Some.add(s, 10),實際上就是透過 Some 類別呼叫了 add 方法。

在〈字典〉談過,在 Python 3.5 以前,不保證字典建立時鍵的順序,Python 3.6 開始,會以字面值時每對鍵值的撰寫順序,或者 dict 的安插順序來作為 itemskeysvalues 的迭代順序,並且這在 Python 3.7 成為正式特性,這對於 __dict__ 來說也是,Python 3.7 以後,你對物件建立屬性的順序,就是 __dict__ 中鍵的順序。

在 Python 中,兩個底線的方法是不建議直接呼叫的,若想取得 __dict__ 的資料,可以使用 vars 函式。例如:

>>> class Ball:
...     PI = 3.14159  # 屬於類別的資料
...
>>> vars(Ball)
mappingproxy({'__dict__': <attribute '__dict__' of 'Ball' objects>, '__weakref__': <attribute '__weakref__' of 'Ball' objects>, 'PI': 3.14159, '__doc__': None, '__module__': '__main__'})
>>> ball = Ball()
>>> vars(ball)
{}
>>> Ball.PI
3.14159
>>> ball.PI
3.14159
>>>

在這邊的 Ball 類別中,直接定義了一個 PI 變數,從 vars(Ball) 的結果可以看到,這樣的變數屬於 Ball 類別,而不是 ball 參考的實例,這類變數是以類別作為名稱空間,因此建議透過類別名稱來存取。

然而,確實也能透過 ball.PI 這樣的方式來取得,當一個實例上找不到對應的屬性時,會尋找實例的類別,看看上頭有沒有對應的屬性,如果有就可以取用,若沒有就會發生 AttributeError

更細部的流程其實是,如果嘗試透過實例取得屬性,而實例的 __dict__ 沒有,會到產生實例的類別之 __dict__ 尋找,若類別的 __dict__ 仍沒有,則會試著呼叫 __getattr__ 來取得,若沒有定義 __getattr__ 方法,就會發生 AttributeError

為什麼一再強調,若函式或變數以類別為名稱空間,建議透過類別名稱來呼叫或存取?一來語義上比較清楚,一眼就可以看出函式或變數是以類別為名稱空間,二來還可以避免以下的問題:

>>> ball.PI = 3.14
>>> ball.PI
3.14
>>> Ball.PI
3.14159
>>>

這邊的操作接續了上一個 REPL 的示範,雖然一個實例上找不到對應的屬性時,會尋找實例的類別,看看上頭有沒有對應的屬性,如果有就可以取用,然而,如果在這樣的實例上指定屬性值時,會直接在實例上建立屬性,而不是修改實例的類別上對應的屬性。

既然自定義的型態,可以在建構出來的實例上,直接新增屬性,那麼可不可以在類別上直接新增方法呢?答案是可以的!

>>> class Account:
...     pass
...
>>> acct = Account()
>>> acct.name = 'Justin'
>>> acct.number = '123-4567'
>>> acct.balance = 1000
>>> def deposit(self, amount):
...     self.balance += amount
...
>>> Account.deposit = deposit
>>> acct.deposit(500)
>>> acct.balance
1500
>>>

可以看到,就算新增的方法是在實例建構之後,透過實例呼叫方法時仍是可以生效的。

del 刪除屬性

del 可以用來刪除變數,或者已匯入目前模組的名稱(本質上也是個變數),它也可以用來刪除某個物件上的屬性。例如:

>>> class Some:
...     def __init__(self, x):
...         self.x = x
...     def add(self, y):
...         return self.x + y
...
>>> s = Some(10)
>>> s.x
10
>>> del s.x
>>> s.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Some' object has no attribute 'x'
>>> del Some.add
>>> Some.add
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Some' has no attribute 'add'
>>>

由於模組也是個物件,因此,也可以使用 del 來刪除模組上定義的名稱。例如:

>>> import math
>>> math.pi
3.141592653589793
>>> del math.pi
>>> math.pi
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'math' has no attribute 'pi'
>>>

其實 del 真正的作用,是刪除某物件上的指定屬性。舉例來說,在全域範圍建立變數時,就是在當時的模組物件上建立屬性,而在全域範圍使用 del 刪除變數時,就是從當時的模組物件上刪除屬性。

每個模組都會有個 __name__ 屬性,一個模組被 import 時,__name__ 屬性會被設定為模組名稱,直接使用 python 指令執行某模組時,__name__ 屬性會被設定為 '__main__',無論如何,想要取得目前的模組物件,可以使用 sys.modules[__name__] 來取得。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter