Python面向?qū)ο蟪绦蛟O計OOP深入分析【構造函數(shù),組合類,工具類等】
本文深入分析了Python面向?qū)ο蟪绦蛟O計OOP。分享給大家供大家參考,具體如下:
下面是一個關于OOP的實例,模塊文件為person.py
# File person.py(start) class Person: def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay def last_name(self): return self.name.split()[-1] def give_raise(self, percent): self.pay = int(self.pay * (1+percent)) print('total percent:%f' % percent) def __str__(self): return '[Person: %s, %s]' % (self.name, self.pay) class Manager(Person): # 這是一種不太好的方法重載的方法,實際應用中我們采用下面的方法 def give_raise(self, percent, bonus=.1): self.pay = int(self.pay * (1+percent+bonus)) # 這個方法利用了這樣的一個事實:類方法總是可以在一個實例中調(diào)用。 # 其實常規(guī)的實例調(diào)用,也是轉(zhuǎn)換為類的調(diào)用 # instance.method(args...) 由Python自動地轉(zhuǎn)換為 class.method(instance,args...) # 所以要記得直接通過類進行調(diào)用時,必須手動傳遞實例,這里就是self參數(shù) # 而且不能寫成self.give_raise,這樣會導致循環(huán)調(diào)用 # # 那么為什么采用這種形式呢?因為它對未來的代碼的維護意義重大,因為give_raise現(xiàn)在 # 只在一個地方,即Person的方法,將來需要修改的時候,我們只需要修改一個版本 def give_raise(self, percent, bonus=.1): Person.give_raise(self, percent+bonus) if __name__ == '__main__': # self-test code bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=100000) print(bob) print(sue) print(bob.last_name(), sue.last_name()) sue.give_raise(.1) print(sue) print('-'*20) tom = Manager('Tom Jones', 'mgr', 50000) tom.give_raise(.1) print(tom.last_name()) print(tom) print('--All three--') for obj in (bob, sue, tom): obj.give_raise(.1) print(obj)
這個示例定義了Person類,并且Person類的構造函數(shù)采用了默認關鍵字參數(shù),重載了__str__方法用以print輸出,定義了得到last_name的方法,定義了give_raise漲工資方法。類Manager繼承自Person,Manager重新定義了自己的give_raise方法,獲得額外的bonus=0.1的獎金。
在代碼最后,寫了自測試的代碼if __name__ == '__main__'
,用以測試。輸出如下:
[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
Smith Jones
total percent:0.100000
[Person: Sue Jones, 110000]
--------------------
total percent:0.200000
Jones
[Person: Tom Jones, 60000]
--All three--
total percent:0.100000
[Person: Bob Smith, 0]
total percent:0.100000
[Person: Sue Jones, 121000]
total percent:0.200000
[Person: Tom Jones, 72000]
這里也可以給Manager增加自己獨有的方法。
定制構造函數(shù)
現(xiàn)在的代碼可以正常工作,但是,如果仔細研究會發(fā)現(xiàn),當我們創(chuàng)建Manager對象的時候,必須為它提供一個mgr工作名稱似乎沒有意義:這已經(jīng)由類自身暗示了。
所以,為了改善這點,我們要重新定義Manager中的__init__
方法,從而提供mgr字符串,而且和give_raise的定制一樣,通過類名的調(diào)用來運行Person中最初的__init__
。
def __init__(self, name, pay): Person.__init__(self, name, 'mgr', pay)
那么之后的實例化就變成了:
tom = Manager('Tom Jones', 50000)
OOP比我們認為的簡單
這是Python的OOP機制中幾乎所有重要的概念:
- 實例創(chuàng)建——填充實例屬性
- 行為方法——在類方法中封裝邏輯
- 運算符重載——為打印這樣的內(nèi)置操作提供行為
- 定制行為——重新定義子類中的方法以使其特殊化
- 定制構造函數(shù)——為超類步驟添加初始化邏輯。
組合類的其他方式
有時候,我們可以以其他的方式組合類。例如,一種常用的編碼模式是把對象彼此嵌套以組成復合對象,而不是繼承。如下的替代方法使用__getattr__
運算符重載方法來攔截未定義屬性的訪問。這時候,我們的代碼如下:
class Person: ...same... class Manager(): def __init__(self, name, pay): self.person = Person(name, 'mgr', pay) def give_raise(self, percent, bonus=.1): self.person.give_raise(percent+bonus) def __getattr__(self, attr): return getattr(self.person, attr) def __str__(self): return str(self.person) if __name__ == '__main__': ...same...
實際上,這個Manager替代方案是一種叫做委托的常用代碼模式的一個代表,委托是一種基于符合的結(jié)構,它管理一個包裝的對象并且把方法調(diào)用傳遞給它。
這里Manager不是一個真正的Person,因此,我們必須增加額外的代碼為嵌入的對象分派方法,比如像__str__
這樣的運算符重載方法必須重新定義。所以它需要的代碼量增加,對于這個例子來說,沒有哪個明智的Python程序員會按照這種方式組織代碼,但是,當嵌入的對象比直接定制隱藏需要與容器之間有更多有限的交互時,對象嵌入以及基于其上的設計模式還是很適合的。
下述代碼假設Department可能聚合其他對象,以便將它們當做一個集合對待。
class Department: def __init__(self, *args): self.members = list(args) def add_member(self, person): self.members.append(person) def give_raise(self, percent): for person in self.members: person.give_raise(percent) def show_all(self): for person in self.members: print(person) development = Department(bob,sue) development.add_member(tom) development.give_raise(.1) development.show_all()
這里的代碼使用了繼承和復合——Department是嵌入并控制其他對象的聚合的一個復合體,但是,嵌入的Person和Manager對象自身使用繼承來定制。作為另一個例子,一個GUI可能類似地使用繼承來定制標簽和按鈕的行為或外觀,但也會復合以構建嵌入的掛件(如輸入表單、計算器和文本編輯器)的一個更大的包。
使用內(nèi)省類工具
我們定制構造函數(shù)之后的Manager類還有幾點小問題如下:
- 打印的時候,Manager會把他標記為Person。如果能夠用最確切(也就是說最低層)的類來顯示對象,這可能會更準確些。
- 其次,當前的顯示格式只是顯示了包含在
__str__
中的屬性,而沒有考慮未來的目標。例如,我們無法通過Manager的構造函數(shù)驗證tom工作名已經(jīng)正確地設置為mgr,因為我們?yōu)镻erson編寫的__str__
沒有打印出這一字段。更糟糕的是,如果我們改變了在__init__
中分配給對象的屬性集合,那么還必須記得也要更新__str__
以顯示新的名字,否則,將無法隨著時間的推移而同步。
我們可以使用Python的內(nèi)省工具來解決這兩個問題,它們是特殊的屬性和函數(shù),允許我們訪問對象實現(xiàn)的一些內(nèi)部機制。例如,在我們的代碼中,有兩個鉤子可以幫助我們解決問題:
- 內(nèi)置的
instance.__class__
屬性提供了一個從實例到創(chuàng)建它的類的鏈接。類反過來有一個__name__
,還有一個__bases__
序列,提供了超類的訪問。我們使用這些來打印創(chuàng)建的一個實例的類的名字,而不是通過硬編碼來做到。 - 內(nèi)置的
object.__dict__
屬性提供了一個字典,帶有一個鍵/值對,以便每個屬性都附加到一個命名空間對象(包括模塊、類和實例)。由于它是字典,因此我們可以獲取鍵的列表、按照鍵來索引、迭代其值等等。我們使用這些來打印出任何實例的每個屬性,而不是在定制顯示中硬編碼。
下面是這些工具在交互模式下的實際使用情形:
>>> from person import Person >>> bob = Person('Bob Smith') >>> print(bob) [Person: Bob Smith, 0] >>> bob.__class__ <class 'person.Person'> >>> bob.__class__.__name__ 'Person' >>> list(bob.__dict__.keys()) ['name', 'pay', 'job'] >>> for key in bob.__dict__: ... print(key,'=>',bob.__dict__[key]) ... name => Bob Smith pay => 0 job => None >>> for key in bob.__dict__: ... print(key,'=>',getattr(bob,key)) ... name => Bob Smith pay => 0 job => None
一種通用的顯示工具
新打開一個文件,并編寫如下代碼:它是一個新的、獨立的模塊,名為classtools.py,僅僅實現(xiàn)了這樣一個類。由于其__str__
,print重載用于通用的內(nèi)省工具,它將會對任何實例有效,不管實例的屬性集合是什么。并且由于這是一個類,所以它自動變成一個公用的工具:得益于繼承,他可以混合到想要使用它顯示格式的任何類中。作為額外的好處,如果我們想要改變實例的顯示,只需要修改這個類即可。
# File classtools.py """Assorted class utilities and tools""" class AttrDisplay: """ Provides an inheritable print overload method that displays instances with their class names and a name-value pair for each attribute stored on the instance itself(but not attrs inherited from its classes).Can be mixed into any class, and will work on any instance. """ def gatherAttrs(self): attrs = [] for key in sorted(self.__dict__): attrs.append('%s = %s' % (key,getattr(self,key))) return ','.join(attrs) def __str__(self): return '[%s:%s]' % (self.__class__.__name__, self.gatherAttrs()) if __name__ == '__main__': class TopTest(AttrDisplay): count = 0 def __init__(self): self.attr1 = TopTest.count self.attr2 = TopTest.count + 1 TopTest.count += 2 class SubTest(TopTest): pass x, y = TopTest(), SubTest() print(x) print(y)
注意這里的文檔字符串,作為通用的工具,我們需要添加一些功能來產(chǎn)生文檔。
這里定義的__str__
顯示了實例的類,及其所有的屬性名和值,按照屬性名排序。
[TopTest:attr1 = 0,attr2 = 1]
[SubTest:attr1 = 2,attr2 = 3]
工具類的命名考慮
最后一點需要考慮的是,由于classtools模塊中的AttrDisplayz類旨在和其他任意類混合的通用性工具,所以我們必須注意與客戶類潛在的無意的命名沖突。如果一個子類無意地自己定義了一個gatherAttrs名稱,它很可能會破壞我們的類。
為了減少這樣的名稱沖突的機會,Python程序員常常對于不想做其他用途的方法添加一個【單個下劃線】的前綴,在我們這個例子中就是_gatherAttrs。這不是很可靠,如果一個子類也定義了_gatherAttrs,該如何是好?但是它通常已經(jīng)夠用了,并且對于類內(nèi)部的方法,這是常用的Python命名慣例。
一種更好但是不太常用的方法是,只在方法名前面使用【兩個下劃線】符號,__gatherAttrs,Python自動擴展這樣的名稱,以包含類的名稱,從而使它們變得真正唯一。這一功能通常叫做【偽私有類屬性】,將在以后介紹。
首先要使用打印這一通用工具,所需要做的是從其模塊中導入它,使用繼承將其混合到頂層類中,并且刪除我們之前編寫的更專門的__str__
方法。新的打印重載方法將會由Person的實例繼承,Manager的實例也會繼承。
下面就是類的最終形式:
# File person.py(start) from classtools import AttrDisplay class Person(AttrDisplay): """ Create and process person records """ def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay def last_name(self): return self.name.split()[-1] def give_raise(self, percent): self.pay = int(self.pay * (1+percent)) print('total percent:%f' % percent) class Manager(Person): """ A customized Person with special requirements """ def __init__(self, name, pay): Person.__init__(self, name, 'mgr', pay) def give_raise(self, percent, bonus=.1): Person.give_raise(self, percent+bonus) if __name__ == '__main__': # self-test code bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=100000) print(bob) print(sue) print(bob.last_name(), sue.last_name()) sue.give_raise(.1) print(sue) print('-'*20) tom = Manager('Tom Jones', 50000) tom.give_raise(.1) print(tom.last_name()) print(tom)
在這個版本中,也添加了一些新的注釋來記錄所做的工作和每個最佳實踐慣例——使用了功能性描述的文檔字符串和用于簡短注釋的#?,F(xiàn)在運行這段代碼,將會看到對象的所有屬性,并且最終的問題也解決了:由于AttrDisplay直接從self實例中提取了類名,所有每個對象都顯示其最近的(最低的)類的名稱——tom現(xiàn)在顯示為Manager,而不是Person。
[Person:job = None,name = Bob Smith,pay = 0]
[Person:job = dev,name = Sue Jones,pay = 100000]
Smith Jones
total percent:0.100000
[Person:job = dev,name = Sue Jones,pay = 110000]
--------------------
total percent:0.200000
Jones
[Manager:job = mgr,name = Tom Jones,pay = 60000]
這正是我們所追求的更有用的顯示,我們屬性顯示類已經(jīng)變成了一個【通用工具】,可以通過繼承將其混合到任何類中,從而利用它所定義的顯示格式。
最后:把對象存儲到數(shù)據(jù)庫中
我們創(chuàng)建的對象還不是真正的數(shù)據(jù)庫記錄,他們只是內(nèi)存中的臨時對象,而沒有存儲到文件這樣更為持久的媒介中。所以,現(xiàn)在要使用Python的一項叫做【對象持久化】的功能把對象保存。
Pickle和Shelve
對象持久化通過3個標準的庫模塊來實現(xiàn),這3個模塊在Python中都可用:
- pickle:任意的Python對象和字節(jié)串之間的序列化
- dbm:實現(xiàn)一個可通過鍵訪問的文件系統(tǒng),以存儲字符串
- shelve:使用另兩個模塊按照把Python對象存儲在一個文件中。
在Shelve數(shù)據(jù)庫中存儲對象
讓我們編寫一個新的腳本,把類的對象存儲到shelve中。在文本編輯器中,打開一個名為makedb.py的新文件,導入shelve模塊,用一個外部文件名打開一個新的shelve,把對象賦給shelve中的鍵,當我們操作完畢之后關閉這個shelve,因為已經(jīng)做了修改:
# File makedb.py:store Person objects on a shelve database from person import Person, Manager import shelve bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay='100000') tom = Manager('Tom Jones', 50000) db = shelve.open('persondb') for obj in (bob, sue, tom): db[obj.name] = obj db.close()
注意,這里我們把對象的名字用作鍵,從而把他們賦給shelve,這么做只是為了方便,在shelve中,鍵可以是任何的字符串,唯一的規(guī)則是,鍵必須是字符串并且是唯一的。這樣,我們就可以針對每個鍵只存儲一個對象。然而,我們在鍵之下的值可以是幾乎任何類型的Python對象:像字符串、列表和字典這樣的內(nèi)置對象,用戶定義的類實例,以及所有這些嵌套式的組合。
運行這段代碼,沒有輸出,意味著他可能有效。
交互式探索shelve
此時,當前的目錄下會有一個或多個真實的文件,它們的名字都以‘persondb'開頭。這就是我們存儲的文件,也就是我們的數(shù)據(jù)庫,是我們備份或移動存儲的時候需要復制和轉(zhuǎn)移的內(nèi)容。
在交互式命令窗口中可以查看這些文件:
>>> import glob >>> glob.glob('person*') ['person.py', 'person2.py', 'persondb.bak', 'persondb.dat', 'persondb.dir'] >>> print(open('persondb.dir').read()) 'Tom Jones', (1024, 91) 'Sue Jones', (512, 100) 'Bob Smith', (0, 80) >>> print(open('persondb.dat','rb').read()) b'\x80\x03cperson\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00jobq\x03NX\x03\x00\x00\x00payq\x04K\x00X\x04\x00\x00\x00nameq\x05X\t\x00\x00\x00Bob ...more omitted...
這些內(nèi)容無法解讀,但是我們可以用常規(guī)的Python語法和開發(fā)模式來處理它,即通過shelve來打開這些文件。
>>> import shelve >>> db = shelve.open('persondb') >>> for key in db: ... print(key, '=>', db[key]) ... Bob Smith => [Person:job = None,name = Bob Smith,pay = 0] Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000] Sue Jones => [Person:job = dev,name = Sue Jones,pay = 100000] >>> len(db) 3 >>> bob = db['Bob Smith'] >>> bob.last_name() 'Smith'
在這里,為了載入或使用存儲的對象,我們不一定必須導入Person或Manager類。因為Python對一個類實例進行pickle操作,它記錄了其self實例屬性,以及實例所創(chuàng)建于的類的名字和類的位置。
這種方法的結(jié)果就是,類實例在未來導入的時候,會自動地獲取其所有的類行為。
更新Shelve中的對象
現(xiàn)在介紹最后一段腳本,編寫一個程序,在每次運行的時候更新一個實例,以此證實我們的對象真的是持久化的。如下的updatadb.py打印出數(shù)據(jù)庫,并且每次把我們所存儲的對象之一增加一次,跟蹤它的變化:
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 110000]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 121000]
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 133100]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
未來方向
通過這個例子,我們可以看到了Python的OOP的所有基本機制的實際運作,并且,學習了在代碼中避免冗余性及其相關可維護性問題的方法,還構建了功能完備的類來完成實際的工作。此外,我們還通過把對象存儲到Python的shelve中創(chuàng)建了正式的數(shù)據(jù)庫記錄,從而使它們的信息持久地存在。
這之后還有更多的內(nèi)容可以探討,比如擴展使用工具的范圍,包括Python附帶的工具以及開源世界中可以免費獲取的工具:
GUI:添加圖形化的用戶界面來瀏覽和更新數(shù)據(jù)庫記錄??梢詷嫿軌蛞浦驳絇ython的tkinter的GUI,或者可以移植到WxPython和PyQt這樣的第三方工具的GUI。tkinter是Python自帶的,允許我們快速地構建簡單的GUI,并且是學習GUI編程技巧的理想工具。
Web站點:盡管GUI方便而且很快,但Web在易用性方面勝出。Web站點可以用Python自帶的基本CGI腳本編程工具來構建,也可以用像Django、TurboGears、Pylons、web2Py、Zope或Google's App Engine這樣的全功能的第三方Web開發(fā)框架來完成。
數(shù)據(jù)庫:如果數(shù)據(jù)庫變得更大或者更關鍵,我們可以將其從shelve轉(zhuǎn)移到像開源的ZODB面向?qū)ο髷?shù)據(jù)庫系統(tǒng)(OODB)這樣一個功能更完備的存儲機制中,或者像MySQL、Oracle、PostgreSQL或SQLLite這樣的一個更傳統(tǒng)的基于SQL的數(shù)據(jù)庫系統(tǒng)中。Python自身帶有一個內(nèi)置的、正在使用的SQLLite數(shù)據(jù)庫。
ORM:如果我們真的遷移到關系數(shù)據(jù)庫中進行存儲,不一定要犧牲Python的OOP工具??梢杂肧QLObject和SQLAlchemy這樣的對象關系映射器(ORM)。
更多關于Python相關內(nèi)容感興趣的讀者可查看本站專題:《Python面向?qū)ο蟪绦蛟O計入門與進階教程》、《Python數(shù)據(jù)結(jié)構與算法教程》、《Python函數(shù)使用技巧總結(jié)》、《Python字符串操作技巧匯總》、《Python編碼操作技巧總結(jié)》及《Python入門與進階經(jīng)典教程》
希望本文所述對大家Python程序設計有所幫助。
相關文章
使用Python3中的gettext模塊翻譯Python源碼以支持多語言
這篇文章主要介紹了使用Python3中的gettext模塊翻譯Python源碼以支持多語言,其中翻譯Python源碼只是作為示例以展示gettext的功能和用法,需要的朋友可以參考下2015-03-03如何利用Python和matplotlib更改縱橫坐標刻度顏色
對于圖表來說最簡單的莫過于作出一個單一函數(shù)的圖像,下面這篇文章主要給大家介紹了關于如何利用Python和matplotlib更改縱橫坐標刻度顏色的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-08-08python關于os.walk函數(shù)查找windows文件方式
這篇文章主要介紹了python關于os.walk函數(shù)查找windows文件方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08Python辦公自動化之數(shù)據(jù)預處理和數(shù)據(jù)校驗詳解
這篇文章主要為大家詳細介紹了Python辦公自動化中數(shù)據(jù)預處理和數(shù)據(jù)校驗的相關知識,文中的示例代碼講解詳細,感興趣的小伙伴可以參考一下2024-01-01利用python在excel里面直接使用sql函數(shù)的方法
今天小編就為大家分享一篇利用python在excel里面直接使用sql函數(shù)的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-02-02三個Python常用的數(shù)據(jù)清洗處理方式總結(jié)
這篇文章主要為大家詳細介紹了python數(shù)據(jù)處理過程中三個主要的數(shù)據(jù)清洗說明,分別是缺失值/空格/重復值的數(shù)據(jù)清洗,感興趣的小伙伴可以了解一下2022-12-12python實現(xiàn)的登錄和操作開心網(wǎng)腳本分享
這篇文章主要介紹了python實現(xiàn)的登錄和操作開心網(wǎng)腳本分享,可以登錄開心網(wǎng),登錄后發(fā)送信息等功能,需要的朋友可以參考下2014-07-07