使用者:Xyy23330121/Python/類的方法與類實例
我們之前已經提到過 __init__
方法,這裡將詳細講解創建實例的過程。
def __new__(cls, value="0", context=None):
self = object.__new__(cls)
...
if isinstance(value, str):
...
self._sign = 1
...
return self
可以看出,自定義的 __new__ 方法,它滿足以下特徵:
- 第一步就是用 object(所有類的父類)定義的 __new__ 方法,把結果賦值給變量 self。
- 之後是對 self 創建各種屬性,由於使用的變量名稱是 self,所以語法和 __init__(self) 中完全一致。
- 最後 return self
本方法是創建實例的第一步。它會在使用時創建一個新實例。
本方法默認為靜態方法、類方法(而無需用裝飾器),它會將所屬的類自動傳入第一個參數。
在自定義該方法時,一般會使用 super().__new__
訪問父類定義的 __new__
方法,並修改父類 __new__
方法返回的結果。
此函數[2]會返回一個特殊對象。任何對該對象的方法都會被重定向到 type
的父類中定義的方法。比如以下示例:
class C:
def method(self): print("C.method()")
class D(C):
def method(self): print("D.method()")
def usemethod(self):
self.method()
super().method()
obj = D()
obj.usemethod()
輸出為:
D.method()
C.method()
從而,在子類中也可以調用已經被子類作用域所覆蓋的、父類的方法。
該方法查找父類方法的順序,和子類繼承父類方法的順序是一致的(但跳過了調用super()
的子類本身)。
如果 __new__
方法在構造對象期間被發起調用,並返回了一個 cls 的實例。則新實例的 __init__
方法將會被調用。調用時,會傳入所有傳入 __new__ 方法的參數。比如以下示例:
class test:
def __new__(cls, value):
print(value)
return object.__new__(test)
def __init__(self, value):
print(value)
a = test("传入的参数")
該實例輸出兩遍 传入的参数
,可見傳入 __new__ 的 value 參數也被傳入了 __init__ 的 value 參數。
__init__
方法直接修改 __new__
方法返回的實例,只要它返回除了None
以外的任何值,就會導致錯誤。
獲取實例屬性時,調用的是實例的 __getattribute__() 方法和 __getattr__() 方法。
在獲取實例屬性時,首先調用的是此方法。此方法返回的結果就是獲取到的屬性。
如果未在類型中自定義此方法,則此方法會自動返回實例的內部屬性,或者報錯 AttributeError
。
如果此方法報錯 AttributeError
,Python 不會直接報錯,而是嘗試調用 __getattr__() 方法來獲取屬性。
此方法傳入的參數 name 是屬性名稱的字符串。這可以從下面的測試代碼看出來:
class C:
def __getattribute__(self, name):
print(name, type(name)) #在获取实例属性时,打印传入的参数内容和参数类型
return 0
obj = C()
print(obj.test_attribute_name) #获取实例属性,打印获取到的属性
輸出為:
test_attribute_name <class 'str'>
0
該方法應該返回找到的、對應屬性名稱的屬性值,或引發 AttributeError
。
需要注意的是,該方法會接管一切獲取屬性的行為。比如:
class C:
testmsg = "这是测试信息"
def __getattribute__(self, name):
return "读取属性的操作被接管"
obj = C()
print(obj.testmsg)
輸出為:
读取属性的操作被接管
因此,以下的代碼會導致無窮遞歸:
class C:
testmsg = "这是测试信息"
def __getattribute__(self, name):
if name == "testmsg":
return self.testmsg #在这个情况下,等同于 return self.__getattribute__("testmsg")
obj = C()
print(obj.testmsg)
如果要在這個方法中使用實例的屬性,應該改為使用:
class C:
testmsg = "这是测试信息"
def __getattribute__(self, name):
if name == "testmsg":
#通过 object 类型的读取属性方法,来获取自身的属性。
return object.__getattribute__(self, "testmsg")
obj = C()
print(obj.testmsg)
當 __getattribute__()
方法引發 AttributeError
時,會調用此方法再次嘗試獲取屬性。
和 __getattribute__()
方法一樣,該方法應該返回找到的屬性值,或引發 AttributeError
。當沒有自定義此方法時,此方法默認總會拋出一個 AttributeError
。
由於該方法在 __getattribute__()
報錯之後才調用。只要 __getattribute__
方法能找到對應的屬性,本方法是可以使用類似 self.name
的方式來讀取屬性的。比如:
class C:
testmsg = "这是测试信息"
def __getattr__(self, name):
return self.testmsg
obj = C()
print(obj.testmsg)
因此,當無需接管已有屬性的讀取時,不去自定義 __getattribute__()
方法,而是自定義此方法是相對安全的。
但是,如果 __getattribute__()
方法找不到對應的屬性,本方法同樣可以引發無窮遞歸。
此方法用於支持 self.name = value
的操作。和獲取實例屬性時不同,設置實例屬性只有這一個函數。
類似 __getattribute__
,在自定義該方法時,應當注意不要使用 self.name = value
的形式。如果有需要,就調用基類的方法 object.__setattr__(self, name, value)
。
此方法用於支持 del self.name
的操作。
該方法用於支持 dir(self)
的操作。它會返回實例中、屬性與方法名稱的字符串組成的列表。
inspect 模塊列出了所有可獲得的內置特殊屬性。詳情請參見 Python 文檔。
我們之前學習「私有量」的時候提到過 property。property 是一種描述器。以下所示的方法僅當它作為類的屬性時才會起作用。
這是因為獲取、設置或刪除類的屬性時,類的 __getattribute__ 等方法會嘗試調用描述器的方法。描述器如果不作為類的屬性、而作為一般的變量使用時,不會有 __getattribute__ 等方法來調用描述器方法。
只要設置了下面方法中的一個,該類型就是一種描述器。
其中,instance 是描述器屬性所在的實例,而 owner 是 instance 所屬的類型。
其中,instance 是描述器屬性所在的實例,而 value 是要賦的值。
其中,instance 是描述器屬性所在的實例。
我們看以下示例:
class ReadOnly:
def __init__(self, value):
self.__value = value
def __get__(self, instance, owner=None):
print(instance.name, "and", owner.clsname)
return self.__value
def __set__(self, instance, value):
raise AttributeError("readonly attribute")
def __delete__(self, instance):
raise AttributeError("readonly attribute")
class C:
clsname = "Class C"
def __init__(self, name):
self.name = name
x = ReadOnly(0)
c = C("Instance c")
output = c.x #输出:Instance c and Class C
print(output) #输出:0
c.x = 2 #报错:AttributeError: readonly attribute
在創建描述器時,還可以添加一個可選的屬性。該屬性應當設置為描述器屬性所在的實例類型。該屬性會被 inspect 模塊所使用,有助於動態類屬性的運行時內省。
為了代碼的易於擴展,有時需要自定義子類創建的行為。
在創建子類時會調用父類的此方法,其中, cls 參數會被自動傳入新的子類。
這裡的參數是在創建類的時候,通過「關鍵詞參數」方法傳入的,比如:
class test:
def __init_subclass__(cls, arg=0):
print(arg)
pass
class test1(test, arg="传入的参数"):
pass
以上示例的輸出是传入的参数
。如果使用位置參數,則會與「父類的繼承」產生混淆。
此方法是子類被創建完畢後,對子類作修改。而非從頭創建子類。此方法返回任何值都會u報錯。
random.Random[3]類就有此方法,其代碼節選如下:
def __init_subclass__(cls, /, **kwargs):
for c in cls.__mro__:
if '_randbelow' in c.__dict__:
# just inherit it
break
if 'getrandbits' in c.__dict__:
cls._randbelow = cls._randbelow_with_getrandbits
break
if 'random' in c.__dict__:
cls._randbelow = cls._randbelow_without_getrandbits
break
random.Random 類對於隨機數有不同的處理。有的方法使用隨機整數以規避浮點誤差,而有的方法使用隨機浮點數。其中,使用隨機整數的方法調用的是 _randbelow 方法。
此處 __init_subclass__ 的處理,在子類定義了 getrandbits (隨機整數方法)的情況下,讓 _randbelow 變為 _randbelow_with_getrandbits 方法,再在 _randbelow_with_getrandbits 中調用 getrandbits 方法。從而實現了隨機整數方法的調用。
如果子類沒有定義 getrandbits,則查詢是否定義了 random (隨機浮點數方法)。如果有,讓 _randbelow 變為 _randbelow_without_getrandbits 方法,再在 _randbelow_without_getrandbits 方法中調用 random 方法,並把結果轉化為整數。
如果子類兩者都沒有定義,則認為子類沒有修改隨機數生成器,此時按繼承順序繼續查找父類的方法。
通過這樣的處理,子類可以只定義 random 方法就修改 random.Random 中所有類型隨機數的生成,也可以附加定義 getrandbits 來為整數作進一步優化。增加了代碼的可擴展性。
在創建類的同時創建屬性時會調用此方法。我們看以下示例:
class AttrTest:
def __set_name__(self, owner, name):
print(self, owner, name)
def __str__(self):
return "AttrTest 的实例"
__repr__ = __str__
attr = AttrTest()
class test:
x = attr #在创建类的同时创建属性
test.y = attr #在创建类之后,再创建属性
attr.__set_name__(test,"z") #手动调用该方法
print(test.x, test.y)
print(test.z)
輸出為:
AttrTest 的实例 <class '__main__.test'> x
AttrTest 的实例 <class '__main__.test'> z
AttrTest 的实例 AttrTest 的实例
Traceback (most recent call last):
File "...", ..., in <module>
print(test.z)
^^^^^^
AttributeError: type object 'test' has no attribute 'z'