Python面向對象篇(3)-封裝、多態、反射及描述符

一、 多態

  多態,最淺顯的意識就是同一事物具備多種形態,這很好理解,動物是一個大類,貓類屬於動物類,狗類屬於動物類,人也是屬於動物類,那能理解成,貓、狗、人是同樣的嗎?固然不是,還有,水,分爲液體、固體、氣體,但它們都是H20構成的,這也是多態。意味着對於不一樣的類的對象,能夠執行相同的操做,html

  多態是面嚮對象語言的一個基本特性,多態就意味着變量不須要知道引用的對象是什麼,經過一個統一的接口,對引入的不一樣對象表現出不一樣的行爲方式。python

  下面這個例子編程

 1 import abc
 2 class Animal(metaclass=abc.ABCMeta):
 3     @abc.abstractmethod
 4     def yell(self):
 5         pass
 6 class Dog(Animal):
 7     def yell(self):
 8         print("汪汪汪~")
 9 class Cat(Animal):
10     def yell(self):
11         print("喵喵喵")

  在上面這段代碼中,類Animal做爲一個大類,定義了yell方法,但對於Animal來講,有不少不一樣的形態表示,貓,狗、人等等都屬於Animal類,而每一個類都有不一樣的叫聲,這就是同一類,多種形態的表示方式。函數

二、 封裝

  面向對象的程序設計中,某個類把所須要的數據(也能夠說是類的屬性)和對數據的操做(也能夠說是類的行爲)所有都封裝在類中,分別稱爲類的成員變量和方法(或成員函數)。這種把成員變量和成員函數封裝在一塊兒的編程特性稱爲封裝。封裝,其實就是爲了對外部隱藏對象的代碼邏輯。測試

       封裝在概念上和多態很類似,可是不等同於多態。多態可讓實例對不知道是什麼的類的對象進行屬性、方法的調用,而封裝是能夠不一樣關心對象是如何構建就能夠直接調用的。spa

  在python中用雙下劃線開頭的方式,將屬性隱藏起來。雙下劃線+屬性名就是私有的標識符,會自動變形爲_類名__屬性名的形式,即__x都會變造成_類名__x的形式,能夠從類的屬性字典中很明顯的看到這種變形。這種變換不會關注標識符的語法位置,所以能夠用來定義類私有的實例和類變量、方法、全局變量。設計

  看下面這個例子:建立一個類Person,類變量,全局變量,和方法都有經過雙下劃線__設置的私有變量代理

 1 class Person:
 2     __skin = "yellow"  #設置類的數據屬性爲私有的
 3     hair = "black"
 4     def __init__(self,name,age,height):
 5         self.__name = name    #變形爲self._Person_name的形式
 6         self.age = age
 7         self.height = height
 8     def __dance(self):        #變形爲_Person__dance的形式
 9         print("%s正在跳舞"%self.name)
10     def sing(self):
11         print("%s正在唱歌"%self.name)
12 p_1 = Person("Tony",18,160)

      在這個例子中,__skin、__self.name、__dance都是私有變量,外部的實例是沒法直接訪問的,經過實例p_1.屬性\方法的形式,是沒法調用這3個屬性的,這就是私有變量的意義所在。code

      但其實,這只是一種python的約定,若是非想在外部的實例調用其私有變量,也並非不可能。若是知道了類名和想調用的私有屬性名,就能夠調用。htm

即p_1._Person__skin這種方式,(注意:類名前是一個下劃線,私有屬性前是雙下劃線),調用方法p_1._Person__dance()

      封裝並不只僅是爲了將類中的屬性隱藏起來,更多的需求是將私有屬性做爲外部調用的一個接口,用來對傳入的數據作相應的限制,以下面這個例子:

 1  class Person:
 2     def __init__(self,name,age):
 3         self.__name = name
 4         self.__age = age
 5     def person_info(self):
 6         print("My name is %s,My age is %s"%(self.__name,self.__age))
 7     def set_person_info(self,name,age):
 8         if not isinstance(name,str):
 9             raise TypeError("名字必須是字符串類型")
10         if not isinstance(age,int):
11             raise TypeError("年齡必須是整數類型")
12             self.__name = name
13             self.__age = age
14 p_1 = Person("Tony",23)
15 p_1.person_info()
16 p_1.set_person_info(11,"An")
17 p_1.person_info()

    在上面這個例子中,想要調用person_info()這個方法來顯示出一我的的姓名和年齡,可是若是想要限制用戶輸入的姓名和年齡的數據類型,就要經過set_person_info()這個方法來做爲用戶輸入的一個接口。

    單下劃線定義的模塊名,不用from..import..的方式導入

    受權是封裝的一個特性,

三、 反射

    反射(或自省)主要是指程序能夠訪問、檢測、和修改它自己狀態或行爲的一種能力,在python中,經過字符串的形式操做對象相關的屬性,python中的一切事物都是對象,即均可以使用反射。

     hasattr、getattr、setattr、delattr

    四個能夠實現自省的函數:

hasattr(object,name)

判斷對象是否包含對應的屬性。

object -- 對象

name   -- 字符串,屬性名

getattr(object,name,[default])

getattr() 函數用於返回一個對象屬性值。

object -- 對象。

name   -- 字符串,對象屬性。

default -- 默認返回值,若是不提供該參數,在沒有對應屬性時,將觸發 AttributeError。

setattr(object,name,value)

setattr 函數對應函數 getatt(),用於設置屬性值,該屬性必須存在。

object – 對象

name    - 字符串,屬性名

value   - 屬性值

 

delattr(object, name)

delattr 函數用於刪除屬性。

delattr(x, 'foobar') 相等於 del x.foobar。

object -- 對象。

name -- 必須是對象的屬性。

   

        經過下面這個實例,來演示一下4個函數的使用方法:

 1 class Person:
 2     skin = "yellow"
 3     hair = "black"
 4     def __init__(self,name,age):
 5         self.name = name
 6         self.age = age
 7     def dance(self):
 8         print("%s正在跳舞"%self.name)
 9 p_1 = Person("Tony",18)
10 #判斷Person類中是否含有dance屬性,
11 print(hasattr(Person,"dance"))
12 #獲得Person類中的skin屬性的值,若是傳入的是方法名,返回的就是方法的內存地址
13 print(getattr(Person,"skin"))
14 print(getattr(Person,"skin11"))   #屬性不存在,程序報錯
15 #修改Person類中skin屬性的值,類屬性字典中也會作相應改變,經過Person.__dict__查看
16 setattr(Person,"skin","black")
17 print(getattr(Person,"skin"))
18 #刪除Person類中hair這個屬性,類的屬性字典中也會刪除
19 delattr(Person,"hair")
20 delattr(Person,"hair11")         #屬性不存在,報錯  

   反射有什麼好處?反射就能夠事先定義好接口,這意味這什麼呢?就是能夠先寫好主要的邏輯,而後再去後期實現實現的功能。

   __getattr__、__setattr__、__delattr__

 1 class Attr:
 2     def __init__(self,x):
 3         self.x = x
 4     def __getattr__(self, item):
 5         print("--->getter")
 6     def __setattr__(self, key, value):
 7         print("--->setattr")
 8     def __delattr__(self, item):
 9         print("--->delattr")

  在這個例子中,重寫了__getattr__、__setattr__、__delattr__這三個方法。這三個方法的存在會對類發生什麼改變呢?

  • __getattr__:

  只有在實例調用屬性且調用的屬性不存在纔會運行

1 print(a_1.x)
2 運行結果:
3 --->setattr    #只要賦值操做,就會觸發__setattr__方法
4 6
5 print(a_1.y)
6 運行結果:
7 --->setattr
8 --->getter
9 None

  還有一個__getattribute__方法,不管調用的屬性是否存在,都會觸發__getattribute__方法。當__getattribute__和__getattr__同時存在的話,只會執行__getattribute__,除非__getattribute__在執行的過程當中拋出異常

  • __setattr__:
    1 a_1 = Attr(6)
    2 print(a_1.__dict__)
    3 運行結果:
    4 {'x': 6}
    5 刪除註釋,再次執行:
    6 運行結果:
    7 --->setattr
    8 {}

    先將這兩行註銷,而後建立一個實例並打印這個實例的屬性字典:

 

    建立了實例,可是屬性確沒有寫進屬性字典中,這是爲何?

    這是由於,凡是賦值的操做,都會觸發__setter__()方法的運行,而在上面代碼中,重寫了__setattr__方法,代碼邏輯僅僅有打印「--> setattr」操做,根本就沒有將屬性值寫入實例屬性的字典的操做,天然不會被寫入,除非直接操做屬性字典,不然根本沒法往裏面寫入值。那麼就須要進行以下修改:

 1 class Attr:
 2     def __init__(self,x):
 3         self.x = x
 4     def __getattr__(self, item):
 5         print("--->getter")
 6        self.__dict__[key]=value
 7     def __setattr__(self, key, value):
 8         print("--->setattr")
 9     def __delattr__(self, item):
10         print("--->delattr")

    直接將實例化傳入的值,寫入實例的屬性字典,有些人很容易將這步操做寫成

    self.key = value ,這條語句是錯誤的,會致使程序無限遞歸。

  • __delattr__

    與__setattr__實質相同,在重寫了__delattr__方法後,也須要定義相應的代碼來從實例的屬性字典中刪除。即:

 1 class Attr:
 2     def __init__(self,x):
 3         self.x = x
 4     def __getattr__(self, item):
 5         print("--->getter")
 6        self.__dict__[key]=value
 7     def __setattr__(self, key, value):
 8         print("--->setattr")
 9     def __delattr__(self, item):
10         print("--->delattr")
11        self.__dict__.pop(item)

    能夠先經過a_1.__dict__["a"]=1在實例的屬性字典中添加屬性a,進行刪除測試,一樣的,不能夠在__delattr__()後經過del self.item的方式來刪除,這樣一樣會引發無限遞歸。

四、 描述符

    描述符是Python語言中很深奧也很重要的一個概念,簡而言之,描述符也是一個對象,該對象表明了一個屬性的值,這就意味着,若是一個類中有一個屬性name,若是給該類添加了描述符,那麼描述符就是另外一個能表明屬性name的對象。描述符協議中定義了_get_、_set_、_del_這些特殊的方法,描述符是實現其中的一個或多個方法的對象。這樣說你們可能沒有辦法理解,仍是經過代碼講解吧。

    __get__():調用一個屬性時,觸發
    __set__():爲一個屬性賦值時,觸發
    __delete__():採用del刪除屬性時,觸發

    定義一個描述符:

1 class Descriptor:
2     def __get__(self, instance, owner):
3         pass
4     def __set__(self, instance, value):
5         pass
6     def __delete__(self, instance):
7         pass

    前面說過了,描述符就是一個類,若是一個類中定義了這三個方法,這個類就被稱爲一個描述符。

    在前面講過了__getattr__、__setattr__、__delattr__、方法,在由類產生實例的過程當中會相應的觸發這三個方法,而對_get_、_set_、_delete_來講,並不會被觸發,那麼觸發這三個函數的條件又是什麼?

 1 class Descriptor:             #建立描述符Descriptor
 2     def __get__(self, instance, owner):
 3         print("--->get")
 4     def __set__(self, instance, value):
 5         print("--->set")
 6     def __delete__(self, instance):
 7         print("--->delete")
 8  
 9 
10 class Person:
11     name = Descriptor()        #屬性name被Descriptor描述符描述
12     def __init__(self,name):
13         self.name = name

    首先,描述符對描述符自身是沒有用的,顯而易見,之因此叫描述符,是用來描述其餘對象的(類也是對象),從上段程序的運行結果能夠明顯的看出,name屬性被描述符代理後,經過實例調用、設置、刪除該屬性,實質上執行的都是描述符Descriptor中的邏輯。描述符不能夠被定義到構造函數中,必須賦值給類屬性。這樣寫有什麼意義呢?

 1 p_1 = Person("Tony")
 2 print(p_1.__dict__)
 3 p_1.name = "An"
 4 print(p_1.__dict__)
 5 p_1.age = 18
 6 print(p_1.__dict__)
 7 運行結果:
 8 --->set
 9 {}
10 --->set
11 {}
12 {'age': 18}

    建立了一個Person類的實例p_1,首先會觸發_set_()方法,可是p_1的name=Tony這個屬性並無寫入到它的屬性字典中,修改name的屬性爲An,一樣沒有寫入。這是由於,將Person類中的name這個屬性經過name = Descriptor()  的方式被描述符代理了,因此調用該屬性的時候,實質上就會去觸發描述符Descriptor中的_set_方法,因此就沒法寫入。在最後添加了一個屬性age,成功的寫入到了實例p_1的屬性字典中,是由於age這個屬性沒有被其餘描述符代理,使用的就是類默認的邏輯來執行。

若是想要自定義的描述中也具有屬性寫入的功能,能夠將Descriptor中的_set_()修改以下:

1 def __set__(self, instance, value):
2         print("--->set")
3         instance.__dict__["name"]=value

    經過這個例子,能夠看到描述符的強大之處,有不少時候,咱們須要控制某些變量屬性,就能夠經過描述符來自定義想要的功能。

    描述符分爲兩種:

    數據描述符:至少實現了_get_()方法和_set_()方法。

    非數據描述符:沒有實現_set_()方法。

    在調用的時候,應遵循如下優先級:類屬性-->數據描述符-->實例屬性-->非數據描述符-->__getattr__()方法(若是找不到屬性的時候觸發)

    __str__()和__repr__()方法:

1 class Person:
2     def __init__(self,name):
3         self.name = name
4     def __str__(self):
5         return ("my name is %s"%self.name)
6 p_1 = Person("An")
7 print(p_1)

    __str__發放就是能夠自定義對象的字符串返回形式,若是不自定義__str__()方法,返回結果是:An,定義後,會按照定義的形式輸出結果,即:my name is An。

       __repr__()方法能夠理解爲__str__()方法的替代品,若是__str__沒有被定義,就會使用__repr__()方法來代替輸出。

         __slots__屬性:

    __slots__實際上是一個類變量,訪問類或實例的時候,實質上就是在訪問類和實例的屬性字典__dict__,(類的屬性字典是共享的,即全部所屬該類的實例均可以訪問,實例的屬性字典是是獨立的)。

1 p_1 = Person()
2 p_1.name = "Tony"
3 #p_1.age = 18           #報錯
4 #print(p_1.__dict__)   #報錯,定義了__slots__,就再也不有dict
5 print(p_1.__slots__)
6 運行結果:
7 name

    可是字典是很佔用計算機內存的,當類的屬性不多,可是須要不少所屬該類的實例的時候,就能夠用__slots__來代替__dict__,__slots__中定義的變量就變成了類的描述符,類的實例只能擁有這些變量,可是不會擁有屬於本身的屬性字典,所以也就是不能增長新的變量。

       經過__slots__,能夠限制實例的屬性,即減少的佔用的內存空間,也會大大提高屬性的訪問速度。

五、 迭代器協議

    __next__和__iter__實現迭代器協議。

 1 class Iterator():
 2     def __init__(self,x):
 3         self.x = x
 4     def __iter__(self):
 5         return self
 6     def __next__(self):
 7         
 8         self.x+=1
 9         return self.x
10 i_1 = Iterator(1)
11 for i in i_1:
12     print(i)

    這樣就經過__iter__和__next__方法實現了迭代器。

    斐波那契數列:

 1 class Fibo():
 2     def __init__(self,x,y):
 3         self.x = x
 4         self.y = y
 5     def __iter__(self):
 6         return self
 7     def __next__(self):
 8         if self.x > 20:
 9             raise StopIteration("超出終止")
10         self.x = self.y
11         self.y = self.x + self.y
12         return self.x
13 f_1= Fibo(0,1)
14 for i in f_1:
15     print(i)

六、 上下文管理協議:__enter__和__exit__

    在管理文件操做的時候,能夠經過with..open的方式來自動釋放文件的內存,在類中,若是想讓對象兼容with語句,必須在這個類中聲明__enter__和__exit__方法

 1 class File:
 2     def __init__(self,x):
 3         self.x = x
 4     def __enter__(self):
 5         print("--->enter")
 6     def __exit__(self, exc_type, exc_val, exc_tb):
 7         print("--->exit")
 8 with File("test.txt") as f:
 9 print("對象兼容with語句")
10 運行結果:
11 --->enter

    對象兼容with語句

    --->exit

    從結果能夠看出,文件操做流程中,首先觸發__enter__方法,而後執行代碼塊中的內容,最後觸發__exit__方法來釋放文件內存。

    __exit__()中的三個參數分別表明異常類型,異常值和追溯信息,with語句中代碼塊出現異常,則with後的代碼都沒法執行。

    __call__:__call__方法的執行是經過對象名或類名後加()來執行的。

相關文章
相關標籤/搜索