面向?qū)ο缶幊?/h2>
面向?qū)ο缶幊獭狾bject Oriented Programming霉撵,簡稱OOP
,是一種程序設(shè)計(jì)思想洪囤。OOP把對(duì)象作為程序的基本單元徒坡,一個(gè)對(duì)象包含了數(shù)據(jù)
和操作數(shù)據(jù)的函數(shù)
。
面向過程的程序設(shè)計(jì)把計(jì)算機(jī)程序視為一系列的命令集合瘤缩,即一組函數(shù)的順序執(zhí)行。為了簡化程序設(shè)計(jì)剥啤,面向過程把函數(shù)繼續(xù)切分為子函數(shù)何暮,即把大塊函數(shù)通過切割成小塊函數(shù)來降低系統(tǒng)的復(fù)雜度。
而面向?qū)ο蟮某绦蛟O(shè)計(jì)把計(jì)算機(jī)程序視為一組對(duì)象的集合铐殃,而每個(gè)對(duì)象都可以接收其他對(duì)象發(fā)過來的消息,并處理這些消息跨新,計(jì)算機(jī)程序的執(zhí)行就是一系列消息在各個(gè)對(duì)象之間傳遞富腊。
在Python中,所有數(shù)據(jù)類型都可以視為對(duì)象
域帐,當(dāng)然也可以自定義對(duì)象赘被。自定義的對(duì)象數(shù)據(jù)類型就是面向?qū)ο笾械?code>類(Class)的概念是整。
我們以一個(gè)例子來說明面向過程和面向?qū)ο笤诔绦蛄鞒躺系牟煌帯?/p>
假設(shè)我們要處理學(xué)生的成績表,為了表示一個(gè)學(xué)生的成績民假,面向過程的程序可以用一個(gè)dict表示:
std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }
而處理學(xué)生成績可以通過函數(shù)實(shí)現(xiàn)浮入,比如打印學(xué)生的成績:
def print_score(std):
print('%s: %s' % (std['name'], std['score']))
如果采用面向?qū)ο蟮某绦蛟O(shè)計(jì)思想,我們首選思考的不是程序的執(zhí)行流程羊异,而是Student
這種數(shù)據(jù)類型應(yīng)該被視為一個(gè)對(duì)象,這個(gè)對(duì)象擁有name
和score
這兩個(gè)屬性(Property)。如果要打印一個(gè)學(xué)生的成績峡钓,首先必須創(chuàng)建出這個(gè)學(xué)生對(duì)應(yīng)的對(duì)象巾遭,然后,給對(duì)象發(fā)一個(gè)print_score
消息平道,讓對(duì)象自己把自己的數(shù)據(jù)打印出來睹欲。
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ù),我們稱之為對(duì)象的方法
(Method)一屋。面向?qū)ο蟮某绦驅(qū)懗鰜砭拖襁@樣:
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
面向?qū)ο蟮脑O(shè)計(jì)思想是從自然界中來的窘疮,因?yàn)樵谧匀唤缰校悾–lass)和實(shí)例(Instance)的概念是很自然的冀墨。Class是一種抽象概念闸衫,比如我們定義的Class——Student,是指學(xué)生這個(gè)概念轧苫,而實(shí)例
(Instance)則是一個(gè)個(gè)具體的Student楚堤,比如,Bart Simpson和Lisa Simpson是兩個(gè)具體的Student含懊。
所以身冬,面向?qū)ο蟮脑O(shè)計(jì)思想是抽象出Class
,根據(jù)Class
創(chuàng)建Instance
岔乔。
面向?qū)ο蟮某橄蟪潭扔直群瘮?shù)要高酥筝,因?yàn)橐粋€(gè)Class既包含數(shù)據(jù),又包含操作數(shù)據(jù)的方法雏门。
類和實(shí)例
面向?qū)ο笞钪匾母拍罹褪?code>類(Class)和實(shí)例
(Instance)嘿歌,必須牢記類是抽象的模板,比如Student類茁影,而實(shí)例是根據(jù)類創(chuàng)建出來的一個(gè)個(gè)具體的“對(duì)象”宙帝,每個(gè)對(duì)象都擁有相同的方法,但各自的數(shù)據(jù)可能不同募闲。
仍以Student類為例步脓,在Python中,定義類是通過class
關(guān)鍵字:
class Student(object):
pass
class
后面緊接著是類名,即Student
靴患,類名通常是大寫開頭的單詞仍侥,緊接著是(object
),表示該類是從哪個(gè)類繼承
下來的鸳君,繼承的概念我們后面再講农渊,通常,如果沒有合適的繼承類或颊,就使用object類砸紊,這是所有類最終都會(huì)繼承的類。
定義好了Student
類饭宾,就可以根據(jù)Student
類創(chuàng)建出Student
的實(shí)例批糟,創(chuàng)建實(shí)例是通過類名+()
實(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è)類弹惦。
可以自由地給一個(gè)實(shí)例變量綁定屬性否淤,比如,給實(shí)例bart綁定一個(gè)name屬性:
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'
由于類可以起到模板
的作用棠隐,因此石抡,可以在創(chuàng)建實(shí)例的時(shí)候,把一些我們認(rèn)為必須綁定的屬性強(qiáng)制填寫進(jìn)去助泽。通過定義一個(gè)特殊的__init__
方法啰扛,在創(chuàng)建實(shí)例的時(shí)候,就把name嗡贺,score等屬性綁上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意:特殊方法“
__init__
”前后分別有兩個(gè)下劃線R狻!诫睬!
注意到__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ù)相比,在類中定義的函數(shù)只有一點(diǎn)不同裹匙,就是第一個(gè)參數(shù)永遠(yuǎn)是實(shí)例變量self,并且末秃,調(diào)用時(shí)概页,不用傳遞該參數(shù)。除此之外练慕,類的方法和普通函數(shù)沒有什么區(qū)別惰匙,所以,你仍然可以用默認(rèn)參數(shù)铃将、可變參數(shù)项鬼、關(guān)鍵字參數(shù)和命名關(guān)鍵字參數(shù)。
數(shù)據(jù)封裝
面向?qū)ο缶幊痰囊粋€(gè)重要特點(diǎn)就是數(shù)據(jù)封裝劲阎。在上面的Student
類中绘盟,每個(gè)實(shí)例就擁有各自的name和score這些數(shù)據(jù)。我們可以通過函數(shù)來訪問這些數(shù)據(jù)悯仙,比如打印一個(gè)學(xué)生的成績:
>>> def print_score(std):
... print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
Bart Simpson: 59
但是龄毡,既然Student
實(shí)例本身就擁有這些數(shù)據(jù),要訪問這些數(shù)據(jù)锡垄,就沒有必要從外面的函數(shù)去訪問沦零,可以直接在Student
類的內(nèi)部定義訪問數(shù)據(jù)的函數(shù),這樣货岭,就把“數(shù)據(jù)”給封裝起來了路操。這些封裝數(shù)據(jù)的函數(shù)是和Student類本身是關(guān)聯(lián)起來的,我們稱之為類的方法:
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))
要定義一個(gè)方法千贯,除了第一個(gè)參數(shù)是self
外屯仗,其他和普通函數(shù)一樣。要調(diào)用一個(gè)方法丈牢,只需要在實(shí)例變量上直接調(diào)用祭钉,除了self
不用傳遞,其他參數(shù)正常傳入:
>>> bart.print_score()
Bart Simpson: 59
這樣一來己沛,我們從外部看Student
類慌核,就只需要知道,創(chuàng)建實(shí)例需要給出name
和score
申尼,而如何打印垮卓,都是在Student類的內(nèi)部定義的,這些數(shù)據(jù)和邏輯被“封裝”起來了师幕,調(diào)用很容易粟按,但卻不用知道內(nèi)部實(shí)現(xiàn)的細(xì)節(jié)诬滩。
封裝的另一個(gè)好處是可以給Student類增加新的方法,比如get_grade:
class Student(object):
...
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
else:
return 'C'
同樣的灭将,get_grade
方法可以直接在實(shí)例變量上調(diào)用疼鸟,不需要知道內(nèi)部實(shí)現(xiàn)細(xì)節(jié):
# -*- coding: utf-8 -*-
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
else:
return 'C'
類是創(chuàng)建實(shí)例的模板,而實(shí)例則是一個(gè)一個(gè)具體的對(duì)象庙曙,各個(gè)實(shí)例擁有的數(shù)據(jù)都互相獨(dú)立空镜,互不影響;
方法就是與實(shí)例綁定的函數(shù)捌朴,和普通函數(shù)不同吴攒,方法可以直接訪問實(shí)例的數(shù)據(jù);
通過在實(shí)例上調(diào)用方法砂蔽,我們就直接操作了對(duì)象內(nèi)部的數(shù)據(jù)洼怔,但無需知道方法內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)。
和靜態(tài)語言不同左驾,Python允許對(duì)實(shí)例變量綁定任何數(shù)據(jù)镣隶,也就是說,對(duì)于兩個(gè)實(shí)例變量什荣,雖然它們都是同一個(gè)類的不同實(shí)例矾缓,但擁有的變量名稱都可能不同:
訪問限制
在Class
內(nèi)部,可以有屬性
和方法
稻爬,而外部代碼可以通過直接調(diào)用實(shí)例變量的方法來操作數(shù)據(jù)嗜闻,這樣,就隱藏了內(nèi)部的復(fù)雜邏輯桅锄。
但是琉雳,從前面Student
類的定義來看,外部代碼還是可以自由地修改一個(gè)實(shí)例的name
友瘤、score
屬性:
>>> bart = Student('Bart Simpson', 59)
>>> bart.score
59
>>> bart.score = 99
>>> bart.score
99
如果要讓內(nèi)部屬性不被外部訪問翠肘,可以把屬性的名稱前加上兩個(gè)下劃線__
,在Python中辫秧,實(shí)例的變量名如果以__
開頭束倍,就變成了一個(gè)私有變量(private),只有內(nèi)部可以訪問盟戏,外部不能訪問绪妹,所以,我們把Student類改一改:
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ì)于外部代碼來說邮旷,沒什么變動(dòng),但是已經(jīng)無法從外部訪問實(shí)例變量.__name
和實(shí)例變量.__score
了:
>>> bart = Student('Bart Simpson', 59)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
這樣就確保了外部代碼不能隨意修改對(duì)象內(nèi)部的狀態(tài)蝇摸,這樣通過訪問限制的保護(hù)婶肩,代碼更加健壯办陷。
但是如果外部代碼要獲取name
和score
怎么辦?可以給Student
類增加get_name
和get_score
這樣的方法:
class Student(object):
...
def get_name(self):
return self.__name
def get_score(self):
return self.__score
如果又要允許外部代碼修改score怎么辦律歼?可以再給Student類增加set_score方法:
class Student(object):
...
def set_score(self, score):
self.__score = score
你也許會(huì)問民镜,原先那種直接通過bart.score = 99
也可以修改啊,為什么要定義一個(gè)方法大費(fèi)周折苗膝?因?yàn)樵诜椒ㄖ醒旰悖梢詫?duì)參數(shù)做檢查,避免傳入無效的參數(shù):
class Student(object):
...
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
需要注意的是辱揭,在Python
中,變量名類似__xxx__
的病附,也就是以雙下劃線開頭问窃,并且以雙下劃線結(jié)尾的,是特殊變量完沪,特殊變量是可以直接訪問的域庇,不是private變量,所以覆积,不能用__name__
听皿、__score__
這樣的變量名。
有些時(shí)候宽档,你會(huì)看到以一個(gè)下劃線開頭的實(shí)例變量名尉姨,比如_name
,這樣的實(shí)例變量外部是可以訪問的吗冤,但是又厉,按照約定俗成的規(guī)定,當(dāng)你看到這樣的變量時(shí)椎瘟,意思就是覆致,“雖然我可以被訪問,但是肺蔚,請(qǐng)把我視為私有變量煌妈,不要隨意訪問”。
雙下劃線開頭的實(shí)例變量是不是一定不能從外部訪問呢宣羊?其實(shí)也不是璧诵。不能直接訪問
__name
是因?yàn)镻ython解釋器對(duì)外把__name
變量改成了_Student__name
,所以段只,仍然可以通過_Student__name
來訪問__name
變量:
>>> bart._Student__name
'Bart Simpson'
但是強(qiáng)烈建議你不要這么干腮猖,因?yàn)椴煌姹镜腜ython解釋器可能會(huì)把__name
改成不同的變量名。
總的來說就是赞枕,Python
本身沒有任何機(jī)制阻止你干壞事澈缺,一切全靠自覺坪创。
最后注意下面的這種錯(cuò)誤寫法:
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 設(shè)置__name變量!
>>> bart.__name
'New Name'
表面上看姐赡,外部代碼“成功”地設(shè)置了
__name
變量莱预,但實(shí)際上這個(gè)__name
變量和class內(nèi)部的__name
變量不是一個(gè)變量!內(nèi)部的__name
變量已經(jīng)被Python解釋器自動(dòng)改成了_Student__name
项滑,而外部代碼給bart新增了一個(gè)__name
變量依沮。不信試試:
>>> bart.get_name() # get_name()內(nèi)部返回self.__name
'Bart Simpson'
請(qǐng)把下面的Student
對(duì)象的gender
字段對(duì)外隱藏起來,用get_gender()
和set_gender()
代替枪狂,并檢查參數(shù)有效性:
class Student(object):
def __init__(self, name, gender):
self.name = name
self.__gender = gender
def get_gender(self):
return self.__gender
def set_gender(self, gender):
self.__gender = gender
# 測試:
bart = Student('Bart', 'male')
if bart.get_gender() != 'male':
print('測試失敗!')
else:
bart.set_gender('female')
if bart.get_gender() != 'female':
print('測試失敗!')
else:
print('測試成功!')
繼承和多態(tài)
在OOP程序設(shè)計(jì)中危喉,當(dāng)我們定義一個(gè)class
的時(shí)候,可以從某個(gè)現(xiàn)有的class
繼承州疾,新的class
稱為子類
(Subclass)辜限,而被繼承的class
稱為基類
、父類
或超類
(Base class严蓖、Super class)薄嫡。
比如,我們已經(jīng)編寫了一個(gè)名為Animal
的class颗胡,有一個(gè)run()
方法可以直接打雍辽睢:
class Animal(object):
def run(self):
print('Animal is running...')
當(dāng)我們需要編寫Dog
和Cat
類時(shí),就可以直接從Animal類繼承:
class Dog(Animal):
pass
class Cat(Animal):
pass
對(duì)于Dog
來說毒姨,Animal
就是它的父類哑蔫,對(duì)于Animal來說,Dog
就是它的子類手素。Cat
和Dog
類似鸳址。
繼承有什么好處?最大的好處是子類獲得了父類的全部功能泉懦。由于Animial實(shí)現(xiàn)了run()方法稿黍,因此,Dog和Cat作為它的子類崩哩,什么事也沒干巡球,就自動(dòng)擁有了run()方法:
dog = Dog()
dog.run()
cat = Cat()
cat.run()
運(yùn)行結(jié)果如下:
Animal is running...
Animal is running...
當(dāng)然,也可以對(duì)子類增加一些方法邓嘹,比如Dog類:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
繼承的第二個(gè)好處需要我們對(duì)代碼做一點(diǎn)改進(jìn)酣栈。你看到了,無論是Dog
還是Cat
汹押,它們run()
的時(shí)候矿筝,顯示的都是Animal is running...
,符合邏輯的做法是分別顯示Dog is running...
和Cat is running...
棚贾,因此窖维,對(duì)Dog和Cat類改進(jìn)如下:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
當(dāng)然榆综,也可以對(duì)子類增加一些方法,比如Dog
類:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
繼承的第二個(gè)好處需要我們對(duì)代碼做一點(diǎn)改進(jìn)铸史。你看到了鼻疮,無論是Dog
還是Cat
,它們run()
的時(shí)候琳轿,顯示的都是Animal is running...
判沟,符合邏輯的做法是分別顯示Dog is running...
和Cat is running...
,因此崭篡,對(duì)Dog
和Cat
類改進(jìn)如下:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
再次運(yùn)行挪哄,結(jié)果如下:
Dog is running...
Cat is running...
當(dāng)子類和父類都存在相同的run()方法時(shí),我們說琉闪,子類的
run()覆蓋了父類的
run()中燥,在代碼運(yùn)行的時(shí)候,總是會(huì)調(diào)用子類的
run()塘偎。這樣,我們就獲得了繼承的另一個(gè)好處:
多態(tài)`拿霉。
要理解什么是多態(tài)吟秩,我們首先要對(duì)數(shù)據(jù)類型再作一點(diǎn)說明。當(dāng)我們定義一個(gè)class
的時(shí)候绽淘,我們實(shí)際上就定義了一種數(shù)據(jù)類型涵防。我們定義的數(shù)據(jù)類型和Python自帶的數(shù)據(jù)類型,比如str
沪铭、list
壮池、dict
沒什么兩樣:
a = list() # a是list類型
b = Animal() # b是Animal類型
c = Dog() # c是Dog類型
判斷一個(gè)變量是否是某個(gè)類型可以用isinstance()
判斷:
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
看來a、b杀怠、c確實(shí)對(duì)應(yīng)著list
椰憋、Animal
、Dog
這3種類型赔退。
但是等等橙依,試試:
>>> isinstance(c, Animal)
True
看來c
不僅僅是Dog
,c
還是Animal
硕旗!
不過仔細(xì)想想窗骑,這是有道理的,因?yàn)?code>Dog是從Animal
繼承下來的漆枚,當(dāng)我們創(chuàng)建了一個(gè)Dog
的實(shí)例c
時(shí)创译,我們認(rèn)為c
的數(shù)據(jù)類型是Dog
沒錯(cuò),但c
同時(shí)也是Animal
也沒錯(cuò)墙基,Dog本來就是Animal的一種软族!
所以刷喜,在繼承關(guān)系中,如果一個(gè)實(shí)例的數(shù)據(jù)類型是某個(gè)子類互订,那它的數(shù)據(jù)類型也可以被看做是父類吱肌。但是,反過來就不行:
>>> b = Animal()
>>> isinstance(b, Dog)
False
Dog
可以看成Animal仰禽,但Animal不可以看成Dog氮墨。
要理解多態(tài)的好處,我們還需要再編寫一個(gè)函數(shù)吐葵,這個(gè)函數(shù)接受一個(gè)Animal類型的變量:
def run_twice(animal):
animal.run()
animal.run()
當(dāng)我們傳入Animal的實(shí)例時(shí)规揪,run_twice()就打印出:
>>> run_twice(Animal())
Animal is running...
Animal is running...
當(dāng)我們傳入Dog的實(shí)例時(shí),run_twice()就打印出:
>>> run_twice(Dog())
Dog is running...
Dog is running...
當(dāng)我們傳入Cat的實(shí)例時(shí)温峭,run_twice()就打印出:
>>> run_twice(Cat())
Cat is running...
Cat is running...
看上去沒啥意思猛铅,但是仔細(xì)想想,現(xiàn)在凤藏,如果我們?cè)俣x一個(gè)Tortoise
類型奸忽,也從Animal派生:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
當(dāng)我們調(diào)用run_twice()
時(shí),傳入Tortoise
的實(shí)例:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
你會(huì)發(fā)現(xiàn)揖庄,新增一個(gè)Animal的子類栗菜,不必對(duì)run_twice()做任何修改,實(shí)際上蹄梢,任何依賴Animal作為參數(shù)的函數(shù)或者方法都可以不加修改地正常運(yùn)行疙筹,原因就在于多態(tài)
。
多態(tài)的好處就是禁炒,當(dāng)我們需要傳入Dog而咆、Cat、Tortoise……時(shí)幕袱,我們只需要接收Animal類型就可以了暴备,因?yàn)镈og、Cat凹蜂、Tortoise……都是Animal類型馍驯,然后,按照Animal類型進(jìn)行操作即可玛痊。由于Animal類型有run()方法汰瘫,因此,傳入的任意類型擂煞,只要是Animal類或者子類混弥,就會(huì)自動(dòng)調(diào)用實(shí)際類型的run()
方法,這就是多態(tài)的意思:
對(duì)于一個(gè)變量,我們只需要知道它是Animal
類型蝗拿,無需確切地知道它的子類型晾捏,就可以放心地調(diào)用run()方法,而具體調(diào)用的run()方法是作用在Animal哀托、Dog惦辛、Cat還是Tortoise對(duì)象上,由運(yùn)行時(shí)該對(duì)象的確切類型決定仓手,這就是多態(tài)真正的威力:調(diào)用方只管調(diào)用胖齐,不管細(xì)節(jié),而當(dāng)我們新增一種Animal的子類時(shí)嗽冒,只要確保run()方法編寫正確呀伙,不用管原來的代碼是如何調(diào)用的。這就是著名的“開閉”原則:
對(duì)擴(kuò)展開放:允許新增Animal子類添坊;
對(duì)修改封閉:不需要修改依賴Animal類型的run_twice()等函數(shù)剿另。
繼承還可以一級(jí)一級(jí)地繼承下來,就好比從爺爺?shù)桨职直嵬堋⒃俚絻鹤舆@樣的關(guān)系雨女。而任何類,最終都可以追溯到根類object
阳准,這些繼承關(guān)系看上去就像一顆倒著的樹戚篙。比如如下的繼承樹:
┌───────────────┐
│ object │
└───────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Animal │ │ Plant │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Cat │ │ Tree │ │ Flower │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
靜態(tài)語言 vs 動(dòng)態(tài)語言
對(duì)于靜態(tài)語言
(例如Java)來說,如果需要傳入Animal類型溺职,則傳入的對(duì)象必須是Animal類型或者它的子類,否則位喂,將無法調(diào)用run()方法浪耘。
對(duì)于Python這樣的動(dòng)態(tài)語言來說,則不一定需要傳入Animal類型塑崖。我們只需要保證傳入的對(duì)象有一個(gè)run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
這就是動(dòng)態(tài)語言的“鴨子類型”七冲,它并不要求嚴(yán)格的繼承體系,一個(gè)對(duì)象只要“看起來像鴨子规婆,走起路來像鴨子”澜躺,那它就可以被看做是鴨子。
Python的“file-like object“
就是一種鴨子類型抒蚜。對(duì)真正的文件對(duì)象掘鄙,它有一個(gè)read()
方法,返回其內(nèi)容嗡髓。但是操漠,許多對(duì)象,只要有read()方法饿这,都被視為“file-like object“浊伙。許多函數(shù)接收的參數(shù)就是“file-like object“撞秋,你不一定要傳入真正的文件對(duì)象,完全可以傳入任何實(shí)現(xiàn)了read()方法的對(duì)象嚣鄙。
小結(jié)
繼承可以把父類的所有功能都直接拿過來吻贿,這樣就不必重零做起,子類只需要新增自己特有的方法哑子,也可以把父類不適合的方法覆蓋重寫舅列。
動(dòng)態(tài)語言的鴨子類型特點(diǎn)決定了繼承不像靜態(tài)語言那樣是必須的。
獲取對(duì)象信息
當(dāng)我們拿到一個(gè)對(duì)象的引用時(shí)赵抢,如何知道這個(gè)對(duì)象是什么類型剧蹂、有哪些方法呢?
使用type()
首先烦却,我們來判斷對(duì)象類型宠叼,使用type()
函數(shù):
基本類型都可以用type()
判斷:
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
如果一個(gè)變量指向函數(shù)或者類,也可以用type()判斷:
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
但是type()
函數(shù)返回的是什么類型呢其爵?它返回對(duì)應(yīng)的Class
類型冒冬。如果我們要在if
語句中判斷,就需要比較兩個(gè)變量的type
類型是否相同:
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
判斷基本數(shù)據(jù)類型可以直接寫int
摩渺,str
等简烤,但如果要判斷一個(gè)對(duì)象是否是函數(shù)怎么辦?可以使用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
使用isinstance()
對(duì)于class的繼承關(guān)系來說摇幻,使用type()
就很不方便横侦。我們要判斷class
的類型,可以使用isinstance()
函數(shù)绰姻。
我們回顧上次的例子枉侧,如果繼承關(guān)系是:
object -> Animal -> Dog -> Husky
那么,isinstance()
就可以告訴我們狂芋,一個(gè)對(duì)象是否是某種類型榨馁。先創(chuàng)建3種類型的對(duì)象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
然后,判斷:
>>> isinstance(h, Husky)
True
沒有問題帜矾,因?yàn)閔變量指向的就是Husky對(duì)象翼虫。
再判斷:
>>> isinstance(h, Dog)
True
h雖然自身是Husky類型,但由于Husky是從Dog繼承下來的屡萤,所以珍剑,h也還是Dog類型。換句話說死陆,isinstance()
判斷的是一個(gè)對(duì)象是否是該類型本身次慢,或者位于該類型的父繼承鏈上。
因此,我們可以確信迫像,h還是Animal類型:
>>> isinstance(h, Animal)
True
同理劈愚,實(shí)際類型是Dog的d也是Animal類型:
>>> isinstance(d, Dog) and isinstance(d, Animal)
True
但是,d不是Husky類型:
>>> isinstance(d, Husky)
False
能用type()
判斷的基本類型也可以用isinstance()判斷:
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True
并且還可以判斷一個(gè)變量是否是某些類型中的一種闻妓,比如下面的代碼就可以判斷是否是list
或者tuple
:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True
總是優(yōu)先使用
isinstance()
判斷類型菌羽,可以將指定類型及其子類“一網(wǎng)打盡”。
使用dir()
如果要獲得一個(gè)對(duì)象的所有屬性和方法由缆,可以使用dir()
函數(shù)注祖,它返回一個(gè)包含字符串的list
,比如均唉,獲得一個(gè)str
對(duì)象的所有屬性和方法:
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
類似__xxx__
的屬性和方法在Python
中都是有特殊用途的是晨,比如__len__
方法返回長度。在Python中舔箭,如果你調(diào)用len()
函數(shù)試圖獲取一個(gè)對(duì)象的長度罩缴,實(shí)際上,在len()函數(shù)內(nèi)部层扶,它自動(dòng)去調(diào)用該對(duì)象的__len__()
方法箫章,所以,下面的代碼是等價(jià)的:
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
我們自己寫的類镜会,如果也想用len(myObj)
的話檬寂,就自己寫一個(gè)len()方法:
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog = MyDog()
>>> len(dog)
100
剩下的都是普通屬性或方法,比如lower()
返回小寫的字符串:
>>> 'ABC'.lower()
'abc'
僅僅把屬性和方法列出來是不夠的戳表,配合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()
緊接著匾旭,可以測試該對(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
通過內(nèi)置的一系列函數(shù),我們可以對(duì)任意一個(gè)Python
對(duì)象進(jìn)行剖析席覆,拿到其內(nèi)部的數(shù)據(jù)史辙。要注意的是,只有在不知道對(duì)象信息的時(shí)候,我們才會(huì)去獲取對(duì)象信息聊倔。如果可以直接寫:
sum = obj.x + obj.y
就不要寫:
sum = getattr(obj, 'x') + getattr(obj, 'y')
一個(gè)正確的用法的例子如下:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
假設(shè)我們希望從文件流fp中讀取圖像晦毙,我們首先要判斷該fp
對(duì)象是否存在read
方法,如果存在耙蔑,則該對(duì)象是一個(gè)流见妒,如果不存在,則無法讀取甸陌。hasattr()
就派上了用場须揣。
請(qǐng)注意,在Python這類動(dòng)態(tài)語言中钱豁,根據(jù)鴨子類型耻卡,有read()
方法,不代表該fp
對(duì)象就是一個(gè)文件流
牲尺,它也可能是網(wǎng)絡(luò)流
卵酪,也可能是內(nèi)存中的一個(gè)字節(jié)流
,但只要read()
方法返回的是有效的圖像數(shù)據(jù)秸谢,就不影響讀取圖像的功能凛澎。
實(shí)例屬性和類屬性
由于Python是動(dòng)態(tài)語言,根據(jù)類創(chuàng)建的實(shí)例可以任意綁定屬性估蹄。
給實(shí)例綁定屬性的方法是通過實(shí)例變量塑煎,或者通過self
變量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
但是,如果Student
類本身需要綁定一個(gè)屬性呢臭蚁?可以直接在class
中定義屬性最铁,這種屬性是類屬性,歸Student類所有:
class Student(object):
name = 'Student'
當(dāng)我們定義了一個(gè)類屬性后垮兑,這個(gè)屬性雖然歸類所有冷尉,但類的所有實(shí)例都可以訪問到。來測試一下:
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 創(chuàng)建實(shí)例s
>>> print(s.name) # 打印name屬性系枪,因?yàn)閷?shí)例并沒有name屬性雀哨,所以會(huì)繼續(xù)查找class的name屬性
Student
>>> print(Student.name) # 打印類的name屬性
Student
>>> s.name = 'Michael' # 給實(shí)例綁定name屬性
>>> print(s.name) # 由于實(shí)例屬性優(yōu)先級(jí)比類屬性高,因此私爷,它會(huì)屏蔽掉類的name屬性
Michael
>>> print(Student.name) # 但是類屬性并未消失雾棺,用Student.name仍然可以訪問
Student
>>> del s.name # 如果刪除實(shí)例的name屬性
>>> print(s.name) # 再次調(diào)用s.name,由于實(shí)例的name屬性沒有找到衬浑,類的name屬性就顯示出來了
Student
從上面的例子可以看出捌浩,在編寫程序的時(shí)候,千萬不要對(duì)實(shí)例屬性和類屬性使用相同的名字工秩,因?yàn)橄嗤Q的實(shí)例屬性將屏蔽掉類屬性尸饺,但是當(dāng)你刪除實(shí)例屬性后进统,再使用相同的名稱,訪問到的將是類屬性浪听。