跳至內容

使用者: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__() 方法。

具體來講,下面兩者(在最終沒報錯的情況下)是等價的:

a = ins.att
try:
    a = type(ins).__getattribute__(ins, 'att')
except AttributeError:
    a = type(ins).__getattr__(ins, 'att')
詳細說明

我們希望確認過程中調用的到底是ins.__getattr__('att')還是type(ins).__getattr__(ins, 'att'),因此做以下實驗:

class test:
    def __getattr__(self, name):
        print('__getattr__', end=' ')
        return 0
    
    def __getattribute__(self, name):
        print('__getattribute__', end=' ')
        return super().__getattribute__(name)

ins = test()

print('以下方法被依次调用:', end='')
ins.att
print()  #输出:以下方法被依次调用:__getattribute__ __getattr__ 

print('以下方法被依次调用:', end='')
try:
    ins.__getattribute__('att')
except AttributeError:
    ins.__getattr__('att')
print()  #输出:以下方法被依次调用:__getattribute__ __getattribute__ __getattribute__ __getattr__ 

print('以下方法被依次调用:', end='')
try:
    type(ins).__getattribute__(ins, 'att')
except AttributeError:
    type(ins).__getattr__(ins, 'att')
print()  #输出:以下方法被依次调用:__getattribute__ __getattr__

不難發現,第一組和第三組的行為是一致的。而第二組的邏輯則類似於:

try:
    _func = type(ins).__getattribute__(ins, '__getattribute__')
    _func('att')
except AttributeError:
    _func = type(ins).__getattribute__(ins, '__getattr__')
    _func('att')

默認情況

[編輯 | 編輯原始碼]
object.__getattribute__(self, name)
[編輯 | 編輯原始碼]

此方法默認會自動返回實例的內部屬性,或者報錯 AttributeError

object.__getattr__(self, name)
[編輯 | 編輯原始碼]

此方法默認會直接報錯 AttributeError

注意:避免無窮遞歸

[編輯 | 編輯原始碼]

在自定義上述兩個方法時,一定要注意避免無限遞歸。

編程要點

這裡給出幾個避免出錯的要點:

1. 儘可能不要修改 __getattribute__ 對已有屬性的行為
如果對這些行為進行修改,則該對象的一切屬性你都將讀不到。

class test:
    def __getattribute__(self, name):
        return 0

a = test()
print(a.__dict__) #输出:0

最好使用形如下面的方法:

class test:
    def __getattribute__(self, name):
        try:
            # 这样就不会改变对已有属性的行为
            return super().__getattribute__(name)
        except AttributeError:
            return 0

特別的。如果像上面一樣、想要實現的功能只靠自定義__getattr__也能實現,則建議讀者不要自定義 __getattribute__

class test:
    def __getattr__(self, name):
        return 0

2. 不要在 __getattribute__ 中用常規方法獲取任何屬性內容
如果用常規方法獲取屬性內容,則會造成死循環。

class test:
    default = 0
    def __getattribute__(self, name):
        try:
            # 这样就不会改变对已有属性的行为
            return super().__getattribute__(name)
        except AttributeError:
            return self.default

a = test()
print(a.not_defined) #报错:无限循环

建議改用:

class test:
    default = 0
    def __getattribute__(self, name):
        try:
            # 这样就不会改变对已有属性的行为
            return super().__getattribute__(name)
        except AttributeError:
            return super().__getattribute__('default')

3. 儘可能不要在 __getattr__ 中用常規方法獲取任何不能通過 __getattribute__ 獲取的屬性內容

原因和上面類似。但總體來講,自定義 __getattr__ 總是更安全的。比如下面的代碼不會報錯:

class test:
    default = 0
    def __getattr__(self, name):
        if name == 'a': return self.b     # 最终会调用 __getattr__(self, 'b')
        if name == 'b': return self.default # 在 __getattribute__ 时就输出了结果,所以不会报错
        raise AttributeError

設置實例屬性

[編輯 | 編輯原始碼]

獲取實例屬性 類似。在設置實例屬性時,同樣也調用了方法。

具體來講,下面兩者(在最終沒報錯的情況下)是等價的:

ins.att = a
del ins.att
type(ins).__setattr__(ins, 'att', a)
type(ins).__delattr__(ins, 'att')

在默認情況下,這兩個方法都應該返回 None

實例屬性與方法列表

[編輯 | 編輯原始碼]

object.__dir__(self)

[編輯 | 編輯原始碼]

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

描述器

[編輯 | 編輯原始碼]

我們之前學習「私有量」的時候提到過 property。property 是一種描述器。

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

__get__(self, instance, owner=None)

[編輯 | 編輯原始碼]

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

__set__(self, instance, value)

[編輯 | 編輯原始碼]

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

__delete__(self, instance)

[編輯 | 編輯原始碼]

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

描述器如果不作為類的屬性、而作為一般的變量使用時,不會有 __getattribute__ 等方法來調用描述器方法。

我們看以下示例:

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'

參考文獻

[編輯 | 編輯原始碼]