1. 程式人生 > >Python的物件和類(6)

Python的物件和類(6)

什麼是物件

物件既包含資料(變數,更習慣稱之為特性attribute),也包含程式碼(函式,也成為方法)。

使用class定義類

之前,我們把物件比作塑料盒子。類(class)則像是製作盒子用的模具。例如,Python的內建類String可以建立像’cat’和’duck’這樣的字串物件。

# 建立Person類
class Person():
    # __init__是Python中一個特殊的函式名,用於根據類的定義建立例項物件
    # self引數指向了這個正在被建立的物件本身
    def __init__(self, name):
        self.name = name
    def
print_name(self):
print(self.name) # Person('Elmer Fudd')建立了一個Person類的物件,並給它賦值someone這個名字 someone = Person('Elmer Fudd') someone.print_name()

someone = Person(‘Elmer Fudd’)

上面這短短一行程式碼實際做了以下工作:
1. 檢視Person類的定義
2. 在記憶體中例項化(建立一個新的物件)
3. 呼叫物件的_init_方法,將這個新建立的物件作為self傳入,並將另一個引數(‘Elmer Fudd’)作為name傳入
4. 將name的值存入物件
5. 返回這個新的物件


6. 將名字hunter與這個物件關聯

someone.print_name()

self代表類的例項,而非類。在Python的直譯器內部,當我們呼叫someone.print_name()時,實際上Python解釋成Person.print_name(self)

self

  • self在定義時需要定義,但是在呼叫時會自動傳入。
  • self的名字並不是規定死的,但是最好還是按照約定是用self
  • self總是指呼叫時的類的例項。

函式方法間的依賴關係

這裡寫圖片描述

Python的函式方法並不想C++那樣,能夠過載

後面的haha函式的定義把前面haha函式的定義覆蓋了(物件的名字才是唯一的標識,跟引數無關),就解釋了下面的結果。
這裡寫圖片描述

正確做法:
這裡寫圖片描述

其他

class A:
    # 此處logo是類A的屬性
    logo = 'Logo A'
class B:
    def __init__(self):
        # 此處logo是類A生成的物件的屬性
        self.logo = 'Logo B'

a = A()
b = B()
print (a.logo)
print (b.logo)

print(A.__dict__)
print(B.__dict__)
print(a.__dict__)
print(b.__dict__)

結果:

Logo A
Logo B
{'__doc__': None, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__module__': '__main__', 'logo': 'Logo A', '__dict__': <attribute '__dict__' of 'A' objects>}
{'__doc__': None, '__init__': <function B.__init__ at 0x0037D348>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'B' objects>, '__dict__': <attribute '__dict__' of 'B' objects>}
{}
{'logo': 'Logo B'}

其中對比A._dict_、B._dict_、a._dict_和b._dict_、A._dict_中有個logo 屬性,B.dict_中有個_init_屬性,A沒有_init_方法,故a沒有任何屬性,B有_init_方法,執行該_init_方法使b中有了一個logo屬性

繼承

從已有的類中衍生出新的類,新增或修改部分功能。這是程式碼複用的一個絕佳的例子。使用繼承得到的新類會自動獲得舊類中的所有方法,而不需要進行任何複製。
這裡寫圖片描述

建構函式

python裡一個class可以定義多個建構函式嗎?

不行,一個class只能有一個用於構造物件的_init_函式
但python中的變數是無型別的,因此傳給init的引數可以是任何型別

python中的函式引數在定義時可以有預設值,可以讓init函式接受多個引數,在後面的一些引數給出預設值的方法讓init接受不同個數的引數,並且執行型別檢查執行不同的程式碼,用上述方法實現類的建構函式的多型性

這裡寫圖片描述
這裡寫圖片描述

  1. Python中如果子類有自己的建構函式,會把父類的建構函式覆蓋(與引數個數等無關),不會自動呼叫父類的建構函式,如果需要用到父類的建構函式,則需要在子類的建構函式中顯式的呼叫
  2. 如果子類沒有自己的建構函式,則會直接從父類繼承建構函式,這在單繼承(一個子類只從一個父類派生)中沒有任何理解上的問題
  3. 子類從多個父類派生,而子類又沒有自己的建構函式時,就按順序繼承,哪個父類在最前面且它又有自己的建構函式,就繼承它的建構函式。即如果最前面第一個父類沒有建構函式,則繼承第2個的建構函式,第2個沒有的話,再往後找,以此類推。若仍然沒有就找超父類,直到起源。
class A:
    def __init__(self):
        print 'a'
class B:
    def __init__(self):
        print 'b'
class C1(B,A):
    pass
class C2(A,B):
    pass
class D1(C1):
    def __init__(self):
        C1.__init__(self)
class D2(C2):
    def __init__(self):
        C2.__init__(self)

if(__name__=='__main__'):
    print 'd1------->:'
    d1=D1()
    print 'd2------->:'
    d2=D2()

>>> 
d1------->:
b
d2------->:
a

這裡寫圖片描述

覆蓋方法

在Python中,子類的同名函式會把父類的同名函式完全覆蓋掉。所以下面的例子中,doctor = MDPerson()會出錯。如果想呼叫被覆蓋的父類函式,需要super的幫忙。
這裡寫圖片描述

使用super從父類得到幫助

Python中類的初始化方法是_init_(),因此父類、子類的初始化方法都是這個,如果子類不實現_init_()這個函式,初始化時呼叫父類的初始化函式.

如果子類實現了這個函式,則要在這個函式裡顯式呼叫一下父類的_init_(),這跟C++,Java不一樣,他們是自動呼叫父類建構函式的

#初始化中呼叫父類初始化方法示例  
#B是A的子類  
class B(A):    
    def __init__(self):    
        super().__init__()  

呼叫父類其他成員函式的三種方法:
1. 直接寫類名呼叫 (A._init_(self));
2. 用 super(type, obj).method(arg)方法呼叫 (super(B, obj)._init_());
3. 在子類的定義內,如果呼叫父類的成員,可以直接用 super().method(arg) (super()._init_())。

class A:    
    def method(self, arg):      
         return  

class B(A):    
    def method(self, arg):    
#        A.method(self,arg)                #1     
#        super(B, self).method(arg)        #2     
#        super().method(arg)               #3   

[注意] 如果在子類定義外(即在其他函式邏輯內,子類物件去呼叫父類成員時),則按照:

ob = B()    
super(B,ob).method(arg)    #呼叫class B的父類class A的method。   

在python中引入super()的目的是保證相同的基類只初始化一次

  1. super()機制是用來解決多重繼承的,對於直接呼叫父類名是沒有問題的,但在之後根據前人的經驗就是:要麼都用類名呼叫,要麼就全部用super(),不要混合的用
  2. super()繼承只能用於新式類,用於經典類時就會報錯。
    • 新式類:必須有繼承的類,如果無繼承的,則繼承object(即起源類仍要繼承object物件 class A(object):)
    • 經典類:沒有父類,如果此時呼叫super就會出現錯誤:(super() argument 1 must be type, not classobj)

利用類名進行呼叫

# parent1與parent2繼承object
class parent1(object):  
    def __init__(self):  
        print 'is parent1'  
        print 'goes parent1'  

class parent2(object):  
    def __init__(self):  
        print 'is parent2'  
        print 'goes parent2'  

class child1(parent1):  
    def __init__(self):  
        print'is child1'  
        parent.__init__(self)  
        print 'goes child1'  

class child2 (parent1) :  
    def __init__(self):  
        print 'is child2'  
        parent.__init__(self)  
        print 'goes child2'  

class child3(parent2):  
    def __init__(self):  
        print 'is child3'  
        parent2.__init__(self)  
        print 'goes child3'  

class grandson(child3,child2,child1):  
    def __init__(self):  
        print 'is grandson'  
        child1.__init__(self)  
        child2.__init__(self)  
        child3.__init__(self)  
        print'goes grandson'  


if __name__=='__main__':  
    grandson()  

結果:

# 基類parent1被多次執行
is grandson  
is child1  
is parent1  
goes parent1  
goes child1  
is child2  
is parent1  
goes parent1  
goes child2  
is child3  
is parent2  
goes parent2  
goes child3  
goes grandson  

利用super()進行呼叫(更多)

有時我們只希望公共的類只被執行一次,那麼此時我們引入super()機制檢視效果:

class parent1(object):
    def __init__(self):  
        super(parent1, self).__init__()  
        print 'is parent1'  
        print 'goes parent1'  

class parent2(object):  
    def __init__(self):  
        super(parent2, self).__init__()  
        print 'is parent2'  
        print 'goes parent2'  

class child1(parent1):  
    def __init__(self):  
        print'is child1'  
        #parent1.__init__(self)  
        super(child1, self).__init__()  
        print 'goes child1'  

class child2 (parent1) :  
    def __init__(self):  
        print 'is child2'  
        #parent1.__init__(self)  
        super(child2, self).__init__()  
        print 'goes child2'  

class child3(parent2):  
    def __init__(self):  
        print 'is child3'  
        #parent2.__init__(self)  
        super(child3, self).__init__()  
        print 'goes child3'  

class grandson(child3,child2,child1):  
    def __init__(self):  
        print 'is grandson'  
        #child1.__init__(self)  
        #child2.__init__(self)  
        #child3.__init__(self)  
        super(grandson, self).__init__()  

        print'goes grandson'  


if __name__=='__main__':  
    grandson()  

結果:

is grandson  
is child3  
is child2  
is child1  
is parent1  
goes parent1  
goes child1  
goes child2  
is parent2  
goes parent2  
goes child3  
goes grandson  

分析:
1. grandson
2. grandson [child3]
3. grandson [child3, child2]
4. grandson [child3, child2, child1]
5. grandson [child3, child2] || child1[parent1]
6. grandson [child3, child2] || child1
7. grandson [child3] || child2[parent1] <==> child2
8. grandson || child3[parent2]
9. grandson || child3
10. grandson
這裡寫圖片描述

使用屬性對特性進行訪問和設定

有一些面向物件的語言支援私有特性(如C++)。這些特性無法從物件外部直接訪問,我們需要編寫getter和setter方法對這些私有特性進行讀寫操作。

Python不需要getter和setter方法,因為Python裡所有特性都是公開的,使用時全憑自覺。

如果你不放心直接訪問物件的特性,可以為物件編寫setter和getter方法。但更具Python風格的解決方案是使用屬性(property)

定義屬性方法1:通過property方法把getter方法和setter方法設定為name屬性:

這裡寫圖片描述

定義屬性方法2:使用裝飾器

  • @property,用於指示getter方法
  • @name.setter,用於指示setter方法

這裡寫圖片描述

屬性還可以指向一個計算結果值

class Circle():
    def __init__(self, radius):
        self.radius = radius
    @property
    def diameter(self):
        return 2 * self.radius
>>> c = Circle(5)
>>> c.radius
5
>>> c.diameter
10
>>> # 我們可以隨時改變radius特性的值,計算屬性diameter會自動根據新的值更新自己,因為本質是對getter方法的呼叫
>>> c.radius = 7
>>> c.diameter
>>> 14

因此,與直接訪問特性相比(如令diameter為特性),使用property還有一個巨大的優勢:如果你改變了某個特性的定義,只需要在類的定義裡修改相關程式碼即可,不需要再每一處修改。例如,修改特性radius,屬性diameter不需要修改,特性diameter則需要手動修改。

使用名稱重整保護私有特性

前面的Duck例子中,為了隱藏內部特性,我們曾將其命名為hidden_name,但hidden_name特性仍然能被訪問到。其實Python對那些需要可以隱藏在類內部的特性有自己的命名規範:由連續的兩個下劃線開頭(__)

我們來把hidden_name改名為__name,如下所示:
這裡寫圖片描述
這種命名規範本質上並沒有把特性變成私有,但Python確實將它的名字重整了,讓外部的程式碼無法使用。

>>> #其實__name重整為_Duck__name
>>> fowl._Duck__name
'Donald'

儘管如我們所見,這種保護特性的方式並不完美,但它確實能在一定程度上避免我們有意無意地對特性進行直接訪問。

方法的型別

  • 有些資料(特性)和函式(方法)是類本身的一部分(如 a = A() A.method()中的method)
    • 類方法(class method)會作用於整個類,對類作出的任何改變會對它的所有例項物件產生影響。在類定義內部,用字首裝飾器@classmethod指定的方法都是類方法。
    • 與例項方法類似,類方法的第一個引數是類本身。在Python中,這個引數常被寫作cls,因為全稱class是保留字,在這裡我們無法使用。
  • 有一些資料(特性)和函式(方法)是由類建立的例項的一部分(如 a = A() a.method()中的method)
    • 在類的定義中,以self作為第一個引數的方法都是例項方法(instance method)。它們在建立自定義類時最常用。例項方法的首個引數是self,當它被呼叫時,Python會把呼叫該方法的物件作為self引數傳入。

這裡寫圖片描述

注意,上面的程式碼中,我們使用的是A.count(類特性),而不是self.count(物件的特性)。在kids()方法中,我們使用的是cls.count,它與A.count的作用一樣。

類特性與物件特性耦合

這裡寫圖片描述

靜態方法(static method)

類的定義中的方法還存在著第三種類型,它既不會影響類也不會影響類的方法。它們出現在類的定義中僅僅是為了方便,否則它們智慧孤零零地出現在程式碼的其他地方,這會影響程式碼的邏輯性。這種型別的方法被稱作靜態方法(static method),用@staticmethod修飾,它既不需要self引數也不需要class引數

這裡寫圖片描述

注意:你可以發現Python是一種太靈活的語言,所以很容易出問題

Python的特殊方法(special method),有時也被稱作魔術方法(magic method)(更多

  • 和比較相關的魔術方法
方法名 使用
__eq__(self, other) self == other
__ne__(self, other) self != other
__lt__(self, other) self < other
__gt__(self, other) self > other
__le__(self, other) self <= other
__ge__(self, other) self >= other

這裡寫圖片描述

  • 和數學相關的魔術方法
方法名 使用
__add__(self, other) self + other
__sub__(self, other) self - other
__mul__(self, other) self * other
__floordiv__(self, other) self // other
__truediv__(self, other) self / other
__mod__(self, other) self % other
__pow__(self, other) self ** other

這裡寫圖片描述

  • 其它種類的魔術方法
方法名 使用
__str__(self) str(self)
__repr__(self) repr(self)
__len__(self) len(self)

這裡寫圖片描述

關於__str__與__repr__

__str__用於定義如何列印物件資訊。print()方法,str()方法以及你將在第7章讀到關於字串格式化的相關方法都會用到__str__()

__repr__用於互動式直譯器輸出變數

例子:
如果在你的類既沒有定義__str()也沒有定義__repr(),Python會輸出類似下面這樣的預設字串:
這裡寫圖片描述

我們將__str__()和__repr__方法都新增到Word類裡,讓輸出的物件資訊變得更好看些
這裡寫圖片描述

組合

繼承: is-a 關係
組合: has-a關係
這裡寫圖片描述

何時使用類和物件而不是模組

有一些方法可以幫助你決定是把你的程式碼封裝到類裡還是模組裡。

# 有空補個更好的例子
#singleton.py
count = 0

def add():
    global count
    count += 1

def getCount():
    global count
    return count

使用:

#test.py
import singleton
a = singleton
a.add()
b = singleton
print a
print b
print a.getCount()
print b.getCount()

結果:

<module 'singleton' from '/Users/bin/Desktop/singleton.pyc'>
<module 'singleton' from '/Users/bin/Desktop/singleton.pyc'>
1
1
  • 如果你有一系列包含多個值的變數,並且它們能作為引數傳入不同的函式,那麼最好將它們封裝到類裡面。
  • 用最簡單的方式解決問題。使用字典、列表和元組往往要比使用模組更加簡單、簡潔而快速。而使用類則更為複雜。

命名元組

命名元組是元組的子類,你既可以通過名稱(使用.name)來訪問其中的值,也可以通過位置進行訪問(使用[offset]
這裡寫圖片描述
也可以用字典來構造一個命名元組:

>>> parts = {'bill': 'wide orange', 'tail': 'long'}
>>> # 等價於 duck2 = Duck(bill = 'wide orange', tail = 'long')
>>> duck2 = Duck(**parts)
>>> duck2
Duck(bill = 'wide orange', tail = 'long')

對比命名元組和字典:
這裡寫圖片描述