14、Python入門____語法入門(七)面向?qū)ο缶幊?/h1>

面向?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ì)象擁有namescore這兩個(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í)例需要給出namescore申尼,而如何打印垮卓,都是在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ù)婶肩,代碼更加健壯办陷。

但是如果外部代碼要獲取namescore怎么辦?可以給Student類增加get_nameget_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)我們需要編寫DogCat類時(shí),就可以直接從Animal類繼承:

class Dog(Animal):
    pass

class Cat(Animal):
    pass

對(duì)于Dog來說毒姨,Animal就是它的父類哑蔫,對(duì)于Animal來說,Dog就是它的子類手素。CatDog類似鸳址。

繼承有什么好處?最大的好處是子類獲得了父類的全部功能泉懦。由于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ì)DogCat類改進(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椰憋、AnimalDog這3種類型赔退。

但是等等橙依,試試:

>>> isinstance(c, Animal)
True

看來c不僅僅是Dogc還是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í)例屬性后进统,再使用相同的名稱,訪問到的將是類屬性浪听。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者

  • 序言:七十年代末螟碎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子馋辈,更是在濱河造成了極大的恐慌抚芦,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迈螟,死亡現(xiàn)場離奇詭異叉抡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)答毫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門褥民,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洗搂,你說我怎么就攤上這事消返。” “怎么了耘拇?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵撵颊,是天一觀的道長。 經(jīng)常有香客問我惫叛,道長倡勇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任嘉涌,我火速辦了婚禮妻熊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仑最。我一直安慰自己扔役,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布警医。 她就那樣靜靜地躺著亿胸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪预皇。 梳的紋絲不亂的頭發(fā)上侈玄,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音深啤,去河邊找鬼。 笑死路星,一個(gè)胖子當(dāng)著我的面吹牛溯街,可吹牛的內(nèi)容都是我干的诱桂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼呈昔,長吁一口氣:“原來是場噩夢啊……” “哼挥等!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起堤尾,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤肝劲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后郭宝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辞槐,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年粘室,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了榄檬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡衔统,死狀恐怖鹿榜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锦爵,我是刑警寧澤舱殿,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站险掀,受9級(jí)特大地震影響沪袭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜迷郑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一枝恋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗡害,春花似錦焚碌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至叹螟,卻和暖如春鹃骂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背罢绽。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工畏线, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人良价。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓寝殴,卻偏偏與公主長得像蒿叠,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蚣常,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容