python屬性管理(1):基礎
管理屬性的幾種方式
在python中訪問、設定、刪除物件屬性的時候,有以下幾種方式:
- 使用內建函式getattr()、setattr()和delattr()
- 自己編寫
getter()
、setter()
、deleter()
方法
- 過載
__getattr__()
、__setattr__()
、__delattr__()
運算子,這決定了x.y
的訪問、賦值方式以及del x.y
的方式
- 使用
__getattribute__()
方法
- 使用描述符協議
- 使用property協議,它是一種特殊的描述符
本文簡單介紹其中的前4種作為基礎,後面使用單獨的文章解釋後2種。
內建函式XXXattr()管理屬性
通過內建函式getattr()、setattr()、delattr()能簡單訪問、設定、刪除物件上的屬性。
先看看它們的幫助文件:
getattr(...) getattr(object, name[, default]) -> value Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y. setattr(obj, name, value, /) Sets the named attribute on the given object to the specified value. setattr(x, 'y', v) is equivalent to ``x.y = v'' delattr(obj, name, /) Deletes the named attribute from the given object. delattr(x, 'y') is equivalent to ``del x.y''
用法很簡單,給定要操作的物件obj以及要操作的屬性名稱name。對於getattr()來說,如果要操作的屬性不存在預設會報錯,可以給定一個default引數表示屬性不存在時返回該給定屬性值。
例如,下面是一個簡單的Person類和物件p:
class Person():
def __init__(self, name):
self.name = name
p = Person("malongshuai")
使用getattr()獲取name屬性和不存在的age屬性:
print(getattr(p, "name")) print(getattr(p, "age", 23))
上面訪問age屬性時,如果把第三個引數"23"去掉,將丟擲異常。
AttributeError: 'Person' object has no attribute 'age'
使用setattr()和delattr()設定和刪除屬性:
setattr(p, "age", 25)
print(p.__dict__)
delattr(p, "age")
print(p.__dict__)
返回結果:
{'name': 'malongshuai', 'age': 25}
{'name': 'malongshuai'}
自己編寫accessor方法
一般面向物件的語言都是自己寫setter、getter、deleter方法來管理屬性的,通用又安全,但是管理起來並不那麼方便。
這裡僅介紹一下,它們更好的寫法參考:python設定物件屬性。
例如,在Person類中加上name、age這兩個屬性的accessor方法:
class Person():
def __init__(self, name):
self.name = name
def set_name(self,name): self.name = name
def get_name(self): return self.name
def del_name(self): del self.name
def set_age(self,age): self.age = age
def get_age(self): return self.age
def del_age(self): del self.age
缺點是很明顯的,對於想要管理的每個屬性,都得去定義這些屬性。也就是說,accessor方法是針對單個屬性的。
運算子過載管理屬性
通常可以直接使用點號運算來訪問、設定屬性。例如:
p.name # (1)訪問p物件的name屬性
p.name = "abc" # (2)為p物件的name屬性賦值
del p.name # (3)刪除p物件的name屬性
先說物件的賦值和刪除操作,也就是上面的(2)和(3)。這兩種操作可以直接被__setattr__()
、__delattr__()
這兩個方法攔截,或者說只要重寫了這兩個方法,每當對屬性賦值、刪除時,都會呼叫對應的這兩個方法。
再說訪問屬性的操作(1),python提供了兩個對應的方法__getattr__()
和__getattribute__()
。前者是在訪問不存在的屬性時被自動呼叫的,後者則是訪問屬性時被呼叫的,它無視屬性是否存在。
這裡提前說一個稍後要遇到的問題總結:對於適用於所有屬性操作的__setattr__
、__delattr__
和__getattribute__
方法,要避免它們的無限遞迴。參考後面的示例即可知。
__getattr__()
__getattr__()
是通過點號訪問不存在屬性時被呼叫的。它有兩個使用標準:要麼返回屬性值,要麼丟擲異常。
例如:
class Person():
def __init__(self, name):
self.name = name
def __getattr__(self, attrname):
if attrname == "name":
print("in getattr1")
return self.name
elif attrname == "age":
print("in getattr2")
return 25
else:
print("in getattr3")
raise AttributeError(attrname)
p = Person("malongshuai")
上面的Person類帶有屬性name,所以訪問name屬性的時候不會呼叫__getattr__()
,而訪問age或其它屬性時會呼叫該方法,只不過age屬性有自定義的返回值,其它屬性則報錯。
print(p.name)
print(p.age)
print(p.job)
以下是輸出結果:
malongshuai
in getattr2
25
in getattr3
Traceback (most recent call last):
File "g:/pycode/b.py", line 21, in <module>
print(p.job)
File "g:/pycode/b.py", line 14, in __getattr__
raise AttributeError(attrname)
AttributeError: job
__getattribute__()
__getattribute__()
和__getattr__()
類似,不同的是它前者適用於所有屬性的訪問,而不管目標屬性是否存在。
需要注意的是,__getattribute__()
適用於所有屬性訪問操作,所以要避免無限遞迴。例如,下面是錯誤的寫法:
def __getattribute__(self, attr):
return self.attr
因為這個方法中的self.attr
會繼續觸發__getattribute__
的呼叫,從而出現無限遞迴問題。
解決辦法是通過父類來訪問,比如super()或object類。
super().__getattribute__(attr)
object.__getattribute__(self, attr)
__getattribute__()
的優先順序高於__getattr__()
,前者存在的時候不會呼叫到後者,除非前者的程式碼中呼叫了後者,或者前者丟擲了異常。
例如:
class Person():
def __init__(self, name):
self.name = name
def __getattribute__(self, attr):
print("in getattribute")
return object.__getattribute__(self, attr)
# return super.__getattribute__(attr)
def __getattr__(self, attrname):
if attrname == "name":
print("in getattr1")
return self.name
elif attrname == "age":
print("in getattr2")
return 25
else:
print("in getattr3")
raise AttributeError(attrname)
p = Person("malongshuai")
print(p.name)
print(p.age)
返回結果:
in getattribute
malongshuai
in getattribute
in getattr2
25
上面輸出了name和age兩個屬性,但是輸出"p.age"的時候該屬性不存在,於是__getattribute__
丟擲異常,然後觸發__getattr__
。
需要注意的是,在解決無限遞迴問題上,後面的__setattr__
和__delattr__
還會有一種訪問__dict__
的方式,這不適合於這裡的__getattribute__
,因為訪問這個字典也會觸發__getattribute__
從而繼續導致無限遞迴。
__setattr__()
__setattr__()
用來攔截物件屬性賦值操作。例如:
p.name = "long"
會轉換為呼叫p.__setattr__(self,name,"long")
。
唯一需要注意的是避免賦值時的無限遞迴問題。因為在__setattr__()
中的賦值語句self.attr = value
會繼續呼叫該方法,最終導致無限遞迴。
所以在__setattr__()
方法中,必須使用__dict__
來獲取屬性並進行賦值,或者訪問父類同名屬性。所以,有下面幾種方式避免無限遞迴呼叫。
self.__dict__[attr] = value
super().__setattr__(attr, value)
object.__setattr__(self, attr, value)
參考下面的示例。
class Person():
def __init__(self, name):
self.name = name
def __setattr__(self, attr, value):
print("in setattr")
#self.__dict__[attr] = value
#super().__setattr__(attr, value)
object.__setattr__(self, attr, value)
p = Person("malongshuai")
p.age = 33 # 自動呼叫__setattr__()
print(p.age)
執行結果:
in setattr
in setattr
33
可能已經發現問題所在了,上面輸出了兩次in setter
,原因是__init__()
中的賦值操作也會觸發__setattr__()
。
__delattr__()
當呼叫del x.y
的時候會自動觸發__delattr__()
的呼叫。
同樣需要注意的是避免賦值時的無限遞迴問題。因為在__delattr__()
中的del語句可能會繼續呼叫該方法,最終導致無限遞迴。所以在__delattr__()
方法中,必須使用__dict__
來獲取屬性並進行賦值,或者訪問父類同名屬性。所以,有下面幾種方式避免無限遞迴呼叫。
del self.__dict__[attr]
super().__delattr__(attr)
object.__delattr__(self, attr)
例如:
class Person():
def __init__(self, name):
self.name = name
def __delattr__(self, attr):
print("%s deleting" % (attr))
#del self.__dict__[attr]
#super().__delattr__(attr)
object.__delattr__(self, attr)
print("%s deleted" % (attr))
p = Person("malongshuai")
p.age = 33
del p.age