新聞中心
本文轉(zhuǎn)載自公眾號(hào)“讀芯術(shù)”(ID:AI_Discovery)。

作為一種OOP語(yǔ)言,Python通過(guò)支持以對(duì)象為主的各種功能來(lái)處理數(shù)據(jù)和功能。例如,數(shù)據(jù)結(jié)構(gòu)是所有對(duì)象,包括原始類型(例如整數(shù)和字符串),而在某些其他語(yǔ)言中,原始類型則不視為對(duì)象。對(duì)于另一個(gè)實(shí)例,函數(shù)是所有對(duì)象,它們僅僅是定義了其他對(duì)象的屬性(例如類或模塊)。
盡管可以使用內(nèi)置數(shù)據(jù)類型,而且無(wú)需創(chuàng)建任何自定義類就能編寫一組函數(shù),但隨著項(xiàng)目范圍的擴(kuò)大,代碼可能會(huì)越來(lái)越難維護(hù)。這些單獨(dú)代碼部分的主題并不相同,盡管有很多信息是相關(guān)的,但管理它們之間的聯(lián)系卻并不簡(jiǎn)單。
在這些情況下,定義自己的類就很劃得來(lái)了,這樣一來(lái)你可以對(duì)相關(guān)信息進(jìn)行分組并且改善項(xiàng)目的結(jié)構(gòu)設(shè)計(jì)。而且由于你即將處理更少的分段代碼,代碼庫(kù)的長(zhǎng)期可維護(hù)性將得到改善。但要注意,僅當(dāng)以正確方式完成類聲明時(shí),操作才可以實(shí)現(xiàn),定義自定義類的益處才能超過(guò)管理它們的支出。
[[342612]]
1. 好的命名
定義自己的類,就好比在代碼庫(kù)中添加了一位新成員。因此應(yīng)該給類起個(gè)好名字。雖然類名的唯一限制是合法Python變量的規(guī)則(例如,不能以數(shù)字開頭),但是有一些好用的方法來(lái)命名類。
- 使用易于發(fā)音的名詞。在參與團(tuán)隊(duì)項(xiàng)目時(shí),這一點(diǎn)尤其重要。在小組演講中,你恐怕不愿意這樣講:“在這種情況下,我們創(chuàng)建Zgnehst類的實(shí)例?!?另外,易于發(fā)音也意味著名稱不應(yīng)太長(zhǎng),使用三個(gè)以上的單詞來(lái)定義類名簡(jiǎn)直無(wú)法想象。一個(gè)字是最佳,兩個(gè)字其次,三個(gè)字不能再多啦!
- 反映其存儲(chǔ)的數(shù)據(jù)和預(yù)期功能。就像在現(xiàn)實(shí)生活中一樣——當(dāng)看到男性化的名字時(shí),我們就會(huì)默認(rèn)這個(gè)孩子是男孩。同樣的方式也適用于類名(或通常的任何其他變量),命名規(guī)則很簡(jiǎn)單——不要讓人感覺(jué)奇怪。如果要處理學(xué)生的信息,那么該課程應(yīng)該命名為Student,KiddosAtCampus并不是一個(gè)常規(guī)的好名字。
- 遵循命名約定。應(yīng)該對(duì)類名使用駱駝拼寫法,例如GoodName。以下是非常規(guī)類名稱的不完整列表:goodName,Good_Name,good_name以及GOodnAme。遵循命名約定是為了使意圖表現(xiàn)明確。在別人閱讀你的代碼時(shí),可以毫無(wú)疑問(wèn)地假定命名為GoodName的對(duì)象是一個(gè)類。
也有適用于屬性和功能的命名規(guī)則和約定,以下各節(jié)將在使用情況下簡(jiǎn)要提及,但是總體原理是相同的。
2. 顯式實(shí)例屬性
在大多數(shù)情況下,我們都想定義自己的實(shí)例初始化方法(即__init__)。在此種方法中,設(shè)置了新創(chuàng)建的類實(shí)例的初始狀態(tài)。但是,Python并沒(méi)有限制可以在何處使用自定義類定義實(shí)例屬性。換句話說(shuō),你可以在創(chuàng)建實(shí)例之后的后續(xù)操作中定義其他實(shí)例屬性。
- classStudent:
- def__init__(self, first_name, last_name):
- self.first_name = first_name
- self.last_name = last_name
- defverify_registration_status(self):
- status = self.get_status()
- self.status_verified = status =="registered"
- defget_guardian_name(self):
- self.guardian ="Goodman"
- defget_status(self):
- # get the registration status from a database
- status =query_database(self.first_name, self.last_name)
- return status
(1) 初始化方法
如上所示,可以通過(guò)指定學(xué)生的名字和姓氏來(lái)創(chuàng)建“學(xué)生”類的實(shí)例。稍后,在調(diào)用實(shí)例方法(即verify_registration_status)時(shí),將設(shè)置“學(xué)生實(shí)例”的status屬性。
但這不是理想的模式,因?yàn)槿绻谡麄€(gè)類中散布了各種實(shí)例屬性,那么該類就無(wú)法明確實(shí)例對(duì)象擁有哪些數(shù)據(jù)。因此,最佳做法是將實(shí)例的屬性放在__init__方法中,這樣代碼閱讀器就可以通過(guò)單一位置來(lái)了解你的類的數(shù)據(jù)結(jié)構(gòu),如下所示:
- classStudent:
- def__init__(self, first_name, last_name):
- self.first_name = first_name
- self.last_name = last_name
- self.status_verified =None
- self.guardian =None
(2) 更好的初始化方法
對(duì)于最初無(wú)法設(shè)置的那些實(shí)例屬性的問(wèn)題,可以使用占位符值(例如None)進(jìn)行設(shè)置。盡管沒(méi)什么好擔(dān)心的,但是當(dāng)忘記調(diào)用某些實(shí)例方法來(lái)設(shè)置適用的實(shí)例屬性時(shí),此更改還有助于防止可能的錯(cuò)誤,從而導(dǎo)致AttributeError(‘Student’ object has noattribute ‘status_verified’)。
在命名規(guī)則方面,應(yīng)使用小寫字母命名屬性,并遵循蛇形命名法——如果使用多個(gè)單詞,請(qǐng)?jiān)谒鼈冎g使用下劃線連接。此外,所有名稱都應(yīng)對(duì)其存儲(chǔ)的數(shù)據(jù)有具有意義的指示(例如first_name比f(wàn)n更好)。
3. 使用屬性——但要精簡(jiǎn)
[[342613]]
圖源:unsplash
有些人在具備其他OOP語(yǔ)言(例如Java)背景的情況下學(xué)習(xí)Python編碼,并且習(xí)慣于為實(shí)例的屬性創(chuàng)建getter和setter??梢酝ㄟ^(guò)在Python中使用屬性裝飾器來(lái)模仿這一模式。以下代碼展示了使用屬性裝飾器實(shí)現(xiàn)getter和setter的基本形式:
- classStudent:
- def__init__(self, first_name, last_name):
- self.first_name = first_name
- self.last_name = last_name
- @property defname(self):
- print("Getter for the name")
- returnf"{self.first_name}{self.last_name}"
- @name.setter defname(self, name):
- print("Setter for the name")
- self.first_name, self.last_name = name.split()
(3) 屬性裝飾
創(chuàng)建此屬性后,盡管它是通過(guò)內(nèi)部函數(shù)實(shí)現(xiàn)的,我們?nèi)匀豢梢允褂命c(diǎn)符號(hào)將其用作常規(guī)屬性。
- >>> student =Student("John", "Smith")
- ... print("StudentName:", student.name)
- ... student.name ="JohnnySmith"
- ... print("Aftersetting:", student.name)
- ... Getterfor the name StudentName: JohnSmith Setterfor the name Getterfor the name
(4) 使用屬性
使用屬性實(shí)現(xiàn)的優(yōu)點(diǎn)包括驗(yàn)證正確的值設(shè)置(檢查是否使用字符串,而不是使用整數(shù))和只讀訪問(wèn)權(quán)限(通過(guò)不實(shí)現(xiàn)setter方法)。但應(yīng)該同時(shí)使用屬性,如果自定義類如下所示,可能會(huì)很讓人分心——屬性太多了!
- classStudent:
- def__init__(self, first_name, last_name):
- self._first_name = first_name
- self._last_name = last_name
- @property
- deffirst_name(self):
- return self._first_name
- @property
- deflast_name(self):
- return self._last_name
- @property
- defname(self):
- returnf"{self._first_name}{self._last_name}"
(5) 濫用屬性
在大多數(shù)情況下,這些屬性可以用實(shí)例屬性代替,因此我們可以訪問(wèn)它們并直接設(shè)置它們。除非對(duì)使用上述屬性的好處有特定的需求(例如:值驗(yàn)證),否則使用屬性優(yōu)先于在Python中創(chuàng)建屬性。
4. 定義有意義的字符串表示法
在Python中,名稱前后帶有雙下劃線的函數(shù)稱為特殊方法或魔術(shù)方法,有些人將其稱為dunder方法。這些方法對(duì)解釋器的基本操作有特殊的用法,包括我們先前介紹的__init__方法。__repr__和__str__這兩種特殊方法對(duì)于創(chuàng)建自定義類的正確字符串表示法至關(guān)重要,這將為代碼閱讀器提供有關(guān)類的更直觀信息。
它們之間的主要區(qū)別在于__repr__方法定義了字符串,你可以使用該字符串通過(guò)調(diào)用eval(repr(“therepr”))重新創(chuàng)建對(duì)象,而__str__方法定義的字符串則更具描述性,并允許更多定制。換句話說(shuō),你可以認(rèn)為__repr__方法中定義的字符串由開發(fā)人員查看,而__str__方法中使用的字符串由常規(guī)用戶查看。請(qǐng)看以下示例:
- classStudent:
- def__init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name def__repr__(self): returnf"Student({self.first_name!r}, {self.last_name!r})"
- def__str__(self): returnf"Student: {self.first_name}{self.last_name}"
字符串表示法的實(shí)現(xiàn):
請(qǐng)注意,在__repr__方法的實(shí)現(xiàn)中,f字符串使用!r來(lái)顯示帶引號(hào)的這些字符串,因?yàn)槭褂酶袷秸_的字符串構(gòu)造實(shí)例很有必要。如果不使用!r格式,則字符串將為Student(John, Smith),這不是構(gòu)造“學(xué)生”實(shí)例的正確方法。
來(lái)看看這些實(shí)現(xiàn)如何為我們顯示字符串:在交互式解釋器中訪問(wèn)對(duì)象時(shí)會(huì)調(diào)用__repr__方法,而在打印對(duì)象時(shí)默認(rèn)會(huì)調(diào)用__str__方法。
- >>> student =Student("David", "Johnson")
- >>> student Student('David', 'Johnson')
- >>>print(student) Student: DavidJohnson
字符串表示法
5. 實(shí)例方法,類方法和靜態(tài)方法
在一個(gè)類中,我們可以定義三種方法:實(shí)例方法、類方法和靜態(tài)方法。我們需要考慮針對(duì)所關(guān)注的功能應(yīng)使用哪些方法,以下是一些常規(guī)準(zhǔn)則。
[[342614]]
圖源:unsplash
例如,如果方法與單個(gè)實(shí)例對(duì)象有關(guān),那么需要訪問(wèn)或更新實(shí)例的特定屬性。在這種情況下,應(yīng)使用實(shí)例方法。這些方法具有如下簽名:def do_something(self):,其中self自變量引用調(diào)用該方法的實(shí)例對(duì)象。
如果方法與單個(gè)實(shí)例對(duì)象無(wú)關(guān),則應(yīng)考慮使用類方法或靜態(tài)方法??梢允褂眠m用的修飾符輕松定義這兩種方法:類方法(classmethod)和靜態(tài)方法(staticmethod)。
兩者之間的區(qū)別在于,類方法允許你訪問(wèn)或更新與類相關(guān)的屬性,而靜態(tài)方法則獨(dú)立于任何實(shí)例或類本身。類方法的一個(gè)常見示例是提供一種方便的實(shí)例化方法,而靜態(tài)方法可以只是一個(gè)實(shí)用函數(shù)。請(qǐng)看以下代碼示例:
- classStudent:
- def__init__(self,first_name, last_name):
- self.first_name = first_name
- self.last_name = last_name
- defbegin_study(self):
- print(f"{self.first_name}{self.last_name} beginsstudying.")
- @classmethod deffrom_dict(cls,name_info): first_name = name_info['first_name']
- last_name = name_info['last_name']
- returncls(first_name,last_name) @staticmethod defshow_duties(): return"Study,Play, Sleep"
不同的方法
也可以用類似的方式創(chuàng)建類屬性。與前面討論的實(shí)例屬性不同,類屬性由所有實(shí)例對(duì)象共享,并且它們應(yīng)當(dāng)反映一些獨(dú)立于各個(gè)實(shí)例對(duì)象的特征。
6. 使用私有屬性進(jìn)行封裝
在為項(xiàng)目編寫自定義類時(shí),需要考慮封裝問(wèn)題,尤其期望其他人也使用你的類的話就更應(yīng)如此。當(dāng)類的功能增長(zhǎng)時(shí),某些功能或?qū)傩詢H與類內(nèi)數(shù)據(jù)處理相關(guān)。換句話說(shuō),除了類之外,這些函數(shù)都將不會(huì)被調(diào)用,并且除你之外其他使用類的用戶甚至不會(huì)在意這些函數(shù)的實(shí)現(xiàn)細(xì)節(jié)。在這些情況下,應(yīng)該考慮封裝。
按照慣例,應(yīng)用封裝的一種重要方法是為屬性和函數(shù)加上下劃線或兩個(gè)下劃線。二者之間有著細(xì)微的區(qū)別:帶有下劃線的被認(rèn)為是受保護(hù)的,而帶有兩個(gè)下劃線的被認(rèn)為是私有的,這涉及在創(chuàng)建后進(jìn)行名稱處理。
從本質(zhì)上來(lái)說(shuō),像這樣命名屬性和功能,是在告訴IDE(即集成開發(fā)環(huán)境,例如PyCharm),盡管在Python中不存在真正的私有屬性,但它們不會(huì)在類之外被訪問(wèn)。
- classStudent:
- def__init__(self,first_name, last_name):
- self.first_name = first_name
- self.last_name = last_name
- defbegin_study(self):
- print(f"{self.first_name}{self.last_name} beginsstudying.")
- @classmethod deffrom_dict(cls,name_info): first_name = name_info['first_name']
- last_name = name_info['last_name']
- returncls(first_name,last_name) @staticmethod defshow_duties(): return"Study,Play, Sleep"
封裝
上面的代碼展示了一個(gè)簡(jiǎn)單的封裝示例。如果想了解學(xué)生的評(píng)價(jià)GPA,那么我們可以使用get_mean_gpa方法獲得GPA。用戶不需要知道平均GPA的計(jì)算方式,我們可以通過(guò)在函數(shù)名稱前添加下劃線來(lái)保護(hù)相關(guān)方法。
這一最佳做法的主要收獲是,與用戶使用你的代碼相關(guān)的公共API,僅公開最少的數(shù)量。對(duì)于僅在內(nèi)部使用的那些代碼,請(qǐng)將其設(shè)置為受保護(hù)的方法或私有方法。
[[342615]]
圖源:unsplash
7. 分離關(guān)注點(diǎn)和解耦
隨著項(xiàng)目的發(fā)展,你會(huì)發(fā)現(xiàn)自己正在處理更多數(shù)據(jù),如果你只堅(jiān)持使用一個(gè)類會(huì)變得很麻煩。繼續(xù)以“學(xué)生”類為例,假設(shè)學(xué)生在學(xué)校吃午餐,并且每個(gè)人都有一個(gè)餐飲帳戶,可以用來(lái)支付餐費(fèi)。從理論上講,我們可以處理學(xué)生類中與帳戶相關(guān)的數(shù)據(jù)和功能,如下所示:
- classStudent:
- def__init__(self, first_name, last_name, student_id):
- self.first_name = first_name
- self.last_name = last_name
- self.student_id = student_id
- defcheck_account_balance(self):
- account_number =get_account_number(self.student_id)
- balance =get_balance(account_number) return balance
- defload_money(self, amount):
- account_number =get_account_number(self.student_id)
- balance =get_balance(account_number) balance += amount update_balance(account_number, balance)
混合功能
上面的代碼向展示了一些有關(guān)檢查賬戶余額和向賬戶充值的偽代碼,這兩種偽代碼都在Student類中實(shí)現(xiàn)。還有更多與該帳戶相關(guān)的操作,例如凍結(jié)丟失的卡、合并帳戶——實(shí)施所有這些操作會(huì)使“學(xué)生”類越來(lái)越大,從而使維護(hù)變得越來(lái)越困難。你應(yīng)該分離這些職責(zé)并使學(xué)生類不負(fù)責(zé)這些與帳戶相關(guān)的功能,即一種稱為解耦的設(shè)計(jì)模式。
- classStudent:
- def__init__(self, first_name, last_name, student_id):
- self.first_name = first_name
- self.last_name = last_name
- self.student_id = student_id
- self.account =Account(self.student_id)
- defcheck_account_balance(self):
- return self.account.get_balance()
- defload_money(self, amount):
- self.account.load_money(amount)
- classAccount: def__init__(self, student_id):
- self.student_id = student_id
- # get additional information from the database
- self.balance =400
- defget_balance(self):
- # Theoretically, student.account.balance will work, but just in case
- # we need to have additional steps to check, such as query the database
- # again to make sure the data is up to date
- return self.balance
- defload_money(self, amount):
- # get the balance from the database
- self.balance += amount
- self.save_to_database()
分離關(guān)注點(diǎn)
上面的代碼展示了我們?nèi)绾问褂酶郊拥腁ccount類來(lái)設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)。如你所見,我們將所有與帳戶相關(guān)的操作移至Account類。要實(shí)現(xiàn)檢索學(xué)生的帳戶信息的功能,學(xué)生類將通過(guò)從Account類中檢索信息來(lái)處理。如果想實(shí)現(xiàn)更多與該類相關(guān)的功能,只需簡(jiǎn)單地更新Account類即可。
設(shè)計(jì)模式的主要要點(diǎn)是,希望各個(gè)類具有單獨(dú)的關(guān)注點(diǎn)。通過(guò)將這些職責(zé)分開,你的類將變小,處理較小的代碼組件會(huì)使將來(lái)的更改變得更容易。
8. 考慮使用__slots__進(jìn)行優(yōu)化
如果你的類主要用于存儲(chǔ)數(shù)據(jù)的數(shù)據(jù)容器,那么可以考慮使用__slots__來(lái)優(yōu)化類的性能。它不僅可以提高屬性訪問(wèn)的速度,還可以節(jié)省內(nèi)存,如果需要?jiǎng)?chuàng)建數(shù)千個(gè)或更多實(shí)例對(duì)象,就是它發(fā)揮大作用之處啦。
原因是,對(duì)于常規(guī)類,實(shí)例屬性是通過(guò)內(nèi)部托管的字典存儲(chǔ)的。相比之下,通過(guò)使用__slots__,實(shí)例屬性將使用在幕后使用C語(yǔ)言實(shí)現(xiàn)的與數(shù)組相關(guān)的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ),并且以更高的效率優(yōu)化了它們的性能。
- classStudentRegular:
- def__init__(self,first_name, last_name):
- self.first_name = first_name
- self.last_name = last_name
- classStudentSlot: __slots__ = ['first_name', 'last_name']
- def__init__(self,first_name, last_name):
- self.first_name = first_name
- self.last_name = last_name
在類的定義中使用__slots__
上面的代碼展示了如何在類中實(shí)現(xiàn)__slots__的簡(jiǎn)單示例。具體來(lái)說(shuō),將所有屬性列為一個(gè)序列,這將在數(shù)據(jù)存儲(chǔ)中創(chuàng)建一對(duì)一匹配,以加快訪問(wèn)速度并減少內(nèi)存消耗。如前所述,常規(guī)類使用字典進(jìn)行屬性訪問(wèn),但不使用已實(shí)現(xiàn)__slots__的字典。以下代碼證實(shí)了這一點(diǎn):
- >>> student_r =StudentRegular('John', 'Smith')
- >>>student_r.__dict__
- {'first_name': 'John', 'last_name': 'Smith'}
- >>> student_s =StudentSlot('John', 'Smith')
- >>>student_s.__dict__
- Traceback (most recentcall last): File"", line 1, in
- AttributeError: 'StudentSlot' object has noattribute '__dict__'
具有__slots__的類中沒(méi)有__dict__
有關(guān)使用__slots__的詳細(xì)討論可以在Stack Overflow找到答案,你也可以從官方文檔中找到更多信息(https://docs.python.org/3/reference/datamodel.html)。
需要注意,使用__slots__會(huì)有一個(gè)副作用——它會(huì)阻止你動(dòng)態(tài)創(chuàng)建其他屬性。有人建議將其作為一種控制類擁有的屬性的機(jī)制,但這并不是它的設(shè)計(jì)初衷。
9. 文件
最后我們必須討論一下類的文檔。我們需要明白編寫文檔并不能替代任何代碼,編寫大量文檔并不能提高代碼的性能,也不一定會(huì)使代碼更具可讀性。如果必須依靠文檔字符串來(lái)澄清代碼,那么你的代碼很可能有問(wèn)題。
以下代碼將向大家展示一個(gè)程序員可能犯的錯(cuò)誤——使用不必要的注釋來(lái)補(bǔ)償錯(cuò)誤的代碼(即,在這種情況下,無(wú)意義的變量名)。相比之下,一些有好名字的好代碼甚至不需要注釋。
- # how many billable hours
- a =6
- # the hourly rate
- b =100
- # total charge
- c = a * b
- # The above vs.the below with no comments
- billable_hours =6
- hourly_rate =100
- total_charge = billable_hours * hourly_rate
失敗解釋案例
我并不是說(shuō)反對(duì)寫評(píng)論和文檔字符串,這實(shí)際上取決于自己的實(shí)例。如果你的代碼被多個(gè)人使用或多次使用(例如,你是唯一一個(gè)多次訪問(wèn)同一代碼的人),那么就應(yīng)考慮編寫一些好的注釋。
[[342616]]
這些注釋可以幫助你自己或者團(tuán)隊(duì)伙伴閱讀你的代碼,但是他們都不可以假定你的代碼完全按照注釋中的說(shuō)明進(jìn)行。換句話說(shuō),編寫好的代碼始終是需要牢記的頭等大事。
如果最終用戶要使用代碼的特定部分,那么需要編寫文檔字符串,因?yàn)檫@些人對(duì)相關(guān)的代碼庫(kù)并不熟悉。他們只想知道如何使用相關(guān)的API,而文檔字符串將構(gòu)成幫助菜單的基礎(chǔ)。因此,作為程序員,你有責(zé)任確保提供有關(guān)如何使用程序的明確說(shuō)明。
本文中回顧了定義自己的類時(shí)需要考慮的重要因素。編寫的代碼越多,你就越會(huì)發(fā)現(xiàn)在定義類之前牢記這些原則的重要性。定義類時(shí),請(qǐng)不斷練習(xí)這些準(zhǔn)則,好的設(shè)計(jì)會(huì)在以后節(jié)省很多開發(fā)時(shí)間。
新聞標(biāo)題:Python進(jìn)階版:定義類時(shí)應(yīng)用的9種最佳做法
URL鏈接:http://m.fisionsoft.com.cn/article/cdpsoho.html


咨詢
建站咨詢
