1. 程式人生 > 實用技巧 >類與例項,類與子類

類與例項,類與子類

@

目錄


前言

通過這篇筆記,來達成兩個目的:
一、幫助自己鞏固一下python中和類有關的一些基礎知識點,明晰在定義類的過程中各部分程式碼(主要是魔法方法)所起的作用。
二、瞭解類的繼承到底是繼承了什麼?super()函式起了什麼作用?呼叫父類方法時python的執行順序是什麼?——以此加深自己對繼承的理解。


閱讀正文前應掌握的兩個工具

魔法屬性__dict__

類名._dict_,返回一個字典,由類中所有屬性、方法的鍵值對組成。
例項的__dict__僅儲存與該例項相關的例項屬性,並不包含該例項的所有有效屬性。類的__dict__儲存所有例項共享的變數和函式(類屬性,方法等),類的__dict__並不包含其父類的屬性。

class A():
    z = 100

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def foo(self):
        print('hello')


a = A(4, 6)

print(a.__dict__)  #{'a': 4, 'b': 6}
print(A.__dict__)
#  {'__module__': '__main__', 'z': 100,
'__init__': <function A.__init__ at 0x000002127989ED30>,
'foo': <function A.foo at 0x000002127989EDC0>, 
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

super()函式

格式:super(cls,self).方法名(引數)
cls是類名,self代指當前類的例項。(你在為哪個類寫方法,這個類就是“當前類”)
python3之後,super().方法名(引數)等同於super(當前類,self).方法名(引數)。(見下方示例)

程式碼執行解析:類super例項化。這一例項包含了兩個資訊:一是以當前類為基礎的MRO列表,二是處於該MRO列表中的類cls。還有一個條件:當前類必須是cls的子類。(當前類也是自身的子類,所以cls可以是當前類)
【當前類的MRO列表可以通過“print(當前類.mro())”檢視】

作用:搜尋在類A的MRO列表中處於類cls右方的類,找到某一方法然後呼叫它。例如[A,B,C,D,cls,E,F,G,object],從類E、類F、類G、類object中搜索。

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')


class B(A):
    def __init__(self, a, b, c):
        super().__init__(a, b)  #等同於super(B,self).__init__(a,b)
        #在這裡可以使用“A.__init__(a,b)”代替上行程式碼
        self.c = c
        print('this is B')


B(1, 2, 3)
#this is A
#this is B

這裡有個問題:雖然python3之後將super(當前類,self).方法名(引數)省略為super().方法名(引數),但並未強制我們將super()的第一個引數寫成當前類的識別符號。

class A():
    def __init__(self):
        print('this is A')


class B():
    def __init__(self):
        print('this is B')


class C(B, A):
    def __init__(self):
        super().__init__()  #等同於super(C,self)

C()  #將類C例項化。或者c=C()也是一樣的。目的是觸發類C的__init__()
#this is B

——類C的MRO列表為“[<class 'main.C'>, <class 'main.B'>, <class 'main.A'>, <class 'object'>]”。

顯然,這裡super()函式會呼叫類B的__init__方法。

但是,類C同樣是類B和類A的子類。

我們可以將程式碼改成super(B,self)._init_() 或者super(A,self)._init_(a,b),然後建立類C的例項,因為這會觸發類C的__init__方法。讓我們看看會發生什麼?

class A():
    def __init__(self):
        print('this is A')


class B():
    def __init__(self):
        print('this is B')


class C(B, A):
    def __init__(self):
        super(B,self).__init__()

C()
#this is A
class A():
    def __init__(self):
        print('this is A')


class B():
    def __init__(self):
        print('this is B')


class C(B, A):
    def __init__(self):
        super(A, self).__init__()


C()  #

還記得類C的MRO列表嗎?

[<class 'main.C'>, <class 'main.B'>, <class 'main.A'>, <class 'object'>]

當我們使用super()._init_()時,實際上是運行了程式碼“super(C,self)._init_()”,它會呼叫類B的_init_(),打印出‘this is B’;
當我們使用super(B,self)._init_()時,它會呼叫類A的_init_(),打印出‘this is A’;
當我們使用super(A,self)._init_()時,它會呼叫object的_init_(),所以最後什麼也沒打印出來。

類與例項

以“類名(屬性1,屬性2,…)”的格式來建立類的例項時,會觸發該類的__new__()與__init__()。
建立類A的例項a:“a=A(屬性1,屬性2,…)”

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')


A()  #TypeError: __init__() missing 2 required positional arguments: 'a' and 'b'
A(1, 2)  #this is A
a=A(1,2)  #this is A

注意,以上三個結果是單獨執行時得到的。

魔法方法簡介

__new__()

new() 方法是在類準備將自身例項化時呼叫。預設是呼叫該類的直接父類的__new__()方法來構造該類的例項。如果其父類沒有定義__new__()方法,則在其父類的父類中尋找,一直找到object類的__new__()方法為止。
new() 方法始終都是類的靜態方法,即使沒有被加上靜態方法裝飾器。
new()至少要有一個引數cls,代表要例項化的類,此引數在例項化時由Python直譯器自動提供。
new()必須要有返回值,返回例項化出來的例項,這點在自己實現__new__時要特別注意,可以return父類__new__出來的例項,或者直接是object的__new__出來的例項。__init__有一個引數self,就是這個__new__返回的例項。

@staticmethod
    def __new__(cls, *args, **kwargs):
        print("__new__方法被呼叫")
        return 父類名.__new__(cls)#或者return object.__new__(cls)  

__init__()

首先,_new_()將返回的例項傳給_init_()的self引數。
__init__定義了類的所有例項共有的一些屬性,接收建立例項時傳來的資料用以給這些屬性(除第一個引數外的所有引數)賦值。

def __init__(self, attr_1, attr_2):
    self.attr_1 = attr_1
    self.attr_2 = attr_2

__call__()

“例項名()”時呼叫該方法,無預設定義。

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')


a=A(1,2) 
a()
#this is A
#TypeError: 'A' object is not callable

輸出‘this is A’是因為程式碼“a=A(1,2)”的存在,它觸發了類A的__new__()和__init__();報錯是因為‘a()’觸發了類A的__call__(),可類A沒有定義它。
由這一例子可知,“例項名()”會直接觸發__call__(),並不會呼叫類A的其他方法。

__getattribute_()與__getattr_()

訪問屬性的值時,_getattribute_()方法將得到呼叫。若其識別符號存在於例項或其類的__dict__中,則return object._ getattribute_(self,item) 或者使用super()._getattribute_(item);若未找到,則報錯並呼叫__getattr__()方法。若__getattr__未被定義,則丟擲AttributeError異常。

__setattr__()

“例項名.屬性=”時自動呼叫該方法.預設執行屬性賦值操作。

__delattr__()

“del 例項名.屬性”時自動呼叫該方法。預設執行刪除屬性操作。

__hash__()

hash(例項名)時自動呼叫該方法,預設返回一個雜湊值。

__eq_()、__ge_()、__gt_()、__le_()和__lt__()

_eq_:使用“==”時自動呼叫,預設返回True或者False。
_ge_:使用“>=”時自動呼叫,預設返回True或者False。
_le_:使用<=”時自動呼叫,預設返回True或者False。
_gt_:使用“>”時自動呼叫,預設返回True或者False。
_lt_:使用<”時自動呼叫,預設返回True或者False。

__getitem_()、__setitem_()和__delitem__()

_getitem_():訪問例項名[key]時呼叫,定義返回的值。
_setitem_():“例項名[key]=value”時自動呼叫,定義接下來的操作。
_delitem_():“del 例項名[key]”時自動呼叫,定義接下來的操作。

__str_()和__repr_()

_str_():改變物件的字串顯示。定義時必須返回一個字串,否則報錯。
_repr_():如果__str__沒有被定義,那麼就會使用__repr__來代替輸出。定義時必須返回一個字串,否則報錯。

__del__()

當物件在記憶體中被釋放時呼叫該方法。

__get_()、__set_()和__delate__()

_get_():呼叫一個屬性時觸發
_set_():為一個屬性賦值時觸發
_delate_():採用del刪除屬性時觸發

__slot__()

不可被繼承。當你定義__slots__後,slots__就會為例項使用一種更加緊湊的內部表示。例項通過一個很小的固定大小的陣列來構建,而不是為每個例項定義一個__dict,這跟元組或列表很類似。在__slots__中列出的屬性名在內部被對映到這個陣列的指定小標上。

類屬性和例項屬性的定義、區別?

類屬性:直接定義在類中的變數。
例項屬性:以“self.屬性名 = 值”形式定義在__init__方法中的變數。
區別:類屬性的值不受例項影響。

class A():
    z=100  
    def __init__(self, a, b):
        self.a = a
        self.b = b
#z是類屬性,a、b是例項屬性

類中的方法有哪些?

例項物件能夠呼叫類方法:它擁有__class__屬性,在其中儲存著類物件的地址。

類物件無法直接呼叫例項方法:儘管例項方法的定義儲存在類物件中,但當類物件訪問方法時,它會先尋找@classmethod、@staticmethod。經修飾器修飾過的方法才能被類物件呼叫,即類物件只能呼叫類方法和靜態方法。

例項方法

⾄少有self(寫法不強制,約定俗成)引數。執⾏例項⽅法時,⾃動將調
⽤該⽅法的物件賦值給self。
它可以訪問類屬性、例項屬性。
呼叫格式:例項名.方法名()或者類名.方法名(例項名)
【類名.方法名(),預設self=類名,無法呼叫例項方法。然而,在括號中填入例項名相當於指定self=例項名,因此可以呼叫例項方法。】

類方法

上方有@classmethod裝飾器,⾄少有cls(寫法不強制,約定俗成)引數。執⾏類⽅法時,⾃動將調⽤該⽅法的類賦值給cls。
它可以訪問類屬性,但無法訪問例項屬性。
呼叫格式:類名.方法名()或者例項名.方法名()

靜態方法

上方有@staticmethod裝飾器,不具有表示自身的引數。
它無法訪問類屬性、例項屬性。(畢竟它沒有表示自身的引數)
相當於一個放在類的作用域裡的函式,具有獨立性。
呼叫格式:類名.方法名()或者例項名.方法名()

例項方法如何取得類屬性的值?

class A():
    z = 100  # 類屬性

    def foo(self):
        print(z)


a = A()
a.foo()
#NameError: name 'z' is not defined

錯誤提示:變數z尚未定義。有三種更改方案:self.z、A.z、a.z,它們都能正確輸出z的值。(self.z和a.z性質一樣)

class A():
    z = 100  # 類屬性

    def foo(self):
        print(self.z)


a = A()
a.foo()  #100
print(A.z)  #100
print(a.z)  #100

總結:例項方法可以通過“self.類屬性名”的形式來訪問類屬性。另外,在類外我們可以通過“類名/例項名.類屬性名”的形式來訪問類屬性。

如果例項方法沒有self引數?

self引數實際上是類定義方法時寫的第一個引數,python會自動將呼叫這一方法的類賦值給這一引數。所以,沒有self引數,幾乎等同於沒有引數。

class A():
    z = 100  #類屬性

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def foo(): #括號內應加上self引數
        print(self.a+self.b)
        print(self.z)


a = A(4, 6)
a.foo()
#TypeError: foo() takes 0 positional arguments but 1 was given

方法__init__定義了self變數,但是它和方法foo()處於平行的兩個程式碼塊中,foo()自己沒有定義變數self,卻使用了self,這顯示是錯誤的。

但是,示例報錯並不是因為我們試圖列印和變數self有關的值。

class A():
    z = 100

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def foo(self):
        print('hello')  #沒有訪問任何屬性


a = A(4, 6)
a.foo()
#TypeError: foo() takes 0 positional arguments but 1 was given

仍然是同一種錯誤,大意是‘’foo()沒有設定引數接收,我們卻給了一個引數‘’。我們並沒有傳值進去,看樣子是python自動傳入的。結合上面內容,我們有理由推測,python往引數裡傳遞了類名,但我們沒有設定接收引數,(原本第一個引數會接收類名),因此而報錯。
總結:例項方法必須要提供一個形參來代表自身。

類與子類

繼承的格式:class 類名(父類名)
若不寫父類名,則預設父類名為“object”,即物件類。

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')


class B(A):
    pass

類B會繼承類A包括__init__()在內的所有方法,因此類B在例項化時,必須要傳入兩個引數,因為類A定義了a、b這兩個形參。
類B可以定義自己的方法,但當它的方法名和類A相同時,便會覆蓋從類A繼承來的方法。例如:

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')


class B(A):
    def __init__(self):
        pass

類B重寫了從類A繼承來__init__(),如今類B例項化可以直接“b=B()”。但是,這也導致B失去了類A的__init__程式碼。如果B想要設定a、b兩個傳參入口,就不得不重新寫一遍。
舉個例子,類A設定了十個傳參入口,類B想在它的基礎上增加一個傳參入口,怎麼辦呢?
答案便是super()。

class A():
    def __init__(self, a, b, c, d, e, f, g, h, i, j):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        self.f = f
        self.g = g
        self.h = h
        self.i = i
        self.j = j


class B(A):
    def __init__(self, a, b, c, d, e, f, g, h, i, j, k):
        super().__init__(a, b, c, d, e, f, g, h, i, j)
        self.k = k

如何改寫父類的方法

先覆蓋(重新定義),再繼承[利用super()函式],最後加上自己的程式碼。

父類和各子類之間__dict__的區別

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')

    def run(self):
        return self.a+self.b

    def say(self):
        print('hello')


class B(A):
    pass


class C(A):
    def __init__(self, a, b,c):
        super().__init__()
        self.c=c
        print('this is C')


class D(A):
    def __init__(self):
        print('this is D')


class E(A):
    def run(self):
        super().run()

    def say(self):
        print('hello')

    def hug(self):
        print('hug')

檢視它們的__dict__屬性,結果如下:

print(A.__dict__)
#{'__module__': '__main__', 
'__init__': <function A.__init__ at 0x00000296A6D0ED30>, 
'run': <function A.run at 0x00000296A6D0EDC0>, 
'say': <function A.say at 0x00000296A6D0EE50>, 
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
print(B.__dict__)
#{'__module__': '__main__', 
'__doc__': None}
print(C.__dict__)
#{'__module__': '__main__', 
'__init__': <function C.__init__ at 0x0000019CCD47EEE0>, '__doc__': None}
print(D.__dict__)
#{'__module__': '__main__', 
'__init__': <function D.__init__ at 0x0000017598F8EF70>, 
'__doc__': None}
print(E.__dict__)
#{'__module__': '__main__', 
'run': <function E.run at 0x0000018031A1B040>, 
'say': <function E.say at 0x0000018031A1B0D0>, 
'hug': <function E.hug at 0x0000018031A1B160>, '__doc__': None}

可以看出,父類和子類的__dict__中都記錄了__module__和__doc__以及它們各自定義過的方法。並且,super()不是引用了父類的方法,而是開闢了新的空間來儲存相同的方法。

如何呼叫父類的屬性

父類的屬性分兩種,一是類屬性,一是例項屬性。
關於例項屬性,在“類與子類”的開頭已經舉過例子,這裡不再複述。
關於類屬性,程式碼如下:

class A():
    z = 100  #類屬性

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def run(self):
        return self.a+self.b

    def say(self):
        print('hello')


class B(A):
    def foo(self):
        print(self.z)  #或者A.z,或者B.z。直接列印z會報錯。


B(2,1).foo()  #100

我的疑惑:在例項方法內為什麼不能直接呼叫“z”,而是必須加上字首?按理說,在區域性搜尋不到變數,系統應該會去外層搜尋,而類屬性就在例項方法的外層。

以上是在類內呼叫父類屬性的方法,倘若在類外:

class A():
    z = 100

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def run(self):
        return self.a+self.b

    def say(self):
        print('hello')


class B(A):
    pass


a = A(2, 1)
b = B(2, 1)
print(A.z)
print(B.z)
print(a.z)
print(b.z)
#100
#100
#100
#100

至於直接print(z)自然是不行的,因為在外部,z還沒被定義。

如何呼叫父類的例項方法

首先,我們使用“class B(A)”的格式來讓類B繼承類A。現在,類B已經能夠使用父類方法了。
使用格式:子類例項名.父類方法名()

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')

    def run(self):
        return self.a+self.b

    def say(self):
        print('hello')


class B(A):
    pass


b = B(2, 3)
b.say()
#this is A
#hello

輸出的第一行是因為'B(2,3)'這個程式碼觸發了類B的__init__(),而類B的__init__()與類A的__init__()一致,所以打印出了‘this is A’
輸出的第二行則是呼叫了B的say()。B的確沒定義過say(),但就像它也沒定義過__init__()一樣,它繼承了類A的say()。

因此,要呼叫父類的方法,首要便是別編寫同名方法,以免覆蓋了父類的方法;倘若不得不編寫同名方法,你也可以通過super()函式將父類的程式碼繼承回來;倘若你既覆蓋了父類的方法,又不想使用super()函式,解決方法如下:

class A():
    def __init__(self, a, b):
        self.a = a
        self.b = b
        print('this is A')

    def run(self):
        return self.a+self.b

    def say(self):
        print('hello')


class B(A):
    def say(self):
        print('sorry')


A(2, 3).say()

直接建立類A的例項,然後呼叫……的確沒什麼意義,或許還有其他方法,這裡就不深究了。

補充一點:類方法的使用格式是類名.方法名(),不需要先例項化。

後記

這篇筆記用了好幾天時間,主要重心放在了__init__()、super()的使用以及理解類的各種引數的意義上面。語言文字不夠簡練、準確,小節劃分太潦草(不同節內容可能有一些重複的地方)。最苦惱的大概就是markdown的下劃線了——它有時沒辦法直接顯示出來,加上斜槓也不盡人意……

最後總結一下遺留的困惑:
例項方法內為什麼不能僅用變數名呼叫類屬性?