面向?qū)ο缶幊獭狾bject Oriented Programming,簡(jiǎn)稱OOP,是一種程序設(shè)計(jì)思想。OOP把對(duì)象作為程序的基本單元,一個(gè)對(duì)象包含了數(shù)據(jù)和操作數(shù)據(jù)的函數(shù)。
- 面向過程的程序設(shè)計(jì)把計(jì)算機(jī)程序視為一系列的命令集合,即一組函數(shù)的順序執(zhí)行。為了簡(jiǎn)化程序設(shè)計(jì),面向過程把函數(shù)繼續(xù)切分為子函數(shù),即把大塊函數(shù)通過切割成小塊函數(shù)來降低系統(tǒng)的復(fù)雜度。
- 面向?qū)ο?/strong>的程序設(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ū)ο笾械?*類(Class)**的概念。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ù)的方法。
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()
數(shù)據(jù)封裝、繼承和多態(tài)是面向?qū)ο蟮娜筇攸c(diǎn),下面逐個(gè)介紹!
類和實(shí)例
class Student(object):
pass
class
后面緊接著是類名,即Student
,類名通常是大寫開頭的單詞,緊接著是(object)
,表示該類是從哪個(gè)類繼承下來的,繼承的概念我們后面再講,通常,如果沒有合適的繼承類,就使用object
類,這是所有類最終都會(huì)繼承的類。
定義好了Student
類,就可以根據(jù)Student
類創(chuàng)建出Student
的實(shí)例
由于類可以起到模板的作用,因此,可以在創(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è)下劃線?。。?/p>
注意到__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ù)封裝
在上面的Student
類中,每個(gè)實(shí)例就擁有各自的name
和score
這些數(shù)據(jù)。我們可以通過函數(shù)來訪問這些數(shù)據(jù),比如打印一個(gè)學(xué)生的成績(jī):
>>>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'
lisa = Student('Lisa', 99)
bart = Student('Bart', 59)
print(lisa.name, lisa.get_grade())
print(bart.name, bart.get_grade())
結(jié)果就會(huì)打印出 Lisa A 和 Bart C。調(diào)用起來很方便。
訪問限制
如果要讓內(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)無(wú)法從外部訪問實(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'
但是如果外部代碼要獲取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
繼承 & 多態(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()
方法可以直接打?。?/p>
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類似。
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
dog = Dog()
dog.run()
cat = Cat()
cat.run()
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類型
但是等等,試試:
>>> isinstance(c, Animal)
True
看來c
不僅僅是Dog
,c
還是Animal
!
在繼承關(guān)系中,如果一個(gè)實(shí)例的數(shù)據(jù)類型是某個(gè)子類,那它的數(shù)據(jù)類型也可以被看做是父類,狗是狗,也是動(dòng)物。但是,反過來就不行,狗是動(dòng)物,但動(dòng)物不是狗。
要理解多態(tài)的好處,我們還需要再編寫一個(gè)函數(shù),這個(gè)函數(shù)接受一個(gè)Animal類型的變量:
def run_twice(animal):
animal.run()
animal.run()
>>> run_twice(Animal())
Animal is running...
Animal is running...
>>> run_twice(Dog())
Dog is running...
Dog is running...
>>> 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)。
對(duì)于一個(gè)變量,我們只需要知道它是Animal
類型,無(wú)需確切地知道它的子類型,就可以放心地調(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)系看上去就像一顆倒著的樹。比如如下的繼承樹:
動(dòng)態(tài)語(yǔ)言
對(duì)于靜態(tài)語(yǔ)言(例如Java)來說,如果需要傳入Animal
類型,則傳入的對(duì)象必須是Animal
類型或者它的子類,否則,將無(wú)法調(diào)用run()
方法。
對(duì)于Python這樣的動(dòng)態(tài)語(yǔ)言來說,則不一定需要傳入Animal
類型。我們只需要保證傳入的對(duì)象有一個(gè)run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
這就是動(dòng)態(tài)語(yǔ)言的“鴨子類型”!,它并不要求嚴(yán)格的繼承體系,一個(gè)對(duì)象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫。
動(dòng)態(tài)語(yǔ)言的鴨子類型特點(diǎn)決定了繼承不像靜態(tài)語(yǔ)言那樣是必須的。
獲取對(duì)象信息
type()
當(dāng)我們拿到一個(gè)對(duì)象的引用時(shí),如何知道這個(gè)對(duì)象是什么類型、有哪些方法呢?
基本類型都可以用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
語(yǔ)句中判斷,就需要比較兩個(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
isinstance()
我們回顧上次的例子,如果繼承關(guān)系是:
object -> Animal -> Dog -> Husky
那么,isinstance()
就可以告訴我們,一個(gè)對(duì)象是否是某種類型。先創(chuàng)建3種類型的對(duì)象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
>>> isinstance(h, Husky)
True
能用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()
使用dir() 獲得一個(gè)對(duì)象的所有屬性和方法。
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
getattr()、setattr()、hasattr()
僅僅把屬性和方法列出來是不夠的,配合getattr()
、setattr()
以及hasattr()
,我們可以直接操作一個(gè)對(duì)象的狀態(tài):
>>> 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
實(shí)例屬性和類屬性
由于Python是動(dòng)態(tài)語(yǔ)言,根據(jù)類創(chuàng)建的實(shí)例可以任意綁定屬性。直接在class中定義屬性,這種屬性是類屬性,歸Student
類所有,直接給實(shí)例綁定屬性,叫實(shí)例屬性,實(shí)例屬性優(yōu)先級(jí)比類屬性高,它會(huì)屏蔽掉類的name屬性,但是類屬性并未消失,當(dāng)實(shí)例屬性沒有找到,自動(dòng)調(diào)用類屬性:
>>>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
千萬(wàn)不要對(duì)實(shí)例屬性和類屬性使用相同的名字,因?yàn)橄嗤Q的實(shí)例屬性將屏蔽掉類屬性,但是當(dāng)你刪除實(shí)例屬性后,再使用相同的名稱,訪問到的將是類屬性。
OOP高級(jí)編程還有多重繼承、定制類、元類等概念,之后繼續(xù)學(xué)習(xí)再補(bǔ)充筆記。
附上Python文章的鏈接:
本文內(nèi)容屬于筆記,大部分內(nèi)容源自 廖雪峰老師的博客, 非常推薦大家去他的網(wǎng)站學(xué)習(xí)!