采用面向?qū)ο蟮某绦蛟O(shè)計(jì)思想,我們首選思考的不是程序的執(zhí)行流程悠咱,而是Student
這種數(shù)據(jù)類(lèi)型應(yīng)該被視為一個(gè)對(duì)象蒸辆,這個(gè)對(duì)象擁有name
和score
這兩個(gè)屬性
(Property
)。如果要打印一個(gè)學(xué)生的成績(jī)析既,首先必須創(chuàng)建出這個(gè)學(xué)生對(duì)應(yīng)的對(duì)象躬贡,然后,給對(duì)象發(fā)一個(gè)print_score
消息眼坏,讓對(duì)象自己把自己的數(shù)據(jù)打印出來(lái)拂玻。
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('%s: %s' % (self.name, self.score))
給對(duì)象發(fā)消息實(shí)際上就是調(diào)用對(duì)象對(duì)應(yīng)的關(guān)聯(lián)函數(shù),我們稱(chēng)之為對(duì)象的方法(Method
)宰译。面向?qū)ο蟮某绦驅(qū)懗鰜?lái)就像這樣:
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
類(lèi)和實(shí)例
面向?qū)ο笞钪匾母拍罹褪?code>類(lèi)(Class)和實(shí)例(Instance)
檐蚜,必須牢記類(lèi)是抽象的模板,比如Student
類(lèi)沿侈,而實(shí)例是根據(jù)類(lèi)創(chuàng)建出來(lái)的一個(gè)個(gè)具體的“對(duì)象”闯第,每個(gè)對(duì)象都擁有相同的方法,但各自的數(shù)據(jù)可能不同缀拭。
仍以Student類(lèi)為例咳短,在Python中,定義類(lèi)是通過(guò)class關(guān)鍵字:
class Student(object):
pass
class
后面緊接著是類(lèi)名智厌,即Student
诲泌,類(lèi)名通常是大寫(xiě)開(kāi)頭的單詞,緊接著是(object
)铣鹏,表示該類(lèi)是從哪個(gè)類(lèi)繼承下來(lái)的敷扫,繼承的概念我們后面再講,通常诚卸,如果沒(méi)有合適的繼承類(lèi)葵第,就使用object
類(lèi),這是所有類(lèi)最終都會(huì)繼承的類(lèi)合溺。
定義好了Student
類(lèi)卒密,就可以根據(jù)Student
類(lèi)創(chuàng)建出Student
的實(shí)例,創(chuàng)建實(shí)例是通過(guò)類(lèi)名+()實(shí)現(xiàn)的:
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
可以看到棠赛,變量bart
指向的就是一個(gè)Student
的實(shí)例哮奇,后面的0x10a67a590
是內(nèi)存地址,每個(gè)object
的地址都不一樣睛约,而Student
本身則是一個(gè)類(lèi)鼎俘。
可以自由地給一個(gè)實(shí)例變量綁定屬性,比如辩涝,給實(shí)例bart
綁定一個(gè)name
屬性:
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'
由于類(lèi)可以起到模板的作用贸伐,因此,可以在創(chuàng)建實(shí)例的時(shí)候怔揩,把一些我們認(rèn)為必須綁定的屬性強(qiáng)制填寫(xiě)進(jìn)去捉邢。通過(guò)定義一個(gè)特殊的__init__
方法脯丝,在創(chuàng)建實(shí)例的時(shí)候,就把name
伏伐,score
等屬性綁上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意到__init__
方法的第一個(gè)參數(shù)永遠(yuǎn)是self
宠进,表示創(chuàng)建的實(shí)例本身,因此藐翎,在__init__
方法內(nèi)部砰苍,就可以把各種屬性綁定到self
,因?yàn)?code>self就指向創(chuàng)建的實(shí)例本身阱高。
有了__init__
方法,在創(chuàng)建實(shí)例的時(shí)候茬缩,就不能傳入空的參數(shù)了赤惊,必須傳入與__init_
_方法匹配的參數(shù),但self
不需要傳凰锡,Python解釋器自己會(huì)把實(shí)例變量傳進(jìn)去:
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
和普通的函數(shù)相比未舟,在類(lèi)中定義的函數(shù)只有一點(diǎn)不同,就是第一個(gè)參數(shù)永遠(yuǎn)是實(shí)例變量self
掂为,并且裕膀,調(diào)用時(shí),不用傳遞該參數(shù)勇哗。除此之外昼扛,類(lèi)的方法和普通函數(shù)沒(méi)有什么區(qū)別,所以欲诺,你仍然可以用默認(rèn)參數(shù)
抄谐、可變參數(shù)
、關(guān)鍵字參數(shù)
和命名關(guān)鍵字參數(shù)
扰法。
-
數(shù)據(jù)封裝
面向?qū)ο缶幊痰囊粋€(gè)重要特點(diǎn)就是數(shù)據(jù)封裝蛹含。在類(lèi)的內(nèi)部做輸出處理的操作:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('%s: %s' % (self.name, self.score))
在類(lèi)的內(nèi)部要定義一個(gè)方法溢十,除了第一個(gè)參數(shù)是self外硝皂,其他和普通函數(shù)一樣诵盼。要調(diào)用一個(gè)方法隙袁,只需要在實(shí)例變量上直接調(diào)用遮咖,除了self
不用傳遞谓谦,其他參數(shù)正常傳入:
>>> bart.print_score()
Bart Simpson: 59
這樣一來(lái)继找,我們從外部看Studen
t類(lèi)忍捡,就只需要知道锤岸,創(chuàng)建實(shí)例需要給出name
和score
竖幔,而如何打印,都是在Student
類(lèi)的內(nèi)部定義的是偷,這些數(shù)據(jù)和邏輯被“封裝”起來(lái)了拳氢,調(diào)用很容易募逞,但卻不用知道內(nèi)部實(shí)現(xiàn)的細(xì)節(jié)。
訪問(wèn)限制
從安全性來(lái)講,外部最好不能修改類(lèi)內(nèi)部的一些參數(shù).如果要讓內(nèi)部屬性不被外部訪問(wèn)馋评,可以把屬性的名稱(chēng)前加上兩個(gè)下劃線__
放接,在Python
中,實(shí)例的變量名如果以__
開(kāi)頭留特,就變成了一個(gè)私有變量(private
)纠脾,只有內(nèi)部可以訪問(wèn),外部不能訪問(wèn)蜕青,所以苟蹈,我們把Student
類(lèi)改一改:
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
改完后,對(duì)于外部代碼來(lái)說(shuō)右核,沒(méi)什么變動(dòng)慧脱,但是已經(jīng)無(wú)法從外部訪問(wèn)實(shí)例變量.__name
和實(shí)例變量.__score
了:
這樣就確保了外部代碼不能隨意修改對(duì)象內(nèi)部的狀態(tài),這樣通過(guò)訪問(wèn)限制的保護(hù)贺喝,代碼更加健壯菱鸥。
如果想要訪問(wèn)內(nèi)部的參數(shù)怎么辦?
可以給Student類(lèi)增加get_name
和get_score
這樣的方法:
#類(lèi)似OC的get方法
class Student(object):
...
def get_name(self):
return self.__name
def get_score(self):
return self.__score
如果又要允許外部代碼修改score
怎么辦?可以再給Student
類(lèi)增加set_score
方法:
#類(lèi)似OC的set方法
class Student(object):
...
def set_score(self, score):
self.__score = score
為什么要定義一個(gè)方法大費(fèi)周折躏鱼?因?yàn)樵诜椒ㄖ械桑梢詫?duì)參數(shù)做檢查,避免傳入無(wú)效的參數(shù):
class Student(object):
...
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
繼承和多態(tài)
靜態(tài)語(yǔ)言 vs 動(dòng)態(tài)語(yǔ)言
對(duì)于靜態(tài)語(yǔ)言(例如Java)來(lái)說(shuō)染苛,如果需要傳入Animal
類(lèi)型鹊漠,則傳入的對(duì)象必須是Animal
類(lèi)型或者它的子類(lèi),否則殖侵,將無(wú)法調(diào)用run()
方法贸呢。
對(duì)于Python
這樣的動(dòng)態(tài)語(yǔ)言來(lái)說(shuō),則不一定需要傳入Animal
類(lèi)型拢军。我們只需要保證傳入的對(duì)象有一個(gè)run()
方法就可以了:
class Animal(object):
def run(self):
print('Animal is running...')
class tiwce(object):
def run(self):
print('123')
def run_twice(animal):#靜態(tài)語(yǔ)言必須傳入Animal的類(lèi)型或者它的子類(lèi)型,但是動(dòng)態(tài)語(yǔ)言傳入有run()方法的類(lèi)型就可以
animal.run()
animal.run()
run_twice(tiwce())
這就是動(dòng)態(tài)語(yǔ)言的“鴨子類(lèi)型”楞陷,它并不要求嚴(yán)格的繼承體系,一個(gè)對(duì)象只要“看起來(lái)像鴨子茉唉,走起路來(lái)像鴨子”固蛾,那它就可以被看做是鴨子。
獲取對(duì)象信息
當(dāng)我們拿到一個(gè)對(duì)象的引用時(shí)度陆,如何知道這個(gè)對(duì)象是什么類(lèi)型艾凯、有哪些方法呢?
- 使用
type()
首先懂傀,我們來(lái)判斷對(duì)象類(lèi)型趾诗,使用type()
函數(shù):
基本類(lèi)型都可以用type()
判斷:
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
- 使用
isinstance()
對(duì)于class
的繼承關(guān)系來(lái)說(shuō),使用type()
就很不方便。我們要判斷class
的類(lèi)型恃泪,可以使用isinstance()
函數(shù)郑兴。
object -> Animal -> Dog -> Husky
那么,isinstance()
就可以告訴我們贝乎,一個(gè)對(duì)象是否是某種類(lèi)型情连。先創(chuàng)建3種類(lèi)型的對(duì)象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
>>> isinstance(h, Husky)
True
>>> isinstance(h, Dog)
True
>>> isinstance(h, Animal)
True
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True
- 使用
dir()
如果要獲得一個(gè)對(duì)象的所有屬性和方法,可以使用dir()
函數(shù)览效,它返回一個(gè)包含字符串的list
却舀,比如,獲得一個(gè)str
對(duì)象的所有屬性和方法:
>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
僅僅把屬性和方法列出來(lái)是不夠的锤灿,配合getattr()
挽拔、setattr()
以及hasattr()
,我們可以直接操作一個(gè)對(duì)象的狀態(tài):
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()
緊接著但校,可以測(cè)試該對(duì)象的屬性:
>>> hasattr(obj, 'x') # 有屬性'x'嗎篱昔?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
False
>>> setattr(obj, 'y', 19) # 設(shè)置一個(gè)屬性'y'
>>> hasattr(obj, 'y') # 有屬性'y'嗎始腾?
True
>>> getattr(obj, 'y') # 獲取屬性'y'
19
>>> obj.y # 獲取屬性'y'
19
如果試圖獲取不存在的屬性,會(huì)拋出AttributeError
的錯(cuò)誤:
>>> getattr(obj, 'z') # 獲取屬性'z'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
可以傳入一個(gè)default參數(shù)空执,如果屬性不存在浪箭,就返回默認(rèn)值:
>>> getattr(obj, 'z', 404) # 獲取屬性'z',如果不存在辨绊,返回默認(rèn)值404
404
也可以獲得對(duì)象的方法:
>>> hasattr(obj, 'power') # 有屬性'power'嗎奶栖?
True
>>> getattr(obj, 'power') # 獲取屬性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 獲取屬性'power'并賦值到變量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 調(diào)用fn()與調(diào)用obj.power()是一樣的
81
要注意的是,只有在不知道對(duì)象信息的時(shí)候门坷,我們才會(huì)去獲取對(duì)象信息宣鄙。如果知道就直接寫(xiě):
sum = obj.x + obj.y
一個(gè)正確的用法的例子如下:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
假設(shè)我們希望從文件流fp
中讀取圖像,我們首先要判斷該fp
對(duì)象是否存在read
方法默蚌,如果存在冻晤,則該對(duì)象是一個(gè)流,如果不存在绸吸,則無(wú)法讀取鼻弧。hasattr()
就派上了用場(chǎng)。
實(shí)例屬性和類(lèi)屬性
給實(shí)例綁定屬性的方法是通過(guò)實(shí)例變量锦茁,或者通過(guò)self變量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
但是攘轩,如果Student類(lèi)本身需要綁定一個(gè)屬性呢?可以直接在class中定義屬性码俩,這種屬性是類(lèi)屬性度帮,歸Student類(lèi)所有:
class Student(object):
name = 'Student'
當(dāng)我們定義了一個(gè)類(lèi)屬性后,這個(gè)屬性雖然歸類(lèi)所有稿存,但類(lèi)的所有實(shí)例都可以訪問(wèn)到笨篷。來(lái)測(cè)試一下
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 創(chuàng)建實(shí)例s
>>> print(s.name) # 打印name屬性瞳秽,因?yàn)閷?shí)例并沒(méi)有name屬性,所以會(huì)繼續(xù)查找class的name屬性
Student
>>> print(Student.name) # 打印類(lèi)的name屬性
Student
>>> s.name = 'Michael' # 給實(shí)例綁定name屬性
>>> print(s.name) # 由于實(shí)例屬性?xún)?yōu)先級(jí)比類(lèi)屬性高冕屯,因此寂诱,它會(huì)屏蔽掉類(lèi)的name屬性
Michael
>>> print(Student.name) # 但是類(lèi)屬性并未消失,用Student.name仍然可以訪問(wèn)
Student
>>> del s.name # 如果刪除實(shí)例的name屬性
>>> print(s.name) # 再次調(diào)用s.name安聘,由于實(shí)例的name屬性沒(méi)有找到痰洒,類(lèi)的name屬性就顯示出來(lái)了
Student
從上面的例子可以看出,在編寫(xiě)程序的時(shí)候浴韭,千萬(wàn)不要把實(shí)例屬性和類(lèi)屬性使用相同的名字丘喻,因?yàn)橄嗤Q(chēng)的實(shí)例屬性將屏蔽掉類(lèi)屬性,但是當(dāng)你刪除實(shí)例屬性后念颈,再使用相同的名稱(chēng)泉粉,訪問(wèn)到的將是類(lèi)屬性。