1. 程式人生 > >[Python]第七章再談抽象

[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