屬性名稱空間
April 25, 2022到目前為止,你已經看過幾種可以作為名稱空間的地方了?應該馬上會想到模組是其中之一,而在剛剛知道,類別也可以作為名稱空間使用,除此之外呢?
物件作為名稱空間
除此之外呢?一個可作為名稱空間的對象,都是一個物件,其實每個模組匯入後,都會是一個物件,是 module
類別的實例,每個類別也會是一個物件,是 type
類別的實例。
如果必要的話,一個自定義的類別實例,也可以作為名稱空間。例如:
>>> class Namespace:
... pass
...
>>> ns = Namespace()
>>> ns.some = 'Just a value'
>>> ns.other = 'Just another value'
>>>
在上面的例子中,ns
參考的物件,不就是作為 some
與 other
的名稱空間嗎?某些程度的意義上,類別的實例,確實是作為屬性的名稱空間。
在一些語言中,本身沒有提供名稱空間的機制,像是 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})
>>>
在這邊可以看到,真正屬於實例的屬性其實只有 x
,Some
中定義的 add
,其實是屬於類別,這也可用來解釋,當呼叫 s.add(10)
時,為何效果相當於 Some.add(s, 10)
,實際上就是透過 Some
類別呼叫了 add
方法。
在〈字典〉談過,在 Python 3.5 以前,不保證字典建立時鍵的順序,Python 3.6 開始,會以字面值時每對鍵值的撰寫順序,或者 dict
的安插順序來作為 items
、keys
、values
的迭代順序,並且這在 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__]
來取得。