摘要:很多人在編寫Python代碼時,只注重相關功能的實現,并不關心代碼的整潔性。當軟件的代碼量達到一定規模后,不少開發者感到代碼越來越混亂、越來越難以維護。學習Python語言,不能只關注語法規則的掌握。通過代碼示例,文章闡述了與提高軟件代碼整潔性相關的幾個方面內容:寫代碼時,要注意可讀性;要撰寫功能單一且功能清晰的函數;應充分利用裝飾器、生成器和迭代器等Python獨有的特性,寫出高效的代碼;應了解并經常使用Python軟件質量保證工具Unittest、Pytest、Pylint和Flake8等測試和掃描代碼。
關鍵詞:編程規范;代碼可讀性;軟件可維護性;單元測試
BriefDiscussiononHowtoWriteCleanPythonCode
ShiYemu
FacultyofAIinEducation,CentralChinaNormalUniversityHubeiWuhan430079
Abstract:WhenwritingPythoncode,manypeopleonlyfocusontheimplementationofrelatedfunctionsanddonotcareaboutthecleanlinessofthecode.Whentheamount?;ofsoftwarecodereachesacertainscale,manydevelopersfeelthatthecodebecomesincreasinglyconfusinganddifficulttomaintain.WhenlearningthePythonlanguage,youcan'tjustfocusonmasteringthegrammaticalrules.Throughexamples,thisarticleexplainsseveralaspectsrelatedtoimprovingthecleanlinessofsoftwarecode:payattentiontoreadabilitywhilewritingcode;writefunctionswithsingleandclearintention;makefulluseofdecorators,generatorsanditerators,whicharePython'sspecialfeatures,towriteefficientcode;youshouldunderstandandfrequentlyusePythonSWqualityassurancetoolssuchasunittest,pytest,pylintandflake8,etc.,totestandscanyourcode.
Keywords:Programmingspecifications;codereadability;softwaremaintainability;unittest
實現同樣的功能,不同的程序員寫出來的代碼完全不一樣,有的人寫的代碼很長,既不容易讀懂又很難維護,軟件的可維護性差,會降低工作效率,相應的項目會越來越難以持續下去,嚴重的會導致項目組不得不重新設計、編寫代碼。有的軟件作者,在不同時期寫的代碼風格不一樣。有的項目組,每個人都有自己的風格,組內風格不統一,大家溝通起來不順暢。這些情況的出現,都是因為程序員沒有注意到代碼的整潔性。本文通過舉例,論述了為寫出整潔的代碼而需要注意的幾個地方。
1要有好的可讀性
針對邏輯上并不復雜的功能需求,有的編程者寫的代碼別人很難讀懂,有的人看不明白自己幾個月前寫的代碼,這都是因為代碼的可讀性差。可讀性不好的軟件,維護起來非常不容易。
寫代碼的同時,要寫相應的注釋,寫代碼和寫注釋要做到同步;注釋要有一定的占比,不能可有可無;定義函數的時候,要有函數功能和各個參數說明的注釋;定義類的時候,類的每個屬性和方法都要有相應的注釋;修改代碼的時候,相應的注釋也必須修改。雖然注釋不參與編譯、不參與程序的運行,但注釋非常重要,要把注釋看作是程序的一個組成部分。
除了增加注釋外,文件名、變量名、函數名和類名等要盡可能地做到顧名思義和一望而知[1],不要讓閱讀代碼的人(包括將來的自己)去猜。例如:
classPerson:
def__init__(self,name,email,phone):
self.name=name
self.email=email
self.phone=phone
顯然,類Person的初始化函數記錄了人名、郵箱和電話。寫代碼能做到見其名知其意很好,但也不要過度。例如,下面的代碼和前面相比,很詳盡,但并沒有增加可讀性,反而讓人覺得過于啰唆:
classPerson:
def__init__(self,personal_username,personal_email_address,personl_telephone_number):
self.personal_username=personal_username
self.personal_email_address=personal_email_address
self.personal_telephone_num=personal_telephone_number
2函數功能要單一
假設有如下函數,功能是取得一個列表,然后打印該列表的所有元素:
deffetch_and_show_users():
users=[…]#由某算法得到列表
foruserinusers:
print(user)#顯示列表
這個函數有兩個功能,但有的用戶只需要得到列表,而有的用戶只想顯示列表。這時,對于某些調用該函數的用戶而言,代碼里出現了累贅,這增加了代碼的冗余,多余的步驟對程序的調試會造成干擾。把前面的函數分解為如下的兩個函數,非常方便于用戶的調用:
deffetch_users():
users=[]#由某算法得到列表
returnusers
defdisplay_users(users):
foruserinusers:
print(user)#顯示列表
再舉個例子,某個函數具備下載、解壓縮和按照某種規則選取解壓之后文件的功能,最好將這個函數分解。否則,可能會出現這樣的情況:有的用戶只是想下載某個壓縮包而調用了該函數,他不得不等著解壓縮和選取文件這兩個他并不需要的步驟完成。
不僅是函數,還有類和模塊,都應功能單一,只做一件事而且要將事情做好[2]。
3函數功能要清晰
先看如下轉換大小寫的函數,該函數的作用不難理解:
deftransform_text(text,uppercase):
ifuppercase:
returntext.upper()
else:
returntext.lower()
顯然,當調用transform_text(text,True)時,字符串text中的小寫字母都轉換為大寫;當調用transform_text(text,False)時,字符串text中的大寫字母都轉換為小寫。如果不看函數transform_text的定義,僅僅看transform_text(text,True/False),很難知道它在做什么。
將上面的函數分為如下兩個函數:
defturn_to_uppercase(text):
returntext.upper()
defturn_to_lowercase(text):
returntext.lower()
當調用turn_to_uppercase(text)時,字符串text中的小寫字母都轉換為大寫;當調用turn_to_lowercase(text)時,字符串text中的大寫字母都轉換為小寫。這時,僅僅看turn_to_uppercase(text)或者turn_to_lowercase(text),就知道在做什么。函數的功能要做到清晰明確[3]。
在調用函數遇到關鍵字參數的時候,帶上關鍵字名字可以增強可讀性,而且不必擔心參數的次序寫錯。例如,SendMail(from=a@x.com,to=b@y.com)比SendMail(a@x.com,b@y.com)的可讀性強很多,用意一目了然,就像在讀簡單明了的英文句子。
4使用更Pythonic的語法
Python代碼要有“Python的味道”,同等條件下,代碼要寫得Pythonic一些。例如,遍歷一個列表,可用如下for循環:
forkinrange(len(a_list)):
print('index:',k,'value:',a_list[k])
上面的代碼是正確的,但這是傳統編程語言的方法。作為Python程序員,應使用針對可迭代對象的內置函數enumerate:
forindex,iteminenumerate(a_list):
print('index:',index,'value:',item)
交換兩個變量a和b的值,傳統語言的代碼如下:
tmp=b
b=a
a=tmp
Python代碼這樣寫也是沒有問題的。實際上,Python語言有如下簡便的寫法,用以交換a和b的值:
a,b=b,a
判斷字符串的前綴與后綴,可以使用內置的字符串方法startswith和endswith,也可以使用切片。例如,如下if語句作用是相同的:
sentence="Helloeveryone,goodmorning"
ifsentence.startswith("Hello"):……
ifsentence[:5]=="Hello":……
ifsentence.endswith("morning"):……
ifsentence[-7:]=="morning":……
很明顯,startswith和endswith的可讀性更好,使用切片的話,可讀性下降,而且容易將字母的個數寫錯。
對一個列表的每個元素進行相同的操作,使用map和列表推導都可以。例如:
>>>nums=[1,2,3,4,5]
>>>squares=list(map(lambdax:x**2,nums))
>>>squares
[1,4,9,16,25]
>>>squares=[x**2forxinnums]
>>>squares
[1,4,9,16,25]
顯然,列表推導的可讀性更強、形式上簡單,而且列表推導的速度比map快,運行效率高。完成同樣的任務,應盡可能使用列表推導而不是map。
5充分利用Python的獨有特性
裝飾器本身是一個函數,裝飾器的返回值是一個函數對象,裝飾器可以讓其他函數在不需要做任何代碼變動的前提下增加額外功能[4]。例如,有N個函數,現在要給它們增加函數日志和函數性能測試的功能。如果是傳統編程語言,需要對每個函數進行修改。對于Python而言,不需要進行N次類似甚至同樣的修改。定義一個裝飾器decorator,對函數進行封裝,添加所需的功能(日志和性能測試),然后在每個函數的頭部添加一行@decorator即可,N個函數的原始代碼保持不變。這樣保證了代碼的穩定性,在減少重復勞動的同時,增加了函數的功能。
生成器支持延遲計算,在需要的時候可以生成相應的值,而不是一次性地生成整個序列。生成器特別適合于大型數據處理,使用生成器,可以減少內存使用,優化代碼,提高程序的效率。
6使用Unittest和Pytest進行測試
有的程序員寫了很長時間的代碼,但從未對自己的代碼進行過單元測試,只有在程序運行遇到錯誤時才開始檢查當中的問題。單元測試是用來對函數、類或者模塊進行正確性檢驗的工作[5]。如果代碼都通過了單元測試,那么軟件的質量將大大提高。Unittest是Python自帶的測試框架,例如,如下代碼使用斷言來測試函數square()是否正確:
importunittest
defsquare(a):
returna*a
classST(unittest.TestCase):
deftest_square(self):
self.assertEqual(square(7),49)
if__name__=="__main__":
unittest.main()
Pytest是一個第三方的測試框架,兼容Unittest,比Unittest框架使用起來更簡潔,效率更高,使用命令pipinstallpytest安裝它。Pytest編寫測試用例很容易,用例可以是類的形式,也可以是函數的形式。例如,如下代碼用來測試函數double()的正確性:
defdouble(x):
return3*x
deftest_dbl():
assertdouble(8)==16
運行pytest,結果為:
deftest_dbl():
>assertdouble(8)==16
Eassert24==16
E+where24=double(8)
test.py:4:AssertionError
根據輸出,很容易看到問題所在,將函數double()中的3*x改為2*x就解決了。
7使用Pylint等工具檢查代碼
在項目編碼完成或者階段性完成后,應該使用質量保證工具對代碼進行掃描,就像體檢一樣。Pylint是一個針對Python代碼中的語法錯誤、潛在問題和代碼風格的靜態檢查工具,使用pipinstallpylint命令安裝它。下面的代碼一共三行,看上去沒有任何問題:
defadd_one(x):
returnx+1;
print(add_one(15))
運行命令python3mpylint<文件名>,用Pylint分析這三行看似正確且毫無瑕疵的代碼,得到的結果為:
2:0:W0311:Badindentation.Found3spaces,expected4(badindentation)
2:0:W0301:Unnecessarysemicolon(unnecessarysemicolon)
1:0:C0114:Missingmoduledocstring(missingmoduledocstring)
1:0:C0116:Missingfunctionormethoddocstring(missingfunctiondocstring)
短短的三行代碼,掃描出了四個問題:(1)第二行的縮進是三個空格,最好是四個空格;(2)第二行結尾的分號沒有必要;(3)文件沒有注釋說明;(4)函數也沒有注釋說明。修改如下,再使用Pylint掃描就沒有問題了:
'''Thispythonscriptisforpylintstudy'''
defadd_one(x):
'''inputparameterisanumber,
add1tothenumber,andreturn'''
returnx+1
print(add_one(15))
8學習PEP8
除了語法,程序員也要學習編碼規范方面的知識。PEP8是Python編碼規范指南,PEP是PythonEnhancementProposals的簡寫,遵循該規范可以讓開發者寫出整潔的代碼,提高代碼的可讀性,有助于同一個項目組內大家的編碼風格保持一致[6]。使用工具Pycodestyle可以檢查代碼是否遵從PEP8,運行命令pipinstallpycodestyle安裝它。如下是非常簡短的四行代碼:
a=1
b=2
print(a==b)
if(b>a):print("bisbigger")
運行命令pycodestyle<文件名>,檢測這四行代碼,結果如下:
1:2:E225missingwhitespacearoundoperator
3:6:E211whitespacebefore'('
4:3:E275missingwhitespaceafterkeyword
4:10:E231missingwhitespaceafter':'
4:10:E701multiplestatementsononeline(colon)
使用Pycodestyle發現的問題有:a=1的等號兩邊應該有空格;print和(之間的空格是不需要的;關鍵字if的后面應該有空格;最后一行最好分為兩行。將代碼修改如下,再使用Pycodestyle掃描就沒有問題了:
a=1
b=2
print(a==b)
ifb>a:
print("bisbigger")
工具Flake8的功能比Pycodestyle更加強大,安裝命令為pipinstallflake8。Flake8將Pyflakes(類似于Pylint)、Pycodestyle和McCabe(代碼復雜性檢查器)整合到一起,使用它可以一次性檢查出多種問題。Flake8非常易于與其他工具結合,比如,在集成開發環境PyCharm中配置Flake8非常簡單。
結語
Python誕生于1991年,近些年,Python語言已經滲透到各個領域,使用Python的人越來越多。Python用戶不能僅僅學語法,還應該了解如何讓代碼變得更整潔。軟件的維護期一般長于(甚至遠遠長于)開發期,不整潔的代碼很難提高工作效率。作為Python開發人員,要想寫出整潔的代碼,除了語法規則之外,需要了解的知識點比較多,很難用一篇短文完全闡述清楚。希望本文能給新Python程序員一點啟發,為代碼質量的提升提供些許幫助。
參考文獻:
[1]HarshitTyagi.PythonicCode:BestPracticestoMakeYourPythonMoreReadable[EB/OL].(20220530).https://www.codementor.io/blog/pythoniccode6yxqdoktzt.
[2]AlexOmeyer.10MustKnowPatternsforWritingCleanCodeWithPython[EB/OL].(20220406).https://dzone.com/articles/10mustknowpatternsforwritingcleancodewith1.
[3]KhuyenTran.PythonCleanCode:6BestPracticestoMakeYourPythonFunctionsMoreReadable[EB/OL].(20210121).https://towardsdatascience.com/pythoncleancode6bestpracticestomakeyourpythonfunctionsmorereadable7ea4c6171d60.
[4]蘇尼爾·卡皮爾.Python代碼整潔之道編寫優雅的代碼[M].連少華,譯.北京:機械工業出版社,2020.
[5]馬里西諾·阿納亞.編寫整潔的Python代碼[M].包永帥,譯.北京:人民郵電出版社,2021.
[6]ThePEPEditors.IndexofPythonEnhancementProposals[EB/OL].(20000713).https://peps.python.org/.
作者簡介:石也牧(2004—),女,漢族,北京人,本科,研究方向:人工智能。