[Python]第七章再談抽象
文章目錄
7.1物件魔法
使用物件的好處:
多型:可對不同型別的物件指向相同的操作,而這些操作一樣能夠正常執行
封裝:對外部隱藏有關物件工作原理的細節
繼承:可基於通用類創建出專用類
7.1.1多型
這大致意味著即便你不知道變數指向的是哪種物件,也能夠對其執行操作,且操作的行為將隨物件所屬的型別(類)而異。
7.1.2多型和方法
方法:與物件屬性相關聯的函式稱為方法
以下是字串、列表的方法count()
>>> 'abc'.count('a')
1
>>> [1, 2, 'a'].count('a' )
1
如果有一個變數x,你無需知道它是字串還是列表就能呼叫方法count:只要你向這個方法提供一個字元作為引數,它就能正常執行。
每當無需知道物件是什麼樣的就能對其執行操作時,都是多型在起作用。
通過內建運算子和函式大量使用了多型。
>>> 1 + 2
3
>>> 'Fish' + 'license'
'Fishlicense'
事實上,要破壞多型,唯一的辦法是使用諸如type、issubclass等函式顯式地執行型別檢查,
7.1.3封裝
封裝(encapsulation)指的是向外部隱藏不必要的細節。
多型讓你無需知道物件所屬的類(物件的型別)就能呼叫其方法,而封裝讓你無需知道物件的構造就能使用它。
>>> o = OpenObject() # 物件就是這樣建立的
>>> o.set_name('Sir Lancelot')
>>> o.get_name()
'Sir Lancelot'
要將名稱‘封裝’在物件中,將其作為一個屬性即可
7.1.4繼承
如果你已經有了一個類,並要建立一個與之很像的類(可能只是新增了幾個方法),就將這個新類繼承之前的類,對新類物件呼叫老類的方法即可
7.2類
7.2.1類到底是什麼
每個物件都屬於特定的類,併成為該類的例項
一個類(雲雀)的物件為另一個類(鳥)的子集時,前者就是後者的子類,後者就是前者的超類
子類的所有例項都有超類的方法,需要定義子類時,只需要新增父類沒有的方法,或者重寫一些既有的方法
7.2.2建立自定義類
class Person:#class語句建立獨立的名稱空間,用於在其中定義函式
class Student:
job=’teacher’
def info(self,name,Id):
self.name=name
self.Id=Id
self.sex='male'
def print(self):
print(self.name,self.Id,self.sex)
>>>s=Student()
>>>s.info('jack',1)
>>>s.print()
jack 1 male
對s呼叫info()和print()時,s都會作為第一個引數自動傳遞給它們。因此在傳參時,實際都自動傳了第一個引數,這個看不見的引數就是self,self作為預設的第一引數不能省略。
這裡self就是指類本身,self.name就是Student類的屬性變數,是Student類所有。而name是外部傳來的引數,不是Student類所自帶的。故,self.name = name的意思就是把外部傳來的引數name的值賦值給Student類自己的屬性變數self.name。
self.sex是物件的屬性,只屬於建立後的例項
job是整個類的屬性,每個例項也具有該屬性
name已經成為該類的一個屬性,封裝在每個物件上,因此可以從外部直接呼叫和訪問
>>>s.name
‘jack’
7.2.3 屬性、函式、方法
#這是一個普通的函式
def funct():
print('I dont not...')
#這是一個方法
class Class:
def meth(ff):
print('I have a self')
>>>type(funct)
function
>>>type(a.meth)
method
函式可以直接執行
>>>funct()
I dont not...
方法需要建立物件來呼叫
>>>a=Class()
>>>a.meth()
I have a self
將屬性關聯到一個普通函式,呼叫這個方法實際執行的是函式
>>>a.meth=funct
>>>a.meth()
I dont not...
方法也可以賦值給變數, 將另一個變數指向同一個方法,這個變數也被關聯到類的例項
class Bird:
color='blue'
def show(self):
print(self.color)
>>>a=Bird()
>>>a.show()
blue
>>>birdcolor=a.show #方法賦值給變數
>>>birdcolor()
blue
7.2.4再談隱藏
一個普通的類普通的方法可以外部訪問(例項.方法名/屬性名)
class Student:
def info(self,name):
self.name=name
>>>s=Student()
>>>s.info('jack')
>>>s.name#外部方式訪問
'jack'
當然也可以通過外部方式修改物件,因為現在name屬性就是公共的
>>>s.name=‘hellen’
>>>s.name
‘hellen’
為避免這種情況,可將屬性定義為私有,讓其名稱以兩個下劃線打頭即可
class Student:
def info(self,name):
self.__name=name
>>>s=Student()
>>>s.info('jack')
>>>s.__name#如果通過內部函式賦值,無法再從外部訪問,只能在內部呼叫
報錯AttributeError: 'Student' object has no attribute '__name
>>>hasattr(s,'__name')
False
如果仍然用外部方法修改和訪問
>>>s.__name='jj'#相當於給s另外添加了一個外部屬性__name,實際上__name改成其他名稱都可以,只是新增了另外一個屬性
>>>s.__name
'jj'
>>>hasattr(s,'__name')
True
這時候原來的name屬性已經是私有的了,無法從外部訪問
需要在類內部構造一個訪問的方法
class Student:
def info(self,name):
self.__name=name
def print(self):#訪問的方法
print(self.__name)
>>>s.print()#通過print方法訪問
jack
這樣外部也無法修改物件內部狀態
同理,如果將方法也加兩個下劃線,正常呼叫方法也會出錯
class Student:
def __info(self,name):
self.__name=name
>>>s=Student()
>>>s.info('jack')
報錯
仍然可以在類內部構造一個訪問私有方法的函式
class Student:
def __info(self,name):
self.__name=name
return self.__name
def print(self):
print(self.__info('alice'))
>>>s=Student()
>>>s.print()
alice
如果一定要從外部訪問私有方法,可以物件名._類名__私有方法名
>>>s._Student__info('allen')
‘allen’
7.2.5類的名稱空間
def foo(x):return x*x等價於foo=lambda x:x*x。它們都建立一個返回引數平方的函式,並將這個函式關聯到變數foo。
在class語句中定義的程式碼都是一個特殊的名稱空間(類的名稱空間)內執行的,而類的所有成員都可訪問這個名稱空間,類定義其實就是要指向的程式碼段。
class C:
print('nothing')
nothing
class C:
n=0#在類裡面定義的屬性是整個類的屬性
def init(self):
C.n+=1
>>>a=C()
>>>a.init() #初始化所有例項
>>>C.n
1
>>>b=C()
>>>b.init()
>>>C.n
2
上述程式碼在類作用域內定義了一個變數,所有的成員(例項)都可以訪問它
>>>a.n#類的屬性值也是類下面例項的屬性值
2
>>>b.n
2
>>>a.n=5#只改變例項的屬性值
>>>a.n
5
>>>b.n
2
>>>C.n
2
新值被寫入a的一個屬性中,這個屬性遮住了類級變數
a=1#全域性空間變數
#scope1=vars()
#print(scope1)
class Test:
a=2#類屬性
#scope2=vars()
#print(scope2)
def sing(self,song):
a=3#方法空間變數,如果方法中a=3不存在
#scope3=vars()
#print(scope3)
self.a=4#方法空間屬性,如果方法中沒有定義self.a=4
self.name='cindy'
self.song=song
sc=vars()
print(self.a,self.name,'is singing',self.song,)
print(a)
>>>t=Test()
>>>t.sing('<happy new year>')
4 cindy is singing <happy new year> # 2 cindy is singing <happy new year>
3 #1
這裡
scope1打印出來,是個全域性作用域,’a’:1在這個全域性作用域中
scope2打印出來的是類的作用域
{’__module__’: ‘__main__’, ‘__qualname__’: ‘Test’, ‘a’: 2, ‘scope2’: {…}}
scope3打印出來是方法的作用域,其中self.a和self.name都不在該作用域
{‘a’: 3, ‘song’: ‘’, ‘self’: <__main__.Test object at 0x0000000004FD5DA0>}
如果方法中self.a=4不存在,則打印出來的self.a=2,用的是類空間的屬性值
2 cindy is singing
如果方法中a=3不存在,則打印出來的a=1
因此引用順序都是由內向外的:
self.a 屬性的引用優先順序:方法空間self.a ——>類空間a
a變數的引用優先順序:方法空間a ——>全域性空間a
7.2.6指定超類
要指定超類,可在class語句中的類名後加上超類名,並將其用圓括號括起。
class Filter: #定義超類
def init(self):
self.bloked=[]#bloked可以改成其他名稱,只是用來指定一個屬性值
def filter(self,sequence):
return [x for x in sequence if x not in self.bloked]
class SpamFilter(Filter):#子類SpamFilter繼承超類Filter
def init(self):#重寫超類的方法
self.bloked=['spam']
>>>f=Filter()
>>>f.init()
>>>f.filter([1,2,3])
[1, 2, 3]
>>>s=SpamFilter()
>>>s.init()
>>>s.filter(['spam','span',1,2,3])
['span', 1, 2, 3]
定義子類的兩大要點:
繼承
重寫
7.2.7深入探討繼承
要確定一個類是否是另一個類的子類,可用內建函式issubclass(子類,超類)
>>>issubclass(SpamFilter,Filter)
True
獲取一個類的基類,可以訪問其特殊屬性:類.__bases__
>>>SpamFilter.__base__
__main__.Filter
>>>SpamFilter.__bases__
(__main__.Filter,)
要確定一個物件是否是該類的例項,可用內建函式isinstance(例項,類)
>>>isinstance(s,SpamFilter)#s是SpamFilter的直接例項
True
>>>isinstance(s,Filter) #s是Filter的間接例項
True
>>>a='dfdfsd'#isinstance也可用於型別
>>>isinstance(a,str)
True
獲取一個物件的類,使用屬性:例項.__class__,也可以用type(例項)檢視
>>>s.__class__
__main__.SpamFilter
>>>type(s)
__main__.SpamFilter
7.2.8多個超類
多重繼承:一個子類繼承自多個超類
class Caculator:
def caculate(self,expression):
self.value=eval(expression)
class Talker:
def talk(self):
print('I am {}'.format(self.value))
class TalkerCaculator(Caculator,Talker):
pass
>>>tc=TalkerCaculator()
>>>tc.caculate('5+9*2')#直接呼叫超類方法
>>>tc.talk()
I am 23
如果多個超類有同名方法,在定義子類的繼承順序上,靠前的超類方法會覆蓋靠後的超類方法,查詢特定方法或屬性時訪問超類的順序稱為方法解析順序(MRO),
class Caculator:
def caculate(self,expression):
self.value=eval(expression)
def talk(self):
print('I come from caculator')
class Talker:
def talk(self):
print('I am {}'.format(self.value))
class TalkerCaculator(Caculator,Talker):
pass
>>>tc=TalkerCaculator()
>>>tc.caculate('5+9*2')
>>>tc.talk()
I come from calculator
7.2.9介面和內省
介面(協議):對外暴露的方法和屬性
hasattr(例項.方法或屬性) 函式用於判斷物件是否包含對應的屬性和方法。
class Test:
a=1#類的屬性
def sing(self,song):
self.name='cindy'
self.song=song
print(self.a,self.name,'is singing',self.song)
>>>t=Test()
>>>t.sing('《好運來》')
1 cindy is singing 《好運來》
>>>hasattr(t,'a')
>>>hasattr(t,'song')
>>>hasattr(t,'name')
>>>hasattr(t,'sing')
True
指定的、未指定的、外部傳入的屬性和方法都可以用該函式判斷
callable()檢查方法是否可被呼叫,getattr()返回屬性值,屬性不存在時可指定預設值,setattr()設定物件的屬性
>>>getattr(t,'sing')
<bound method Test.sing of <__main__.Test object at 0x0000000004BCD400>>
>>>callable(getattr(t,'sing',None))
True
>>>getattr(t,'name')
cindy
>>>setattr(t,'name','alice')
>>>t.name
‘alice’
要檢視物件中儲存的所有值,可檢查屬性:例項.__dict__
>>>t.__dict__
{'name': 'cindy', 'song': '《好運來》'}#類a屬性不在其中
7.2.10抽象基類
有些第三方模組提供了顯式指定介面的理念的各種實現,Python通過引入模組abc提供了官方解決方案。
一般而言,抽象類是不能(至少是不應該)例項化的類,其職責是定義子類應實現的一組抽象方法。
from abc import ABC,abstractmethod
class Talker(ABC):
@abstractmethod#裝飾器用來將方法標記為抽象的,在子類中必須實現方法
def talk(self):
pass
這裡定義的Talker類就是抽象類(即包含抽象方法的),最重要的特徵是不能被例項化
>>>tt=Talker()
報錯
即使是從它派生子類,只要沒有把抽象的方法重寫,它仍然無法被例項化
class Ta(Talker):
pass
>>>tr=Ta()
報錯
但是如果重寫這個抽象方法
class Ta(Talker):
def talk(self):
print('hi')
>>>tr=Ta()
則不會報錯
>>>tr.talk()
hi
>>>isinstance(tr,Talker)#tr也是Talker抽象類的例項
True
Notice:如果抽象類Talker()裡面有多個方法,需要在子類中重寫被@abstractmethod
標記的全部方法,如果只是普通的方法,沒有被標記的,就可以不重寫。
另外再寫一個不是從Talker派生出來的類
class Duck:
def talk(self):
print('duck')
>>>d=Duck()
>>>isinstance(d,Talker)
False
顯然它不是Talker類
但是,可以將它註冊到Talker類:基類.register(被註冊的類)
>>>Talker.register(Duck)
__main__.Duck
>>>isinstance(d,Talker)
True
這樣Duck類的例項也是Talker類的例項,同時Duck類也成為了Talker的子類
>>>issubclass(Duck,Talker)
True
如果Duck沒有重寫talk方法,Duck類同樣不能例項化
參考文獻:https://blog.csdn.net/CLHugh/article/details/75000104
PS:內建屬性__init__,建立物件時可以直接傳參
class Student(object):
def __init__(self, name, score):#這裡的__init__是內建方法,把屬性繫結到物件
self.name = name
self.score = score
>>>student = Student("Hugh", 99)#直接傳參
>>>student.name
"Hugh"
>>>student.score
99