1. 程式人生 > >Python基礎學習——類

Python基礎學習——類

class Student(object):
    pass

可以自由地給一個例項變數繫結屬性,比如,給例項bart繫結一個name屬性:

>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'

或者通過__init__(self)類函式實現。

如果要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下劃線__,在Python中,例項的變數名如果以__開頭,就變成了一個私有變數(private),只有內部可以訪問,外部不能直接通過“例項名.屬性名”這種方式, 只能通過內部函式進行訪問或修改。

原先那種直接通過bart.score = 99也可以修改啊,為什麼要定義一個方法大費周折?因為在方法中,可以對引數做檢查,避免傳入無效的引數:

class Student(object):
    ...

    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

需要注意的是,在Python中,變數名類似__xxx__的,也就是以雙下劃線開頭,並且以雙下劃線結尾的,是特殊變數,特殊變數是可以直接訪問的,不是private變數,所以,不能用__name__

__score__這樣的變數名。

有些時候,你會看到以一個下劃線開頭的例項變數名,比如_name,這樣的例項變數外部是可以訪問的,但是,按照約定俗成的規定,當你看到這樣的變數時,意思就是,“雖然我可以被訪問,但是,請把我視為私有變數,不要隨意訪問”。

雙下劃線開頭的例項變數是不是一定不能從外部訪問呢?其實也不是。不能直接訪問__name是因為Python直譯器對外把__name變數改成了_Student__name,所以,仍然可以通過_Student__name來訪問__name變數:

>>> bart._Student__name
'Bart Simpson'

但是強烈建議你不要這麼幹,因為不同版本的Python直譯器可能會把__name改成不同的變數名。

總的來說就是,Python本身沒有任何機制阻止你幹壞事,一切全靠自覺。

最後注意下面的這種錯誤寫法:

>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 設定__name變數!
>>> bart.__name
'New Name'

表面上看,外部程式碼“成功”地設定了__name變數,但實際上這個__name變數和class內部的__name變數不是一個變數!內部的__name變數已經被Python直譯器自動改成了_Student__name,而外部程式碼給bart新增了一個__name變數。

繼承和多型

當子類和父類都存在相同的run()方法時,我們說,子類的run()覆蓋了父類的run(),在程式碼執行的時候,總是會呼叫子類的run()。這樣,我們就獲得了繼承的另一個好處:多型。

要理解什麼是多型,我們首先要對資料型別再作一點說明。當我們定義一個class的時候,我們實際上就定義了一種資料型別。我們定義的資料型別和Python自帶的資料型別,比如str、list、dict沒什麼兩樣:

a = list() # a是list型別
b = Animal() # b是Animal型別
c = Dog() # c是Dog型別
>>> isinstance(c, Animal)
True

看來c不僅僅是Dogc還是Animal

不過仔細想想,這是有道理的,因為Dog是從Animal繼承下來的,當我們建立了一個Dog的例項c時,我們認為c的資料型別是Dog沒錯,但c同時也是Animal也沒錯,Dog本來就是Animal的一種!

所以,在繼承關係中,如果一個例項的資料型別是某個子類,那它的資料型別也可以被看做是父類,但是,反過來就不行。

要理解多型的好處,我們還需要再編寫一個函式,這個函式接受一個Animal型別的變數:

def run_twice(animal):
    animal.run()
    animal.run()

你會發現,新增一個Animal的子類,不必對run_twice()做任何修改,實際上,任何依賴Animal作為引數的函式或者方法都可以不加修改地正常執行,原因就在於多型。

多型的好處就是,當我們需要傳入DogCatTortoise……時,我們只需要接收Animal型別就可以了,因為DogCatTortoise……都是Animal型別,然後,按照Animal型別進行操作即可。由於Animal型別有run()方法,因此,傳入的任意型別,只要是Animal類或者子類,就會自動呼叫實際型別的run()方法,這就是多型的意思:

對於一個變數,我們只需要知道它是Animal型別,無需確切地知道它的子型別,就可以放心地呼叫run()方法,而具體呼叫的run()方法是作用在AnimalDogCat還是Tortoise物件上,由執行時該物件的確切型別決定,這就是多型真正的威力:呼叫方只管呼叫,不管細節,而當我們新增一種Animal的子類時,只要確保run()方法編寫正確,不用管原來的程式碼是如何呼叫的。這就是著名的“開閉”原則:

對擴充套件開放:允許新增Animal子類;

對修改封閉:不需要修改依賴Animal型別的run_twice()等函式。

靜態語言 vs 動態語言

對於靜態語言(例如Java)來說,如果需要傳入Animal型別,則傳入的物件必須是Animal型別或者它的子類,否則,將無法呼叫run()方法。

對於Python這樣的動態語言來說,則不一定需要傳入Animal型別。我們只需要保證傳入的物件有一個run()方法就可以了:

class Timer(object):
    def run(self):
        print('Start...')

這就是動態語言的“鴨子型別”,它並不要求嚴格的繼承體系,一個物件只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。

Python的“file-like object“就是一種鴨子型別。對真正的檔案物件,它有一個read()方法,返回其內容。但是,許多物件,只要有read()方法,都被視為“file-like object“。許多函式接收的引數就是“file-like object“,你不一定要傳入真正的檔案物件,完全可以傳入任何實現了read()方法的物件。

 

我們來判斷物件型別,使用type()函式,它返回對應的Class型別。

>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True

如果要判斷一個物件是否是函式怎麼辦?可以使用types模組中定義的常量:

>>> import types
>>> def fn():
...     pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

對於class的繼承關係來說,使用type()就很不方便。我們要判斷class的型別,可以使用isinstance()函式,能用type()判斷的基本型別也可以用isinstance()判斷。並且還可以判斷一個變數是否是某些型別中的一種,比如下面的程式碼就可以判斷是否是list或者tuple:

>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True

如果要獲得一個物件的所有屬性和方法,可以使用dir()函式,它返回一個包含字串的list,僅僅把屬性和方法列出來是不夠的,配合getattr()setattr()以及hasattr(),我們可以直接操作一個物件的狀態:

>>> hasattr(obj, 'x') # 有屬性'x'嗎?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
False
>>> setattr(obj, 'y', 19) # 設定一個屬性'y'
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
True
>>> getattr(obj, 'y') # 獲取屬性'y'
19
>>> obj.y # 獲取屬性'y'
19

可以傳入一個default引數,如果屬性不存在,就返回預設值:

>>> getattr(obj, 'z', 404) # 獲取屬性'z',如果不存在,返回預設值404
404

 

 

 

 

 

 

 

 

 

 

 

 

 

 

-

 

 

 

 

-