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
不僅僅是Dog
,c
還是Animal
!
不過仔細想想,這是有道理的,因為Dog
是從Animal
繼承下來的,當我們建立了一個Dog
的例項c
時,我們認為c
的資料型別是Dog
沒錯,但c
同時也是Animal
也沒錯,Dog
本來就是Animal
的一種!
所以,在繼承關係中,如果一個例項的資料型別是某個子類,那它的資料型別也可以被看做是父類,但是,反過來就不行。
要理解多型的好處,我們還需要再編寫一個函式,這個函式接受一個Animal
型別的變數:
def run_twice(animal):
animal.run()
animal.run()
你會發現,新增一個Animal
的子類,不必對run_twice()
做任何修改,實際上,任何依賴Animal
作為引數的函式或者方法都可以不加修改地正常執行,原因就在於多型。
多型的好處就是,當我們需要傳入Dog
、Cat
、Tortoise
……時,我們只需要接收Animal
型別就可以了,因為Dog
、Cat
、Tortoise
……都是Animal
型別,然後,按照Animal
型別進行操作即可。由於Animal
型別有run()
方法,因此,傳入的任意型別,只要是Animal
類或者子類,就會自動呼叫實際型別的run()
方法,這就是多型的意思:
對於一個變數,我們只需要知道它是Animal
型別,無需確切地知道它的子型別,就可以放心地呼叫run()
方法,而具體呼叫的run()
方法是作用在Animal
、Dog
、Cat
還是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
-
-