您現在的位置是:首頁 > 綜合
Python進階版:定義類時應用的9種最佳做法
- 由 讀芯術 發表于 綜合
- 2023-01-24
定義的規則是什麼請簡要闡述
全文共
9544
字,預計學習時長
24
分鐘
圖源:unsplash
作為一種OOP語言,Python透過支援以物件為主的各種功能來處理資料和功能。例如,資料結構是所有物件,包括原始型別(例如整數和字串),而在某些其他語言中,原始型別則不視為物件。對於另一個例項,函式是所有物件,它們僅僅是定義了其他物件的屬性(例如類或模組)。
儘管可以使用內建資料型別,而且無需建立任何自定義類就能編寫一組函式,但隨著專案範圍的擴大,程式碼可能會越來越難維護。這些單獨程式碼部分的主題並不相同,儘管有很多資訊是相關的,但管理它們之間的聯絡卻並不簡單。
在這些情況下,定義自己的類就很划得來了,這樣一來你可以對相關資訊進行分組並且改善專案的結構設計。而且由於你即將處理更少的分段程式碼,程式碼庫的長期可維護性將得到改善。但要注意,僅當以正確方式完成類宣告時,操作才可以實現,定義自定義類的益處才能超過管理它們的支出。
1。好的命名
定義自己的類,就好比在程式碼庫中添加了一位新成員。因此應該給類起個好名字。雖然類名的唯一限制是合法Python變數的規則(例如,不能以數字開頭),但是有一些好用的方法來命名類。
· 使用易於發音的名詞。在參與團隊專案時,這一點尤其重要。在小組演講中,你恐怕不願意這樣講:“在這種情況下,我們建立Zgnehst類的例項。” 另外,易於發音也意味著名稱不應太長,使用三個以上的單詞來定義類名簡直無法想象。一個字是最佳,兩個字其次,三個字不能再多啦!
· 反映其儲存的資料和預期功能。就像在現實生活中一樣——當看到男性化的名字時,我們就會預設這個孩子是男孩。同樣的方式也適用於類名(或通常的任何其他變數),命名規則很簡單——不要讓人感覺奇怪。如果要處理學生的資訊,那麼該課程應該命名為Student,KiddosAtCampus並不是一個常規的好名字。
· 遵循命名約定。應該對類名使用駱駝拼寫法,例如GoodName。以下是非常規類名稱的不完整列表:goodName,Good_Name,good_name以及GOodnAme。遵循命名約定是為了使意圖表現明確。在別人閱讀你的程式碼時,可以毫無疑問地假定命名為GoodName的物件是一個類。
也有適用於屬性和功能的命名規則和約定,以下各節將在使用情況下簡要提及,但是總體原理是相同的。
2。顯式例項屬性
在大多數情況下,我們都想定義自己的例項初始化方法(即__init__)。在此種方法中,設定了新建立的類例項的初始狀態。但是,Python並沒有限制可以在何處使用自定義類定義例項屬性。換句話說,你可以在建立例項之後的後續操作中定義其他例項屬性。
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
初始化方法
如上所示,可以透過指定學生的名字和姓氏來建立“學生”類的例項。稍後,在呼叫例項方法(即verify_registration_status)時,將設定“學生例項”的status屬性。
但這不是理想的模式,因為如果在整個類中散佈了各種例項屬性,那麼該類就無法明確例項物件擁有哪些資料。因此,最佳做法是將例項的屬性放在__init__方法中,這樣程式碼閱讀器就可以透過單一位置來了解你的類的資料結構,如下所示:
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
更好的初始化方法
對於最初無法設定的那些例項屬性的問題,可以使用佔位符值(例如None)進行設定。儘管沒什麼好擔心的,但是當忘記呼叫某些例項方法來設定適用的例項屬性時,此更改還有助於防止可能的錯誤,從而導致AttributeError(‘Student’ object has noattribute ‘status_verified’)。
在命名規則方面,應使用小寫字母命名屬性,並遵循蛇形命名法——如果使用多個單詞,請在它們之間使用下劃線連線。此外,所有名稱都應對其儲存的資料有具有意義的指示(例如first_name比fn更好)。
3。使用屬性——但要精簡
圖源:unsplash
有些人在具備其他OOP語言(例如Java)背景的情況下學習Python編碼,並且習慣於為例項的屬性建立getter和setter。可以透過在Python中使用屬性裝飾器來模仿這一模式。以下程式碼展示了使用屬性裝飾器實現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()
屬性裝飾
建立此屬性後,儘管它是透過內部函式實現的,我們仍然可以使用點符號將其用作常規屬性。
>>> 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
After setting: JohnnySmith
使用屬性
使用屬性實現的優點包括驗證正確的值設定(檢查是否使用字串,而不是使用整數)和只讀訪問許可權(透過不實現setter方法)。但應該同時使用屬性,如果自定義類如下所示,可能會很讓人分心——屬性太多了!
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}”
濫用屬性
在大多數情況下,這些屬性可以用例項屬性代替,因此我們可以訪問它們並直接設定它們。除非對使用上述屬性的好處有特定的需求(例如:值驗證),否則使用屬性優先於在Python中建立屬性。
4。定義有意義的字串表示法
在Python中,名稱前後帶有雙下劃線的函式稱為特殊方法或魔術方法,有些人將其稱為dunder方法。這些方法對直譯器的基本操作有特殊的用法,包括我們先前介紹的__init__方法。__repr__和__str__這兩種特殊方法對於建立自定義類的正確字串表示法至關重要,這將為程式碼閱讀器提供有關類的更直觀資訊。
它們之間的主要區別在於__repr__方法定義了字串,你可以使用該字串透過呼叫eval(repr(“therepr”))重新建立物件,而__str__方法定義的字串則更具描述性,並允許更多定製。換句話說,你可以認為__repr__方法中定義的字串由開發人員檢視,而__str__方法中使用的字串由常規使用者檢視。請看以下示例:
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}”
字串表示法的實現
請注意,在__repr__方法的實現中,f字串使用!r來顯示帶引號的這些字串,因為使用格式正確的字串構造例項很有必要。如果不使用!r格式,則字串將為Student(John, Smith),這不是構造“學生”例項的正確方法。
來看看這些實現如何為我們顯示字串:在互動式直譯器中訪問物件時會呼叫__repr__方法,而在列印物件時預設會呼叫__str__方法。
>>> student =Student(“David”, “Johnson”)
>>> student
Student(‘David’, ‘Johnson’)
>>>print(student)
Student: DavidJohnson
字串表示法
5。例項方法,類方法和靜態方法
在一個類中,我們可以定義三種方法:例項方法、類方法和靜態方法。我們需要考慮針對所關注的功能應使用哪些方法,以下是一些常規準則。
圖源:unsplash
例如,如果方法與單個例項物件有關,那麼需要訪問或更新例項的特定屬性。在這種情況下,應使用例項方法。這些方法具有如下簽名:def do_something(self):,其中self自變數引用呼叫該方法的例項物件。
如果方法與單個例項物件無關,則應考慮使用類方法或靜態方法。可以使用適用的修飾符輕鬆定義這兩種方法:類方法(classmethod)和靜態方法(staticmethod)。
兩者之間的區別在於,類方法允許你訪問或更新與類相關的屬性,而靜態方法則獨立於任何例項或類本身。類方法的一個常見示例是提供一種方便的例項化方法,而靜態方法可以只是一個實用函式。請看以下程式碼示例:
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”
不同的方法
也可以用類似的方式建立類屬性。與前面討論的例項屬性不同,類屬性由所有例項物件共享,並且它們應當反映一些獨立於各個例項物件的特徵。
6。使用私有屬性進行封裝
在為專案編寫自定義類時,需要考慮封裝問題,尤其期望其他人也使用你的類的話就更應如此。當類的功能增長時,某些功能或屬性僅與類內資料處理相關。換句話說,除了類之外,這些函式都將不會被呼叫,並且除你之外其他使用類的使用者甚至不會在意這些函式的實現細節。在這些情況下,應該考慮封裝。
按照慣例,應用封裝的一種重要方法是為屬性和函式加上下劃線或兩個下劃線。二者之間有著細微的區別:帶有下劃線的被認為是受保護的,而帶有兩個下劃線的被認為是私有的,這涉及在建立後進行名稱處理。
從本質上來說,像這樣命名屬性和功能,是在告訴IDE(即整合開發環境,例如PyCharm),儘管在Python中不存在真正的私有屬性,但它們不會在類之外被訪問。
classStudent:
defget_mean_gpa(self):
grades = self。_get_grades()
gpa_list =Student。_converted_gpa_from_grades(grades)
returnsum(gpa_list) /len(gpa_list)
def_get_grades(self):
# fetch grades from a database
grades = [99, 100, 94, 88]
return grades
@staticmethod
def_converted_gpa_from_grades(grades):
# convert the grades to GPA
gpa_list = [4。0, 4。0, 3。7, 3。4]
return gpa_list
封裝
上面的程式碼展示了一個簡單的封裝示例。如果想了解學生的評價GPA,那麼我們可以使用get_mean_gpa方法獲得GPA。使用者不需要知道平均GPA的計算方式,我們可以透過在函式名稱前新增下劃線來保護相關方法。
這一最佳做法的主要收穫是,與使用者使用你的程式碼相關的公共API,僅公開最少的數量。對於僅在內部使用的那些程式碼,請將其設定為受保護的方法或私有方法。
圖源:unsplash
7。分離關注點和解耦
隨著專案的發展,你會發現自己正在處理更多資料,如果你只堅持使用一個類會變得很麻煩。繼續以“學生”類為例,假設學生在學校吃午餐,並且每個人都有一個餐飲帳戶,可以用來支付餐費。從理論上講,我們可以處理學生類中與帳戶相關的資料和功能,如下所示:
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)
混合功能
上面的程式碼向展示了一些有關檢查賬戶餘額和向賬戶充值的虛擬碼,這兩種虛擬碼都在Student類中實現。還有更多與該帳戶相關的操作,例如凍結丟失的卡、合併帳戶——實施所有這些操作會使“學生”類越來越大,從而使維護變得越來越困難。你應該分離這些職責並使學生類不負責這些與帳戶相關的功能,即一種稱為解耦的設計模式。
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()
分離關注點
上面的程式碼展示了我們如何使用附加的Account類來設計資料結構。如你所見,我們將所有與帳戶相關的操作移至Account類。要實現檢索學生的帳戶資訊的功能,學生類將透過從Account類中檢索資訊來處理。如果想實現更多與該類相關的功能,只需簡單地更新Account類即可。
設計模式的主要要點是,希望各個類具有單獨的關注點。透過將這些職責分開,你的類將變小,處理較小的程式碼元件會使將來的更改變得更容易。
8。考慮使用__slots__進行最佳化
如果你的類主要用於儲存資料的資料容器,那麼可以考慮使用__slots__來最佳化類的效能。它不僅可以提高屬性訪問的速度,還可以節省記憶體,如果需要建立數千個或更多例項物件,就是它發揮大作用之處啦。
原因是,對於常規類,例項屬性是透過內部託管的字典儲存的。相比之下,透過使用__slots__,例項屬性將使用在幕後使用C語言實現的與陣列相關的資料結構儲存,並且以更高的效率優化了它們的效能。
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__
上面的程式碼展示瞭如何在類中實現__slots__的簡單示例。具體來說,將所有屬性列為一個序列,這將在資料儲存中建立一對一匹配,以加快訪問速度並減少記憶體消耗。如前所述,常規類使用字典進行屬性訪問,但不使用已實現__slots__的字典。以下程式碼證實了這一點:
>>> 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__的類中沒有__dict__
有關使用__slots__的詳細討論可以在Stack Overflow找到答案,你也可以從官方文件中找到更多資訊(https://docs。python。org/3/reference/datamodel。html)。
需要注意,使用__slots__會有一個副作用——它會阻止你動態建立其他屬性。有人建議將其作為一種控制類擁有的屬性的機制,但這並不是它的設計初衷。
9。檔案
最後我們必須討論一下類的文件。我們需要明白編寫文件並不能替代任何程式碼,編寫大量文件並不能提高程式碼的效能,也不一定會使程式碼更具可讀性。如果必須依靠文件字串來澄清程式碼,那麼你的程式碼很可能有問題。
以下程式碼將向大家展示一個程式設計師可能犯的錯誤——使用不必要的註釋來補償錯誤的程式碼(即,在這種情況下,無意義的變數名)。相比之下,一些有好名字的好程式碼甚至不需要註釋。
# 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
失敗解釋案例
我並不是說反對寫評論和文件字串,這實際上取決於自己的例項。如果你的程式碼被多個人使用或多次使用(例如,你是唯一一個多次訪問同一程式碼的人),那麼就應考慮編寫一些好的註釋。
這些註釋可以幫助你自己或者團隊夥伴閱讀你的程式碼,但是他們都不可以假定你的程式碼完全按照註釋中的說明進行。換句話說,編寫好的程式碼始終是需要牢記的頭等大事。
如果終端使用者要使用程式碼的特定部分,那麼需要編寫文件字串,因為這些人對相關的程式碼庫並不熟悉。他們只想知道如何使用相關的API,而文件字串將構成幫助選單的基礎。因此,作為程式設計師,你有責任確保提供有關如何使用程式的明確說明。
本文中回顧了定義自己的類時需要考慮的重要因素。編寫的程式碼越多,你就越會發現在定義類之前牢記這些原則的重要性。定義類時,請不斷練習這些準則,好的設計會在以後節省很多開發時間。
留言點贊關注
我們一起分享AI學習與發展的乾貨
如轉載,請後臺留言,遵守轉載規範