面向?qū)ο缶幊?/h1>
6.1 類和實(shí)例
class后面緊接類名,通常是以大寫字母為開頭的單詞,緊接著是(object),表示該類是從哪個類繼承下來的。如果沒有合適的繼承類,就使用object類,這是所有類最終都會繼承的類。
class Student(object):
pass
可以自由地給一個實(shí)例變量綁定屬性,比如,給實(shí)例bart綁定一個name屬性:
>>> class Student(object):
... pass
>>> bart = Student()
>>> bart.name = 'Bart.Simpson'
>>> bart.name
'Bart.Simpson'
由于類可以起到模板的作用,因此,可以在創(chuàng)建實(shí)例的時(shí)候,把一些我們認(rèn)為必須綁定的屬性強(qiáng)制填寫進(jìn)去。通過定義一個特殊的__init__方法,在創(chuàng)建實(shí)例的時(shí)候,就把name,scope等屬性綁上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意到_init_方法的第一個參數(shù)永遠(yuǎn)是self,表示創(chuàng)建的實(shí)例本身。因此,在_init_方法內(nèi)部,就可以把各種屬性綁定到self,因?yàn)閟elf就指向創(chuàng)建實(shí)例的本身。
有了_init_方法就,在創(chuàng)建實(shí)例的方法匹配的參數(shù)時(shí)候就不能傳入空的參數(shù)了,必須傳入與_init_方法匹配的參數(shù),但self不需要傳,Python解釋器自己會把實(shí)例變量傳進(jìn)去。
>>> bart = Student('Bart Simon', 59)
>>> bart.score
59
>>> bart.name
'Bart Simon'
數(shù)據(jù)封裝
可以直接在類的內(nèi)部定義訪問數(shù)據(jù)的函數(shù),這樣,就把“數(shù)據(jù)”封裝起來了。這些封裝數(shù)據(jù)的函數(shù)是和類本身是關(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))
要定義一個方法,除了第一個參數(shù)是self外,其他和普通函數(shù)一樣。要調(diào)用一個方法,只需要在實(shí)例變量上直接調(diào)用,除了self不用傳遞,其他參數(shù)正常傳人。
>>> bart = Student('Kevin', 99)
>>> bart.print_score()
Kevin: 99
這樣的話,我們從外部看Student類,就只需要知道,創(chuàng)建實(shí)例需要給出類的屬性name和score,而如何打印,則是在Student類的內(nèi)部定義的。
封裝的另一個優(yōu)點(diǎn)在于,可以給類增加新的方法。比如下例給Person類增加新的判別身材的方法:
class Person(object):
def __init__(self, name, sex, height,weight):
self.sex = sex
self.height = height
self.name = name
self.weight = weight
def print_sex_height(self):
print('%s: %s, %s' %(self.name, self.sex, self.height))
def figure(self):
if self.weight > 140:
print('Too fat, you should take more exsrcise.')
elif self.weight <110:
print('Too thin, you should be extra mindful of getting the right nourishment.')
else:
print('You have a good figure, keep it.')
同樣的,新增的figure方法可以直接在實(shí)例變量上調(diào)用,不需要知道內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。
>>> baby = Person('zhengning', 'female', 162, 98)
>>> baby.print_sex_height()
zhengning: female, 162
>>> baby.figure()
Too thin, you should be extra mindful of getting the right nourishment.
小結(jié)
- 類是創(chuàng)建實(shí)例的模板,而實(shí)例則是一個一個具體的對象,每個實(shí)例擁有的數(shù)據(jù)都互相獨(dú)立,互不影響。
- 方法就是與實(shí)例綁定的函數(shù),和普通函數(shù)不同,方法可以直接訪問實(shí)例的數(shù)據(jù)。
- 通過在實(shí)例上調(diào)用方法,我們就直接操作了對象內(nèi)部的數(shù)據(jù),但無需知道方法內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)。
- 和靜態(tài)語言不同,Python允許對實(shí)例變量綁定任何數(shù)據(jù),也就是說,對于兩個實(shí)例變量,雖然它們都是同一個類的不同實(shí)例,但擁有的變量名稱都可能不同。
>>> baby = Person('zhengning', 'female', 162, 98)
>>> kevin = Person('wukaiwen', 'male', 165, 126)
>>> baby.age = 8
>>> baby.age
8
>>> kevin.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'age'
6.2 訪問限制
在class內(nèi)部,可以通過定義屬性和方法,而外部代碼則可以通過直接實(shí)例變量的方法來操作數(shù)據(jù)。這樣,就隱藏了內(nèi)部的復(fù)雜邏輯。但是這樣,則存在一個內(nèi)部屬性易被修改的問題,即外部代碼能通過實(shí)例的方法來修改類的屬性。
>>> kevin = Person('wukaiwen', 'male', 165, 126)
>>> kevin.weight
126
>>> kevin.weight = 130
>>> kevin.weight
130
如果要讓內(nèi)部屬性不被外部訪問,可以在屬性的名稱前加上兩個下劃線。在Python中,實(shí)例的變量如果在類的內(nèi)部定義時(shí)以__開頭,就變成了一個私有變量(private),只有內(nèi)部可以訪問,而外部不能訪問:
class Person(object):
def __init__(self, name, sex, height, weight):
self.__sex = sex
self.__name = name
self.__height = height
self.__weight = weight
改完后,對于外部代碼來說,沒什么變動,但是已經(jīng)無法從外部訪問實(shí)例變量.__height及.__height了:
>>>zhengning = Person('zhengning', 'female', 162, 98)
>>>zhengning.__height
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-6-1abc5119b3fc>", line 1, in <module>
zhengning.__height
AttributeError: 'Person' object has no attribute '__height'
這樣保證了外部代碼不能隨意修改對象內(nèi)部的狀態(tài),即通過訪問限制的保護(hù),代碼更加健壯。
如果要獲取類內(nèi)部的私有屬性的話,可以給Person類增加諸如get_name和get_sex這樣的方法:
class Person(object):
...
def get_name(self):
return self.__name
def get_sex(self):
return self.__sex
此時(shí),調(diào)用方法獲取實(shí)例的姓名和性別及輸出如下:
>>>baby = Person('zhengning', 'female', 162, 98)
>>>baby.get_name()
'zhengning'
>>>baby.get_sex()
'female'
如果又要允許外部代碼修改height和weight的話,可以再給Person類增加set_height和set_weight方法:
class Person(object):
...
def set_name(self):
self.__name = name
def set_sex(self):
self.__sex = sex
此時(shí),調(diào)用方法獲取實(shí)例的姓名和性別及輸出如下:
>>>from test1 import *
>>>baby = Person('zhengning', 'female', 162, 98)
>>>baby.get_height()
162
>>>baby.set_height(165)
>>>baby.get_height()
165
這里我們就需要考慮一個問題了,即最開始直接通過修改實(shí)例的屬性kevin.weight = 130即可修改屬性值,那么為什么要單獨(dú)定義一個方法來修改屬性呢?這是因?yàn)樵诜椒ㄖ校梢詫?shù)做檢查,避免傳入無效的參數(shù)。
class Person(object):
...
def set_weight(self, weight):
if 0 <= weight <= 200:
self.__weight = weight
else:
raise ValueError('Invalid weight.')
使用情況如下:
>>>from test1 import *
>>>baby = Person('zhengning', 'female', 162, 210)
>>>baby.get_weight()
210
>>> baby.set_weight(102)
>>>baby.get_weight()
102
>>>baby.set_weight(220)
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-7-038c5fd42666>", line 1, in <module>
baby.set_weight(220)
File "G:/PyCharm/PycharmProjects/Python_Liaoxuefeng/chapter06\test1.py", line 25, in set_weight
raise ValueError('Invalid weight.')
ValueError: Invalid weight.
上面說了這么多,我們再來考慮這么一個問題:雙下劃線開頭的實(shí)例是不是一定不能從外部訪問呢?其實(shí)也不是。不能直接訪問的原因在于Python解釋器對外把__name變量改成了_Student__name,所以,仍然可以通過_Student__name來訪問__name變量:
>>>zhengning._Person__height = 163
>>>zhengning._Person__height
163
6.3 繼承和多態(tài)
在面向?qū)ο蟮某绦蛟O(shè)計(jì)(OPP)中,當(dāng)我們定義一個類class時(shí),可以從某個現(xiàn)有的已定義的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Base class、Super class)。
比如,我們已經(jīng)編寫了一個名為Fruit的class,有一個run()方法可以直接打印:
class Fruit(object):
def taste(self):
print('Fruit is delicious.')
當(dāng)我們需要編寫Apple類和Pear類時(shí),就可以直接從Fruit類繼承:
class Apple(Fruit):
pass
class Pear(Fruit):
pass
對于Apple類和Pear類而言,F(xiàn)ruit類就是它們的父類,而它們就是Fruit類的子類。
繼承有什么好處呢?最大的好處是子類獲得了父類的全部功能。上述中,由于Fruit類定義了taste方法,因此,Apple和Pear作為它的子類,什么事也沒干,就自動獲得了taste()方法:
>>>from test2 import *
>>>apple = Apple()
>>>apple.taste()
Fruit is delicious.
>>>pear = Pear()
>>>pear.taste()
Fruit is delicious.
當(dāng)然,也可以直接對子類增加一些方法,比如在子類Apple中:
class Apple(Fruit):
def color(self):
print('The apple is red.')
繼承的第二個好處是:多態(tài)。即當(dāng)子類和父類都存在相同的方法時(shí),子類的方法會覆蓋父類的方法:
class Apple(Fruit):
def taste(self):
print('Fruit is delicious.')
class Pear(Fruit):
def taste(self):
print('Fruit is delicious.')
再次運(yùn)行,結(jié)果如下:
>>>from test2 import *
>>>apple = Apple()
>>>apple.taste()
The apple is delicious.
>>>pear = Pear()
>>>pear.taste()
The pear is delicious.
要理解什么是多態(tài),我們首先要對數(shù)據(jù)類型再做一點(diǎn)說明。當(dāng)我們定義一個class的時(shí)候,我們實(shí)際上就定義了一種數(shù)據(jù)類型。我們自己定義的數(shù)據(jù)類型和Python自帶的數(shù)據(jù)類型,比如str、list、dict沒什么兩樣:
aList = list() # a是list類型
fruit = Fruit() # b是Fruit類型
apple = Apple() # c是Apple類型
判斷一個變量是否是某個類型可以用isinstance()判斷:
>>>isinstance(apple, Apple)
True
現(xiàn)在,我們再思考一個關(guān)于繼承的問題:顧名思義,如果一個實(shí)例的數(shù)據(jù)類型是某個子類例如Aplle類型,那么它是否也屬于該類的父類Fruit類型呢?答案是肯定的:
>>>isinstance(apple, Fruit)
True
那么多態(tài)的好處在哪里呢?我們需要再編寫一個函數(shù),這個函數(shù)接受一個Fruit類型的變量:
def taste_twice(fruit):
fruit.taste()
fruit.taste()
當(dāng)我們傳入Fruit的實(shí)例時(shí),taste_twice()就打印出:
>>>taste_twice(Fruit())
Fruit is delicious.
Fruit is delicious.
當(dāng)我們傳入Apple實(shí)例時(shí),taste_twice()就打印出:
>>>taste_twice(Apple())
The apple is delicious.
The apple is delicious.
在這種情況下,如果我們再定義一個Orange類型,也從Fruit派生出來:
class Orange(Fruit):
def taste(self):
print('The orange is delicious.')
當(dāng)我們調(diào)用taste_twice()時(shí),傳入Orange的實(shí)例:
>>>taste_twice(Orange())
The orange is delicious.
The orange is delicious.
這時(shí),我們發(fā)現(xiàn),新增一個Fruit的子類Orange,而不必對taste_twice()做任何修改。實(shí)際上,任何以Fruit作為參數(shù)的函數(shù)或者方法都可以不加修改地正常運(yùn)行,原因就在于多態(tài)。
靜態(tài)語言 vs 動態(tài)語言
對于靜態(tài)語言而言,如果需要傳入Fruit類型,則傳入的對象必須是Fruit類型或者它的子類,否則,將無法調(diào)用run()方法。
對于Python這樣的動態(tài)語言來說,則不一定需要傳入Fruit類型,我們只需要保證傳入的對象有一個run()就可以了:
class Timer(object):
def taste(self):
print('Start...')
調(diào)用結(jié)果如下:
>>>taste_twice(Timer())
Start...
Start...
小結(jié)
- 繼承可以把父類的所有功能都直接拿過來,而不必直接定義屬性和方法。
- 多態(tài)則使得子類可以新增自己特有的方法,也可以把父類不合適的方法覆蓋重寫,只需從新定義和父類相同的方法。
6.4 獲取對象信息
當(dāng)我們拿到一個對象的引用時(shí),可以用哪些方法來知道這個對象是什么類型呢?
使用type()
首先,我們使用type()函數(shù)來判斷對象類型,基本類型都可以通過type()來判斷:
>>>type(123)
int
>>>type('str')
str
如果一個變量指向函數(shù)或者類,也可以用type()判斷:
>>>type(abs)
builtin_function_or_method
通過上述幾個例子,可以看出type()函數(shù)返回的類型是對象所應(yīng)的class類型。如果我們要在if語句中判斷,就需要比較兩個變量的type類型是否相同:
>>>type(123) type(456)
True
>>>type(123) int
True
>>>type('123') type('abc') # 注意123加了引號
True
>>>type(123) type('abc')
False
判斷基本類型可以直接寫int,str等,但如果要判斷一個對象是否是函數(shù)怎么辦?可以使用types模塊中定義的常量:
>>>import types
>>>def fcn():
pass
>>>type(fcn) 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
使用instance()
對于class的繼承關(guān)系來說,使用type()就不太方便。我們要判斷class類型的時(shí)候,可以使用instance()函數(shù),這在上節(jié)使用過。
此外,還可以判斷一個變量是否是某些類型中的一種:
>>>isinstance([1, 2, 3], (list, tuple))
True
>>>isinstance((1, 2, 3), (list, tuple))
True
這里需要注意幾個問題:
- 能用type()判斷的基本類型也可以使用isinstance()判斷
- 可以在if判斷語句中,同時(shí)使用多個type()和isinstance()來進(jìn)行邏輯運(yùn)算
使用dir()
如果要獲得一個對象的所有屬性和方法,可以使用dir()函數(shù),它返回一個包含字符串的list,比如,獲得一個str對象的所有屬性和方法:
>>>dir('zhengning')
Out[22]:
['__add__',
'__class__',
'__contains__',
'__len__',
...
'translate',
'upper',
'zfill']
類似__xxx__的屬性和方法在Python中都是有特殊用途的,比如__len__方法返回長度。在Python中,如果你試圖調(diào)用len()函數(shù)試圖獲取一個對象的長度,實(shí)際上,在len()函數(shù)內(nèi)部,它自動去調(diào)用該對象的__len__()方法,所以下面的代碼是等價(jià)的:
>>>len('zhengning')
9
>>>'zhengning'.__len__()
9
我們自己寫的類,也想用len(myObj)的話,就自己寫一個__len__方法:
class MyObj(object):
def __len__(self):
return 10
>>>baby = MyObj()
>>>len(baby)
100
剩下的就都是普通屬性或方法,比如upper()返回大寫的字符串:
>>>'zhengning'.upper()
'ZHENGNING'
上述只說明了如何把屬性和方法列出來,其實(shí)配合getattr()、setattr()、hasattr(),我們可以直接操作一個對象的狀態(tài):
class MyObj(object):
def __init__(self):
self.name = 'zhengning'
def love(self):
print('I love u.')
>>>from test3 import *
>>>baby = MyObj()
>>>hasattr(baby, name) # 屬性name需要加引號,否則會報(bào)錯
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-4-e82eb9214bf5>", line 1, in <module>
hasattr(baby, name)
NameError: name 'name' is not defined
>>>hasattr(baby, 'name') # 檢查是否有屬性'name'
True
>>>baby.name # 獲取屬性'name'
'zhengning'
>>>hasattr(baby, 'age') # 檢查是否有屬性'age'
False
>>>setattr(baby, 'age', 25) # 設(shè)置一個屬性'age'
>>>hasattr(baby, 'age')
True
>>>getattr(baby, 'age') # 獲取屬性'age'
25
>>>baby.age
25
如果試圖獲取不存在的屬性,會拋出AttributeError的錯誤:
>>>getattr(baby, 'height') # 獲取baby的屬性'height',沒有該屬性則報(bào)錯
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-14-15ec7e1fc4a1>", line 1, in <module>
getattr(baby, 'height')
AttributeError: 'MyObj' object has no attribute 'height'
也可以獲得對象的方法:
>>>hasattr(baby, 'love') # 檢查對象baby有方法'love'
True
>>>getattr(baby, 'love') # 獲取對象baby的方法'love'
<bound method MyObj.love of <test3.MyObj object at 0x000002074210F6A0>>
>>>myLove = getattr(baby, 'love') # 獲取方法'love'并賦值給變量myLove
>>>myLove # myLove指向baby.love
<bound method MyObj.love of <test3.MyObj object at 0x000002074210F6A0>>
>>>myLove() # 調(diào)用myLove()與調(diào)用baby.love()是一樣的
I love u.
小結(jié)
通過內(nèi)置的一系列函數(shù),我們可以對任意一個Python對象進(jìn)行剖析,拿到其內(nèi)部的數(shù)據(jù)。但需要注意的是,只有在不知道對象信息的時(shí)候,我們才會取獲取信息。比如,如果可以直接寫:
sum = obj.x + obj.y
就不要寫:
sum = getattr(obj, 'x') + getattr(obj, 'y')
一個正確的用法的例子如下:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
假設(shè)我們希望從文件流fp中讀取圖像,我們首先要判斷fp對象中是否存在read方法,如果存在,則該對象是一個流,如果不存在,則無法讀取。hasattr()就派上了用場。
6.5 實(shí)例屬性和類屬性
由于Python是動態(tài)語言,根據(jù)類創(chuàng)建的實(shí)例可以任意綁定屬性。給實(shí)例綁定屬性有兩種方法,即通過實(shí)例變量或者self變量:
class Person(object):
def __init__(self, name): # 通過self變量創(chuàng)建屬性
self.name = name
>>>baby = Person('zhengning')
>>>baby.name
'zhengning'
>>>baby.age = 25 # 通過實(shí)例變量創(chuàng)建屬性
那么,Person類本身怎么綁定屬性呢?可以直接在class中定義屬性,這種屬性是類屬性,歸Person類所有:
class Person(object):
race = 'Asian'
def __init__(self, name):
self.name = name
當(dāng)我們定義了一個類屬性后,這個屬性雖然歸類所有,但類的所有實(shí)例都可以訪問到:
>>>from test3 import * # 程序保存在test3.py文件中
>>>baby = Person('zhengning') # 創(chuàng)建實(shí)例baby
>>>baby.race
'Asian'
>>>print(baby.race) # 打印race屬性,因?yàn)閷?shí)例并沒有race屬性,所以會基礎(chǔ)查找class的name屬性。注意和上一條語句進(jìn)行對比
Asian
>>>print(Person.race) # 打印類的race屬性
Asian
>>>baby.race = 'Han' # 給實(shí)例綁定race屬性
>>>print(baby.race) # 由于實(shí)例屬性的優(yōu)先級比類屬性高,因此,它會屏蔽掉類的race屬性
Han
>>>print(Person.race) # 但是類屬性并未消失,仍然可以用左邊這種方式訪問
Asian
>>>del baby.race # 如果刪除實(shí)例的race屬性
>>>print(baby.race) # 再次調(diào)用baby.race,由于實(shí)例的race屬性沒有找到,類的race屬性就顯示出來了
Asian
從上面的例子可以看出,在編寫程序的時(shí)候,千萬不能把實(shí)例屬性和類屬性使用相同的名字,因?yàn)橄嗤Q的實(shí)例屬性將屏蔽掉類屬性。但是當(dāng)你刪除實(shí)例屬性后,再使用相同的名稱,訪問到的將是類屬性。