跳至內容

用戶:Xyy23330121/Python/類的方法與類實例

來自維基學院


實例創建

[編輯 | 編輯原始碼]

我們之前已經提到過 __init__ 方法,這裏將詳細講解創建實例的過程。

object.__new__(cls[, ...])

[編輯 | 編輯原始碼]
備註
如果讀者在學習時,對於文檔中不明確的部分查閱過 Python 內置模塊的原始碼,可以發現一些模塊的原始碼中,__new__方法所達成的事項(甚至連語句的表述)都完全可以用__init__方法達成。比如 decimal.Decimal[1]類的方法,其代碼(刪除掉註釋部分)節選如下:
def __new__(cls, value="0", context=None):
    self = object.__new__(cls)
    ...
    if isinstance(value, str):
        ...
        self._sign = 1
        ...
        return self

可以看出,自定義的 __new__ 方法,它滿足以下特徵:

  1. 第一步就是用 object(所有類的父類)定義的 __new__ 方法,把結果賦值給變量 self。
  2. 之後是對 self 創建各種屬性,由於使用的變量名稱是 self,所以語法和 __init__(self) 中完全一致。
  3. 最後 return self

本方法是創建實例的第一步。它會在使用時創建一個新實例。

本方法默認為靜態方法、類方法(而無需用裝飾器),它會將所屬的類自動傳入第一個參數。

在自定義該方法時,一般會使用 super().__new__ 訪問父類定義的 __new__ 方法,並修改父類 __new__ 方法返回的結果。

super(type, object_or_type=None)

[編輯 | 編輯原始碼]

此函數[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()的子類本身)。

object.__init__(self[, ...])

[編輯 | 編輯原始碼]

如果 __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__() 方法。

object.__getattribute__(self, name)

[編輯 | 編輯原始碼]

在獲取實例屬性時,首先調用的是此方法。此方法返回的結果就是獲取到的屬性。

如果未在類型中自定義此方法,則此方法會自動返回實例的內部屬性,或者報錯 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)

object.__getattr__(self, name)

[編輯 | 編輯原始碼]

__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__() 方法找不到對應的屬性,本方法同樣可以引發無窮遞歸。

設置實例屬性

[編輯 | 編輯原始碼]

object.__setattr__(self, name, value)

[編輯 | 編輯原始碼]

此方法用於支持 self.name = value 的操作。和獲取實例屬性時不同,設置實例屬性只有這一個函數。

類似 __getattribute__,在自定義該方法時,應當注意不要使用 self.name = value 的形式。如果有需要,就調用基類的方法 object.__setattr__(self, name, value)

object.__delattr__(self, name)

[編輯 | 編輯原始碼]

此方法用於支持 del self.name 的操作。

實例屬性與方法列表

[編輯 | 編輯原始碼]

object.__dir__(self)

[編輯 | 編輯原始碼]

該方法用於支持 dir(self) 的操作。它會返回實例中、屬性與方法名稱的字符串組成的列表。

inspect 模塊

[編輯 | 編輯原始碼]

inspect 模塊列出了所有可獲得的內置特殊屬性。詳情請參見 Python 文檔。

描述器

[編輯 | 編輯原始碼]

我們之前學習「私有量」的時候提到過 property。property 是一種描述器。以下所示的方法僅當它作為類的屬性時才會起作用。

這是因為獲取、設置或刪除類的屬性時,類的 __getattribute__ 等方法會嘗試調用描述器的方法。描述器如果不作為類的屬性、而作為一般的變量使用時,不會有 __getattribute__ 等方法來調用描述器方法。

只要設置了下面方法中的一個,該類型就是一種描述器。

object.__get__(self, instance, owner=None)

[編輯 | 編輯原始碼]

其中,instance 是描述器屬性所在的實例,而 owner 是 instance 所屬的類型。

object.__set__(self, instance, value)

[編輯 | 編輯原始碼]

其中,instance 是描述器屬性所在的實例,而 value 是要賦的值。

object.__delete__(self, instance)

[編輯 | 編輯原始碼]

其中,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

object.__objclass__

[編輯 | 編輯原始碼]

在創建描述器時,還可以添加一個可選的屬性。該屬性應當設置為描述器屬性所在的實例類型。該屬性會被 inspect 模塊所使用,有助於動態類屬性的運行時內省。

子類創建

[編輯 | 編輯原始碼]

為了代碼的易於擴展,有時需要自定義子類創建的行為。

object.__init_subclass__(cls[, ...])

[編輯 | 編輯原始碼]

在創建子類時會調用父類的此方法,其中, 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 來為整數作進一步優化。增加了代碼的可擴展性。

object.__set_name__(self, owner, name)

[編輯 | 編輯原始碼]

在創建類的同時創建屬性時會調用此方法。我們看以下示例:

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'

參考文獻

[編輯 | 編輯原始碼]