面向對象編程——Object Oriented Programming,簡稱OOP,是一種程序設計思想。OOP把對象作為程序的基本單元,一個對象包含了數(shù)據(jù)和操作數(shù)據(jù)的函數(shù)。
- 面向過程的程序設計把計算機程序視為一系列的命令集合,即一組函數(shù)的順序執(zhí)行。為了簡化程序設計,面向過程把函數(shù)繼續(xù)切分為子函數(shù),即把大塊函數(shù)通過切割成小塊函數(shù)來降低系統(tǒng)的復雜度。
- 面向對象的程序設計把計算機程序視為一組對象的集合,而每個對象都可以接收其他對象發(fā)過來的消息,并處理這些消息,計算機程序的執(zhí)行就是一系列消息在各個對象之間傳遞。
在Python中,所有數(shù)據(jù)類型都可以視為對象,當然也可以自定義對象。自定義的對象數(shù)據(jù)類型就是面向對象中的**類(Class)**的概念。Class是一種抽象概念,比如我們定義的Class——Student,是指學生這個概念,而實例(Instance)則是一個個具體的Student,比如,Bart Simpson和Lisa Simpson是兩個具體的Student。
所以,面向對象的設計思想是抽象出Class,根據(jù)Class創(chuàng)建Instance。面向對象的抽象程度又比函數(shù)要高,因為一個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))
給對象發(fā)消息實際上就是調(diào)用對象對應的關聯(lián)函數(shù),我們稱之為對象的方法(Method)。面向對象的程序寫出來就像這樣:
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
數(shù)據(jù)封裝、繼承和多態(tài)是面向對象的三大特點,下面逐個介紹!
類和實例
class Student(object):
pass
class
后面緊接著是類名,即Student
,類名通常是大寫開頭的單詞,緊接著是(object)
,表示該類是從哪個類繼承下來的,繼承的概念我們后面再講,通常,如果沒有合適的繼承類,就使用object
類,這是所有類最終都會繼承的類。
定義好了Student
類,就可以根據(jù)Student
類創(chuàng)建出Student
的實例
由于類可以起到模板的作用,因此,可以在創(chuàng)建實例的時候,把一些我們認為必須綁定的屬性強制填寫進去。通過定義一個特殊的__init__
方法,在創(chuàng)建實例的時候,就把name
,score
等屬性綁上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意:特殊方法“init”前后分別有兩個下劃線!??!
注意到__init__
方法的第一個參數(shù)永遠是self
,表示創(chuàng)建的實例本身,因此,在__init__
方法內(nèi)部,就可以把各種屬性綁定到self
,因為self
就指向創(chuàng)建的實例本身。
有了__init__
方法,在創(chuàng)建實例的時候,就不能傳入空的參數(shù)了,必須傳入與__init__
方法匹配的參數(shù),但self
不需要傳,Python解釋器自己會把實例變量傳進去:
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
和普通的函數(shù)相比,在類中定義的函數(shù)只有一點不同,就是第一個參數(shù)永遠是實例變量self
,并且,調(diào)用時不用傳遞該參數(shù)。除此之外,類的方法和普通函數(shù)沒有什么區(qū)別,所以,你仍然可以用默認參數(shù)、可變參數(shù)、關鍵字參數(shù)和命名關鍵字參數(shù)。
數(shù)據(jù)封裝
在上面的Student
類中,每個實例就擁有各自的name
和score
這些數(shù)據(jù)。我們可以通過函數(shù)來訪問這些數(shù)據(jù),比如打印一個學生的成績:
>>>def print_score(std):
... print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
Bart Simpson: 59
既然Student
實例本身就擁有這些數(shù)據(jù),要訪問這些數(shù)據(jù),就沒有必要從外面的函數(shù)去訪問,可以直接在Student
類的內(nèi)部定義訪問數(shù)據(jù)的函數(shù),這樣,就把“數(shù)據(jù)”給封裝起來了。這些封裝數(shù)據(jù)的函數(shù)是和Student
類本身是關聯(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))
要定義一個方法,除了第一個參數(shù)是self
外,其他和普通函數(shù)一樣。要調(diào)用一個方法,只需要在實例變量上直接調(diào)用,除了self
不用傳遞,其他參數(shù)正常傳入:
>>> bart.print_score()
Bart Simpson: 59
這樣一來,我們從外部看Student
類,就只需要知道,創(chuàng)建實例需要給出name
和score
,而如何打印,都是在Student
類的內(nèi)部定義的,這些數(shù)據(jù)和邏輯被“封裝”起來了,調(diào)用很容易,而且不用知道內(nèi)部實現(xiàn)的細節(jié)。
封裝的另一個好處是可以給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())
結果就會打印出 Lisa A 和 Bart C。調(diào)用起來很方便。
訪問限制
如果要讓內(nèi)部屬性不被外部訪問,可以把屬性的名稱前加上兩個下劃線__,在Python中,實例的變量名如果以__開頭,就變成了一個私有變量(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))
改完后,對于外部代碼來說,沒什么變動,但是已經(jīng)無法從外部訪問實例變量.__name
和實例變量.__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程序設計中,當我們定義一個class的時候,可以從某個現(xiàn)有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Base class、Super class)。繼承的最大好處就是子類可以繼承父類的全部功能!
比如,我們已經(jīng)編寫了一個名為Animal
的class,有一個run()
方法可以直接打?。?/p>
class Animal(object):
def run(self):
print('Animal is running...')
當我們需要編寫Dog
和Cat
類時,就可以直接從Animal
類繼承:
class Dog(Animal):
pass
class Cat(Animal):
pass
對于Dog來說,Animal就是它的父類,對于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...
當子類和父類都存在相同的run()方法時,我們說,子類的run()覆蓋了父類的run(),在代碼運行的時候,總是會調(diào)用子類的run()。這樣,我們就獲得了繼承的另一個好處:多態(tài)。
要理解什么是多態(tài),我們首先要對數(shù)據(jù)類型再作一點說明。當我們定義一個class的時候,我們實際上就定義了一種數(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
!
在繼承關系中,如果一個實例的數(shù)據(jù)類型是某個子類,那它的數(shù)據(jù)類型也可以被看做是父類,狗是狗,也是動物。但是,反過來就不行,狗是動物,但動物不是狗。
要理解多態(tài)的好處,我們還需要再編寫一個函數(shù),這個函數(shù)接受一個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...
看上去沒啥意思,但是仔細想想,現(xiàn)在,如果我們再定義一個Tortoise
類型,也從Animal
派生:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
當我們調(diào)用run_twice()
時,傳入Tortoise
的實例:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
你會發(fā)現(xiàn),新增一個Animal
的子類,不必對run_twice()
做任何修改,實際上,任何依賴Animal
作為參數(shù)的函數(shù)或者方法都可以不加修改地正常運行,原因就在于多態(tài)。
對于一個變量,我們只需要知道它是Animal
類型,無需確切地知道它的子類型,就可以放心地調(diào)用run()
方法,而具體調(diào)用的run()
方法是作用在Animal
、Dog
、Cat
還是Tortoise
對象上,由運行時該對象的確切類型決定,這就是多態(tài)真正的威力:調(diào)用方只管調(diào)用,不管細節(jié),而當我們新增一種Animal
的子類時,只要確保run()
方法編寫正確,不用管原來的代碼是如何調(diào)用的。 這就是著名的“開閉”原則:
- 對擴展開放:允許新增
Animal
子類; - 對修改封閉:不需要修改依賴
Animal
類型的run_twice()
等函數(shù)。
繼承還可以一級一級地繼承下來,就好比從爺爺?shù)桨职?、再到兒子這樣的關系。而任何類,最終都可以追溯到根類object,這些繼承關系看上去就像一顆倒著的樹。比如如下的繼承樹:
動態(tài)語言
對于靜態(tài)語言(例如Java)來說,如果需要傳入Animal
類型,則傳入的對象必須是Animal
類型或者它的子類,否則,將無法調(diào)用run()
方法。
對于Python這樣的動態(tài)語言來說,則不一定需要傳入Animal
類型。我們只需要保證傳入的對象有一個run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
這就是動態(tài)語言的“鴨子類型”!,它并不要求嚴格的繼承體系,一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫。
動態(tài)語言的鴨子類型特點決定了繼承不像靜態(tài)語言那樣是必須的。
獲取對象信息
type()
當我們拿到一個對象的引用時,如何知道這個對象是什么類型、有哪些方法呢?
基本類型都可以用type()
判斷:
>>> type(123)
<class 'int'>
>>>type('str')
<class 'str'>
>>>type(None)
<type(None) 'NoneType'>
如果一個變量指向函數(shù)或者類,也可以用type()
判斷:
>>> type(abs)
<class 'builtin_function_or_method'>
>>>type(a)
<class '__main__.Animal'>
但是type()
函數(shù)返回的是什么類型呢?它返回對應的Class
類型。如果我們要在if
語句中判斷,就需要比較兩個變量的type
類型是否相同:
在這里插入代碼片
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type()
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
isinstance()
我們回顧上次的例子,如果繼承關系是:
object -> Animal -> Dog -> Husky
那么,isinstance()
就可以告訴我們,一個對象是否是某種類型。先創(chuàng)建3種類型的對象:
>>> 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
并且還可以判斷一個變量是否是某些類型中的一種,比如下面的代碼就可以判斷是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True
總是優(yōu)先使用isinstance()判斷類型,可以將指定類型及其子類“一網(wǎng)打盡”。
dir()
使用dir() 獲得一個對象的所有屬性和方法。
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
getattr()、setattr()、hasattr()
僅僅把屬性和方法列出來是不夠的,配合getattr()
、setattr()
以及hasattr()
,我們可以直接操作一個對象的狀態(tài):
>>> 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
實例屬性和類屬性
由于Python是動態(tài)語言,根據(jù)類創(chuàng)建的實例可以任意綁定屬性。直接在class中定義屬性,這種屬性是類屬性,歸Student
類所有,直接給實例綁定屬性,叫實例屬性,實例屬性優(yōu)先級比類屬性高,它會屏蔽掉類的name屬性,但是類屬性并未消失,當實例屬性沒有找到,自動調(diào)用類屬性:
>>>class Student(object):
... name = 'Student'
...
>>> s = Student()# 創(chuàng)建實例s
>>> print(s.name)# 打印name屬性,因為實例并沒有name屬性,所以會繼續(xù)查找class的name屬性
Student
>>> print(Student.name)# 打印類的name屬性
Student
>>> s.name = 'Michael'# 給實例綁定name屬性
>>> print(s.name)# 由于實例屬性優(yōu)先級比類屬性高,因此,它會屏蔽掉類的name屬性
Michael
>>> print(Student.name)# 但是類屬性并未消失,用Student.name仍然可以訪問
Student
>>>del s.name# 如果刪除實例的name屬性
>>> print(s.name)# 再次調(diào)用s.name,由于實例的name屬性沒有找到,類的name屬性就顯示出來了
Student
千萬不要對實例屬性和類屬性使用相同的名字,因為相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性后,再使用相同的名稱,訪問到的將是類屬性。
OOP高級編程還有多重繼承、定制類、元類等概念,之后繼續(xù)學習再補充筆記。
附上Python文章的鏈接:
本文內(nèi)容屬于筆記,大部分內(nèi)容源自 廖雪峰老師的博客, 非常推薦大家去他的網(wǎng)站學習!