Python學習筆記八 面向對象高級編程(一)
參考教程:廖雪峰官網https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000
一、使用__slots__
正常情況下,當定義了一個類之後,我們可以給這個類的實例綁定任何屬性,這就是動態語言的優勢:
class Student(object): def __init__(self,name,age): self.name=name self.age=age bob=Student(‘Bob‘,19) stam=Student(‘Stam‘,20) bob.score=98 #給示例bob增加了屬性score可以訪問 print(‘%s:%d years old,score is %d.‘%(bob.name,bob.age,bob.score)) #實例stam並沒有score屬性,報錯 print(‘%s:%d years old,score is %d.‘%(stam.name,stam.age,stam.score))
同樣也可以給類的實例綁定方法:
#先定義一個類的實例方法 def set_num(self,num): self.num=num #導入types庫 from types import MethodType #使用綁定方法Methodtype()bob.set_num=MethodType(set_num,bob) bob.set_num(11) #輸出11 print(bob.num) #報錯,因為實例stam並沒有綁定這個方法 stam.set_num(12) print(stam.num)
上述代碼中給實例綁定方法的方式是先創建一個類的實例方法,然後再通過MethodType()把這個方法綁定到實例對象bob上。
另外一種給實例綁定方法的方式如下:
def sumfunc(x): print(‘this is sumfunc function!---%d‘% x) bob.pfunc=sumfunc bob.pfunc(101)
而上述這種綁定方法只能針對具體的實例綁定 ,如果在一個類定義之後,發現這個類需要一種方法,也可以通過如下方式綁定:
#先定義一個Student類 #此時類中沒有set_score方法 class Student(object): def __init__(self,name): self.name=name #創建類的實例 bob=Student(‘Bob‘) #此時發現需要給Student增加一個設置分數的方法 #先定義一個類的方法 def set_score(self,score): self.score=score #再將這個方法名直接賦給類的屬性,為方便起見,當然是給類新增一個和這個方法同名的類方法 #此時,Student類不僅有了這個set_score方法,也有了score這個屬性 Student.set_score=set_score bob.set_score(96) print(bob.score)
在實際應用中,我們往往可以實現知道某個類可能需要新增的屬性,而可以通過某些手段限制類定義的屬性,這裏就需要用到‘__slots__‘:
class Student(object): __slots__ = (‘name‘, ‘age‘) # 用tuple定義允許綁定的屬性名稱
通過給實例增加屬性看一下__slots__的效果:
class Student(object): __slots__ = (‘name‘, ‘age‘) # 用tuple定義允許綁定的屬性名稱 bob=Student() bob.name=‘Bob‘ print(bob.name) bob.age=‘16‘ print(bob.age) #以下代碼出錯,因為屬性country不在__slots__範圍裏 bob.country=‘British‘ print(bob.country)
但__slots__僅對其所在的類的實例起作用,對這個類的所有子類實例是沒有限制作用的,如果要限制子類的新增屬性,需要在子類定義中的__slots__設置。而子類設置__slots__之後,其可以新增的屬性不僅僅是其slots限定的範圍,還可以包括父類slots所含的屬性:
class Student(object): __slots__ = (‘name‘, ‘age‘) # 用tuple定義允許綁定的屬性名稱 class Wstudent1(Student): pass Wangm=Wstudent1() #子類在本身沒有__slots__限制的情況下,其實例新增屬性不受父類__slots__限制 Wangm.num=16 print(Wangm.num) #因Wstudent2定義時通過__slots__限制了屬性 #所以其實例新增屬性受到父類和其本身的__slots__的限制 class Wstudent2(Student): __slots__=(‘country‘,‘score‘) Wanglei=Wstudent2() Wanglei.score=100 print(Wanglei.score) Wanglei.country=‘China‘ print(Wanglei.country) Wanglei.age=15 print(Wanglei.age) Wanglei.name=‘WangLei‘ print(Wanglei.name) #本身和父類的__slots__中均不含num屬性,所以報錯 Wanglei.num=1 print(Wanglei.num)
二、使用@property
"在這一節的學習中,發現自己對於類屬性中訪問限制一些概念理解得還不夠透徹,因此在開始@property之前,先復習鞏固一下訪問限制的相關內容。"
首先看一下最簡單、基礎的代碼:
class Student(object): def __init__(self,name,score): self.name=name self.score=score wanghai=Student(‘WangHai‘,99) liyue=Student(‘LiYue‘,100) print(wanghai.score) print(liyue.score) wanghai.score=95 liyue.name=‘LiYU‘ print(wanghai.score) print(liyue.name)
對於上面的代碼,Student類有兩個屬性score和name,並且在類定義結束後,使用者可以隨時修改實例的屬性值。
那麽,針對於此,怎樣才可以讓使用者不能隨意修改呢?我們首先想到添加類方法set方法:
class Student(object): def __init__(self,name,score): self.name=name self.score=score def set_name(self,name): self.name=name def set_score(self,score): self.score=score wanghai=Student(‘WangHai‘,99) print(wanghai.score) #是不是真的不能修改了呢? wanghai.score=95 wanghai.name=‘wanghang‘ print(wanghai.name) print(wanghai.score)
可是運行結果可以發現,還是可以直接給實例的屬性賦值修改。所以可見,set方法不能約束使用者直接修改屬性值的行為。
那麽有沒有什麽方法可以起到約束的作用呢。
答案是在定義類的時候在需要約束的屬性名前增加兩個下劃線"__":
class Student(object): def __init__(self,name,score): self.__name=name self.score=score def set_name(self,name): self.__name=name def set_score(self,score): self.score=score wanghai=Student(‘WangHai‘,99) #正常輸出99 print(wanghai.score) #下一句報錯 print(wanghai.__name)
我們可以發現,通過對name屬性名修改為‘__name‘之後,訪問wanghai.__name報錯,提示信息為類沒有這個屬性。
而訪問score屬性正常,因為並沒有對score設置限制。
再進一步,是不是對於"__name"屬性無法直接賦值修改了呢,上段代碼中增加:
wanghai.__name="WH" print(wanghai.__name)
我們卻奇怪的發現,居然代碼可以執行,可以修改,並且正常輸出了"WH"。這到底是怎麽回事呢?
是因為對於設置屬性名的訪問限制方法,即增加雙下劃線的方法後,其實在Python解釋器遇見這個屬性名後,會在解釋器內部再次修改這個屬性名為:‘_Student__name‘,而非代碼中我們看到的"__name"!這也就可以解釋為何上述代碼可以執行了,因為上面代碼實質上是給實例增加了一個__name屬性,這個屬性非類定義中的‘__name"屬性(類定義中的該屬性名實際為_Student__name)。
如果類定義中有get方法,那通過get方法我們可以更清晰的看出上述代碼增加後其實並沒有改變類定義中的‘__name‘屬性值:
class Student(object): #簡便起見,僅考慮一個屬性 def __init__(self,score): #這裏的__score解釋器轉換為_Student__score self.__score=score def get_score(self): #這裏的__score解釋器轉換為_Student__score return self.__score def set_score(self,score): self.__score=score #1、常規創建實例 wanghai=Student(99) #正常輸出99 print(wanghai.get_score()) #通過set方法修改屬性值 wanghai.set_score(100) #正常輸出100 print(wanghai.get_score()) #嘗試直接訪問屬性 #因為類定義中的__score非這裏的__score #並且實例中也沒有增加這個__score屬性,所以報錯類不存在這個__score print(wanghai.__score) #那麽嘗試直接通過_Student__name屬性名直接訪問 #可以正常輸出,輸出100 print(wanghai._Student__score) #繼續測試,給實例增加__score屬性 wanghai.__score=88 #此時增加的非類定義中的__score,所以是可以添加成功,並且可以直接訪問 #輸出88 print(wanghai.__score) #再看一下類定義中的__score屬性值(實際為_Student__name) #輸出依然是100,沒有變化 print(wanghai.get_score()) print(wanghai._Student__score)
所以綜上,小結一下:
1、如果要真正限制類定義中的屬性的修改和訪問,需要在類定義中的屬性名前面增加"__",而這個方法的實質是在Python解釋器解釋類定義代碼過程中將其真正的屬性名視為"_類名__屬性名",而這一點對於使用者是不可見的,所以對於使用者無法真正修改和訪問這個屬性。
2、對應於此,為規範代碼,同時體現訪問限制,類定義中須配套定義get和set方法。
3、其他:如要進一步限制使用者對於類屬性的增加、屬性值的修改這些,可以使用__slots__方法。
___________________________________________________________________________________________
接上,按照上述的一個規範的訪問限制類定義的寫法(按最簡單的僅有一個屬性的情況):
class Student(object): #簡便起見,僅考慮一個屬性 def __init__(self,score): #這裏的__score解釋器轉換為_Student__score self.__score=score def get_score(self): #這裏的__score解釋器轉換為_Student__score return self.__score def set_score(self,score): self.__score=score
並且在使用過程中,須自覺嚴格使用這些set和get方法;那麽一個帶來的煩惱就是每次使用都需要寫實例名.set_屬性名()或get_屬性名(),相比直接通過‘實例名.屬性名‘要麻煩一些,由此,Python提供了一種便利的方法,也是本節的重點:@property。
通過@property和@屬性名.setter語句的添加可以實現直接使用‘實例名.屬性名‘來訪問或修改屬性值。
同時,需要註意的是,當通過@裝飾後,原先定義的set/get方法名同時需要修改為屬性名(只是set/get對應的參數不同,分別是self和self、參數。
class Student(object): def __init__(self,score): self.__score=score @property def score(self): return self.__score @score.setter def score(self,score): self.__score=score wanghai=Student(96) print(wanghai.score) wanghai.score=99 print(wanghai.score)
針對上例,可以擴展成一個更為健壯的代碼,在setter代碼段中增加判斷傳入參數的合理性的代碼:
class Student(object): def __init__(self,score): self.__score=score @property def score(self): return self.__score @score.setter def score(self,score): if not isinstance(score,int): raise ValueError(‘輸入參數非數值!‘) elif score>100 or score<0: raise ValueError(‘輸入數值參數應該在0~100範圍內!‘) else: self.__score=score wanghai=Student(96) print(wanghai.score) wanghai.score=‘a‘ wanghai.score=109
也可以設置類的只讀屬性,這種情況下,只需要對需要設為只讀的屬性增加@property,無需對應設置‘@屬性名.setter‘。
class Student(object): def __init__(self,name,score): self.__name=name self.__score=score #__name為只讀屬性 @property def name(self): return self.__name @property def score(self): return self.__score @score.setter def score(self,score): if not isinstance(score,int): raise ValueError(‘輸入參數非數值!‘) elif score>100 or score<0: raise ValueError(‘輸入數值參數應該在0~100範圍內!‘) else: self.__score=score wanghai=Student(‘WangHai‘,96) print(wanghai.name) print(wanghai.score) wanghai.score=100 print(wanghai.score) #報錯,name為只讀屬性 #wanghai.name=‘WH0‘ #下面代碼雖然執行成功,但只是給實例新增了一個‘__name‘屬性 wanghai.__name=‘WH1‘ #訪問類定義中的屬性,即_Student__name print(wanghai.name) print(wanghai._Student__name)
練習:
‘‘‘ 請利用@property給一個Screen對象加上width和height屬性 以及一個只讀屬性resolution ‘‘‘ #這題有些沒弄清的是屬性名前面必須加下劃線或者雙下劃線,否則就報錯 #那就當一個習慣保持雙下劃線命名吧 class Screen(object): @property def width(self): return self.__width @property def height(self): return self.__height @property def resolution(self): return self.__width*self.__height @width.setter def width(self,width): if not isinstance(width,int): raise ValueError(‘參數必須是整數值!‘) else: self.__width=width @height.setter def height(self,height): if not isinstance(height,int): raise ValueError(‘參數必須是整數值!‘) else: self.__height=height # 測試: s = Screen() s.width = 1024 s.height = 768 print(‘resolution =‘, s.resolution) if s.resolution == 786432: print(‘測試通過!‘) else: print(‘測試失敗!‘)
三、定制類
(一)__str__
在一個類的實例通過print()方法打印出來時總是類似於"<__main__.Student object at 0x109afb190>"的型式,不僅復雜,也不明確看出實例的具體信息。
那麽在類的定義中,可以增加__str__方法,返回這個類的實例信息:
class Student(): def __init__(self,name): self.name=name def __str__(self): return ‘Student object (name:%s).‘% self.name #輸出:Student object (name:Bob). print(Student(‘Bob‘))
需要註意的是,上述__str__能替代的情況是Student(‘參數‘)這種情況,如果對於"變量名=Student(‘參數‘)"時,在命令行格式下,直接敲‘變量名‘還是返回的是原始信息,這是因為這種情況下,使用的是__repr__()而非__str__()。
如果要設置__repr__,最簡單的方法如下:
class Student(): def __init__(self,name): self.name=name def __str__(self): return ‘Student object (name:%s).‘% self.name __repr__=__str__
(二)__iter__
如果一個類想通過for/in循環,類似於list或tuple,就必須實現一個__iter__()方法,這個方法返回一個叠代對象,然後Python的for循環就可以不斷調用這個叠代對象的__next__()方法獲取循環下一個值,直到遇到StopIteration錯誤。
class Fib(object): def __init__(self): self.a,self.b=0,1 def __iter__(self): return self def __next__(self): self.a,self.b=self.b,self.a+self.b if self.a<100: return self.a else: raise StopIteration() for i in Fib(): print(i)
(三)__getitem__
上面的Fib()類雖然可以通過for/in循環,但依然不能當成list使用,比如不能使用列表的下標訪問元素。如果要實現這個功能,需要給這個類添加__getitem__方法。
class Fib(object): def __getitem__(self,n): a,b=1,1 for x in range(n): a,b=b,a+b return a f=Fib() print(f[0]) print(f[3]) print(f[13]) print(f[33]) #可以看出__getitem__()方法定義後自動也給類添加了for循環功能 for i in Fib(): if i<1000: print(i) else: break
(四)__getattr__
正常情況下如果調用一個不存在的類的屬性或方法時就會報錯。
如果要處理這個報錯,那就需要寫一個__getattr__方法,當調用不存在的屬性時,Python解釋器就會嘗試去調用這個方法。
class Student(object): def __init__(self,name): self._name=name def __getattr__(self,attr): #可以根據情況設定有可能被使用者誤訪問的屬性 if attr==‘score‘: return 99 #對於其他情況,報錯 else: raise AttributeError(‘\‘Student\‘ object has no attribute \‘%s\‘‘ % attr) s=Student(‘Bob‘) #正常訪問,輸出Bob print(s._name) #通過getattr方法返回,輸出99 print(s.score) #通過getattr方法,拋出錯誤提示 print(s.country)
(五)__call__
通常在調用類的實例的方法的時候,需要以“實例名.方法名()”的形式,那麽能不能直接通過“實例名()”直接調用呢?在類定義中只需要加一個__call__()方法,就可以實現:
class Student(object): def __init__(self,name): self._name=name def __call__(self): print(‘instance\‘s name is %s‘ % self._name) def sp(self): print(‘Student\‘s another method for printing 12345!‘) Student(‘Bob‘)() Student(‘Stam‘)() Student(‘Steve‘).sp()
通過代碼我們可以發現,當定義了__call__()方法後,實例名()對應的就是call方法,而對於其他類中定義的方法還需要原始的寫法才可以使用。
在實例直接調用後,這裏實例對象就類似於函數,而實質上這兩者也沒有本質的區別。對於帶有__call__()定義的類的實例,它就是一個Callable對象(可調用對象),同樣函數也是一個Callable對象。
class Student0(object): def __init__(self,name): self._name=name def __call__(self): print(‘instance\‘s name is %s‘ % self._name) class Student1(object): def __init__(self,name): self._name=name s0=Student0(‘Bob‘) s1=Student1(‘Bob‘) #s0是Callable對象,輸出True print(callable(s0)) #s1不是Callable對象,輸出False print(callable(s1)) #max是函數,輸出True print(callable(max)) #輸出False print(callable(1))
四、使用枚舉類
枚舉類型可以看做是一系列常量的集合,在Python中可以通過字典、類去實現,在通過字典或者常規的類定義的話,無法避免對其的修改,因此在Python3.4後的版本提供了enum庫,可以定義枚舉類型的類(枚舉類)。
from enum import Enum #註意在定義枚舉類的時候基類參數需要設置為Enum #星期的枚舉類共有7個成員 #每個成員都有名字和值 #定義成員的時候,成員名字不能相同 #成員的值可以相同 #但相同的值的成員,後面的成員名被視為第一個對應該值的成員的別名 class Weekday(Enum): Monday=1 Mon=1 #Mon被視為Monday的成員 Tuesday=2 Wednsday=3 Thursday=4 Friday=5 Saturday=6 Sunday=7 #打印成員 print(Weekday.Mon) print(Weekday.Sunday) #遍歷成員 for x in Weekday: #獲取成員名 print(x.name) #獲取成員值 print(x.value) #獲取成員信息 print(x)
而如果對於具有同一值的多個成員名的情況,需要全部遍歷出來,則需要用到__members__屬性:
‘‘‘ 輸出: (‘Monday‘, <Weekday.Monday: 1>) (‘Mon‘, <Weekday.Monday: 1>) (‘Tuesday‘, <Weekday.Tuesday: 2>) (‘Wednsday‘, <Weekday.Wednsday: 3>) (‘Thursday‘, <Weekday.Thursday: 4>) (‘Friday‘, <Weekday.Friday: 5>) (‘Saturday‘, <Weekday.Saturday: 6>) (‘Sunday‘, <Weekday.Sunday: 7>) ‘‘‘ for x in Weekday.__members__.items(): print(x)
枚舉類也可以限制為不可以設定同一值的多個成員名,則需要import進unique模塊,並且在枚舉類定義前加上裝飾器@unique
from enum import Enum,unique @unique class Weekday(Enum): Monday=1 #Mon=1,這句代碼就不能設置了,會報錯 Tuesday=2 Wednsday=3 Thursday=4 Friday=5 Saturday=6 Sunday=7
在for循環遍歷中通過替代變量可以直接獲取各個成員,如果知道某個成員的名字或者值,那麽也可以獲取該成員。
from enum import Enum class Weekday(Enum): Monday=1 Mon=1 Tuesday=2 Wednsday=3 Thursday=4 Friday=5 Saturday=6 Sunday=7 #通過成員名獲取成員 print(Weekday[‘Tuesday‘]) print(Weekday[‘Monday‘]) print(Weekday[‘Mon‘]) #這裏也可以看出Mon是Monday的別名 #通過成員值獲取成員 print(Weekday(6))
練習:
‘‘‘ 把Student的gender屬性改造為枚舉類型,可以避免使用字符串 ‘‘‘ from enum import Enum,unique @unique class Gender(Enum): Male=0 Femal=1 class Student(object): def __init__(self,name,gender): self._name=name self._gender=gender # 測試: bart = Student(‘Bart‘, Gender.Male) if bart._gender == Gender.Male: print(‘測試通過!‘) else: print(‘測試失敗!‘)
Python學習筆記八 面向對象高級編程(一)