格與編程哲學(xué))
在 Python 社區(qū)混久了你一定會(huì)聽(tīng)到一句話“import this”。我第一次敲下這行代碼是在大二終端里刷出二十多行英文格言我以為是某種彩蛋腳本讀完之后除了“Zen of Python”這個(gè)名字有點(diǎn)酷剩下的說(shuō)實(shí)話沒(méi)太看懂。什么“優(yōu)美勝于丑陋”“簡(jiǎn)單勝于復(fù)雜”聽(tīng)起來(lái)像雞湯離我手頭正在折騰的爬蟲(chóng)、Web 開(kāi)發(fā)和數(shù)據(jù)處理差了十萬(wàn)八千里。直到后來(lái)寫(xiě)了幾萬(wàn)行 Python 代碼看過(guò)各種“能跑但讓人頭禿”的項(xiàng)目再翻回頭讀這一小段文本才意識(shí)到當(dāng)年沒(méi)看懂的不是英文是經(jīng)驗(yàn)。Python 之禪不是裝飾性的格言而是 Python 語(yǔ)言設(shè)計(jì)者和社區(qū)在無(wú)數(shù)取舍中沉淀出來(lái)的判斷標(biāo)準(zhǔn)。它直接影響你的代碼風(fēng)格、API 設(shè)計(jì)、框架選型甚至影響你怎么跟隊(duì)友溝通“這代碼到底該怎么寫(xiě)”。這篇文章我會(huì)把這 19 句話一句一句拆開(kāi)結(jié)合實(shí)際寫(xiě)代碼時(shí)的真實(shí)場(chǎng)景聊聊每一句背后到底在說(shuō)什么。然后分享一些我在落地這些原則時(shí)踩過(guò)的坑包括怎么命名、怎么寫(xiě)條件、怎么處理異常、怎么在“簡(jiǎn)潔”和“可讀”之間找平衡。不管你是剛接觸 Python 的初學(xué)者還是已經(jīng)寫(xiě)了幾年 Python 的老手重新審視這段詩(shī)大概率都會(huì)有新的收獲。1. 初識(shí)Python之禪它從哪來(lái)又該如何看1.1 它到底是什么Python 之禪是 Tim Peters 在 1999 年寫(xiě)的一組格言式編程原則一共 19 條。它在 Python 社區(qū)的地位有點(diǎn)類(lèi)似“程序員的行為準(zhǔn)則”或者“代碼品味綱領(lǐng)”。1999 年前后Python 語(yǔ)言還在發(fā)展期社區(qū)急需一套明確的設(shè)計(jì)哲學(xué)來(lái)引導(dǎo)語(yǔ)言演進(jìn)和庫(kù)的設(shè)計(jì)。Tim Peters 在 Python 郵件列表里發(fā)布了這 19 句話后來(lái)被 Python 的核心開(kāi)發(fā)者接受。早在 Python 2 時(shí)代它就躺在標(biāo)準(zhǔn)庫(kù)里隨時(shí)能通過(guò)import this調(diào)出來(lái)2004 年又被正式收錄為 PEP 20。所以你在網(wǎng)上看到有人提到 PEP 20說(shuō)的就是這一段文本。我印象里最早接觸它的渠道不是官方文檔而是某個(gè)論壇里有人發(fā)了個(gè)帖子問(wèn)“Python 里最裝逼的代碼是什么”下面一堆人回復(fù)import this。這個(gè)場(chǎng)景本身也挺有禪意——真正重要的東西往往藏在看似好玩的小彩蛋里。1.2 怎么查看打開(kāi)終端、IDLE 或者任何你習(xí)慣的 Python 環(huán)境敲兩行import this屏幕上會(huì)刷出下面這段The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases arent special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless youre Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, its a bad idea. If the implementation is easy to explain, it may still be a good idea. Namespaces are one honking great idea -- lets do more of those!順帶說(shuō)個(gè)冷知識(shí)this.py這個(gè)模塊的源碼本身也很“禪”。它把上面的文本用 ROT13凱撒密碼的一種加密之后硬編碼在源碼里導(dǎo)入時(shí)再解碼。你去 Python 安裝目錄下的lib/this.py看一眼就會(huì)明白——明明可以直接放明文偏要繞一下這種“實(shí)在沒(méi)什么用但很有意思”的小設(shè)計(jì)正是 Python 社區(qū)偏愛(ài)的氣質(zhì)。1.3 先糾正一個(gè)誤區(qū)很多初學(xué)者把 Python 之禪當(dāng)成“強(qiáng)制規(guī)范”好像代碼不按這 19 句寫(xiě)就是錯(cuò)的。其實(shí)它是設(shè)計(jì)哲學(xué)層面的指南不是 PEP 8 那種具體到“變量名要小寫(xiě)、縮進(jìn)要 4 個(gè)空格”的硬性規(guī)定。它不會(huì)告訴你細(xì)節(jié)它告訴你的是一件事當(dāng)你在兩難境地時(shí)優(yōu)先考慮什么。舉個(gè)例子。團(tuán)隊(duì)里爭(zhēng)論用列表推導(dǎo)式還是顯式 for 循環(huán)按“優(yōu)美勝于丑陋”可能會(huì)選列表推導(dǎo)式但按“可讀性優(yōu)先”又可能傾向 for 循環(huán)。真正的結(jié)論往往取決于具體上下文而不是某一條格言的教條。這一點(diǎn)恰恰是 Python 之禪的深意——它是思考框架不是規(guī)則集。2. 十九句詩(shī)逐句拆碎了講2.1 美學(xué)三連優(yōu)美、明了、簡(jiǎn)潔“Beautiful is better than ugly.”優(yōu)美勝于丑陋是 Python 之禪的第一句。什么是“優(yōu)美”的代碼我的理解是讀起來(lái)順暢、結(jié)構(gòu)對(duì)稱、意圖明顯。舉個(gè)最典型的對(duì)比# 能跑但丑 result [] for i in range(100): if i % 2 0: result.append(i * i) # 更 Pythonic result [i * i for i in range(100) if i % 2 0]兩條代碼功能完全一樣但第二條一眼就知道“把偶數(shù)平方收集起來(lái)”。列表推導(dǎo)式把“循環(huán) 條件 收集”三個(gè)動(dòng)作壓縮成一行順序閱讀大腦負(fù)擔(dān)明顯小得多。不過(guò)我也要潑盆冷水“優(yōu)美”是主觀標(biāo)準(zhǔn)不同人的審美不同。寫(xiě)單行推導(dǎo)式的人覺(jué)得自己的代碼如詩(shī)如畫(huà)看的人可能覺(jué)得是加密電報(bào)。所以“優(yōu)美”適合當(dāng)個(gè)人追求不適合當(dāng)團(tuán)隊(duì)的唯一標(biāo)準(zhǔn)落到團(tuán)隊(duì)還得靠后文的“可讀性”來(lái)兜底?!癊xplicit is better than implicit.”明了勝于晦澀強(qiáng)調(diào)的是顯式。拿字典取值來(lái)說(shuō)# 明確表達(dá)“key 可能不存在” value d.get(key, default_value) # 心里清楚“key 一定存在” value d[key]dict.get讓你把“key 可能不存在”這個(gè)事實(shí)擺在明面上比if key in d: value d[key]這種分支寫(xiě)法更直接。很多時(shí)候代碼很難懂不是因?yàn)檫壿嫃?fù)雜而是因?yàn)樽髡甙阉须[含假設(shè)都藏在腦子里看代碼的人只能靠猜。“Simple is better than complex.”簡(jiǎn)潔勝于復(fù)雜針對(duì)的是“過(guò)度設(shè)計(jì)”。新手寫(xiě)代碼有個(gè)經(jīng)典毛病一個(gè)“hello world”級(jí)別的需求非要抽象三層、繼承兩代、裝飾器元類(lèi)全上最后寫(xiě)了一千行。簡(jiǎn)潔的含義不是“行數(shù)少”而是“沒(méi)有多余的復(fù)雜度”。一個(gè)函數(shù)如果能用 5 行說(shuō)清楚就不要拆成 5 個(gè)單行函數(shù)互相調(diào)用一個(gè)類(lèi)如果只有 2 個(gè)方法就不要硬套抽象基類(lèi)。2.2 復(fù)雜和凌亂是兩回事“Complex is better than complicated.”復(fù)雜勝于凌亂是我非常喜歡的一句。復(fù)雜complex指的是問(wèn)題本身需要多層結(jié)構(gòu)、多種機(jī)制才能解決凌亂complicated指的是實(shí)現(xiàn)方式繞來(lái)繞去、理解成本極高而且很多繞路其實(shí)毫無(wú)必要。舉個(gè)例子寫(xiě)一個(gè)處理多層嵌套 JSON 的數(shù)據(jù)清洗腳本用遞歸、用棧、用狀態(tài)機(jī)你可以說(shuō)它復(fù)雜但這些復(fù)雜度是問(wèn)題本身帶來(lái)的。反過(guò)來(lái)你把一個(gè)簡(jiǎn)單的字典取值寫(xiě)成三層try-except嵌套那才是凌亂——問(wèn)題本身不難是你把它做難了。“Flat is better than nested.”扁平勝于嵌套說(shuō)的是結(jié)構(gòu)。寫(xiě)三層 if 嵌套的時(shí)候通??梢杂锰崆皉eturn或提取函數(shù)來(lái)壓平。我見(jiàn)過(guò)太多這樣的代碼# 嵌套地獄 def process(data): if data is not None: if data.status ok: if data.items: return sum(data.items) return 0 # 扁平化之后 def process(data): if data is None: return 0 if data.status ! ok: return 0 if not data.items: return 0 return sum(data.items)兩種寫(xiě)法邏輯一模一樣但后者一眼能看穿所有出口前者得數(shù)括號(hào)。這里的“扁平”還可以延伸到類(lèi)的繼承層次繼承深度超過(guò)三層出事時(shí)你根本想不起來(lái)哪個(gè)方法被誰(shuí)覆寫(xiě)過(guò)。“Sparse is better than dense.”稀疏勝于密集是從排版角度說(shuō)的。一行代碼不要塞太多邏輯。我見(jiàn)過(guò)一條鏈?zhǔn)秸{(diào)用.filter(...).map(...).sort(...).slice(...)直接超過(guò)一個(gè)屏幕寬度讀起來(lái)必須橫向滾動(dòng)。這種就該拆幾行或者干脆提取中間變量。2.3 可讀性優(yōu)先這句話是地基“Readability counts.”可讀性重要是我認(rèn)為全詩(shī)最核心的一句。Python 這門(mén)語(yǔ)言為什么偏愛(ài)縮進(jìn)而不是花括號(hào)本質(zhì)上就是在強(qiáng)制可讀性。花括號(hào)寫(xiě)起來(lái)自由但代碼塊邊界肉眼識(shí)別困難縮進(jìn)本身就是語(yǔ)法逼著你把結(jié)構(gòu)顯性化。這等于語(yǔ)言層面把“可讀性”變成了硬約束。可讀性不只是給同事看的更是給未來(lái)的自己看的。我經(jīng)??吹竭@種代碼# 這個(gè) 57.3 是什么 angle 57.3改成DEG_TO_RAD 3.1415926 / 180 angle 30 * DEG_TO_RAD可讀性立刻上升。變量名、函數(shù)名、模塊名都在傳達(dá)語(yǔ)義別把語(yǔ)義藏在魔法數(shù)字和難以理解的縮寫(xiě)里。我有個(gè)習(xí)慣寫(xiě)完一個(gè)函數(shù)隔一天再看一遍如果哪一行需要注釋才能看懂首選不是補(bǔ)注釋而是重寫(xiě)那行代碼讓它不需要注釋。注釋是最后的手段不是萬(wàn)能的遮羞布。2.4 特殊情況與實(shí)用主義規(guī)則不是用來(lái)綁死你的“Special cases arent special enough to break the rules. Although practicality beats purity.”特殊情況不足以打破規(guī)則盡管實(shí)用性勝過(guò)純粹這兩句成對(duì)看才有意思。第一句說(shuō)不要為個(gè)別特殊情況破壞通用規(guī)則。比如你寫(xiě)了一個(gè)排序函數(shù)它能處理各種數(shù)據(jù)類(lèi)型但有個(gè)人傳了一個(gè)“特殊”的對(duì)象進(jìn)來(lái)你是加一個(gè)if特例處理還是讓它走通用流程低速思考時(shí)人人都會(huì)喊“加個(gè)特例多簡(jiǎn)單”但代碼是會(huì)長(zhǎng)大的每多一個(gè)特例就多一條只有你懂的隱藏路徑最終會(huì)變成一團(tuán)補(bǔ)丁。第二句是補(bǔ)充但如果你真的遇到了極端情況實(shí)用性優(yōu)先。比如你為了 0.1% 的性能提升犧牲可讀性不能接受但如果這是核心路徑、每毫秒都關(guān)乎線上收入那合理的優(yōu)化是可以接受的。規(guī)則是用來(lái)指導(dǎo)我們做判斷的不是用來(lái)當(dāng)教條自我感動(dòng)的。我見(jiàn)過(guò)有人抱著“Special cases arent special enough”拒絕給一個(gè)業(yè)務(wù)接口加必要的兼容處理結(jié)果上線直接出錯(cuò)。這種機(jī)械執(zhí)行原則比不執(zhí)行更糟。2.5 錯(cuò)誤處理別讓異常靜默消失“Errors should never pass silently. Unless explicitly silenced.”錯(cuò)誤永遠(yuǎn)不應(yīng)該靜默傳遞除非顯式地沉默在 Python 社區(qū)里最經(jīng)典的壞習(xí)慣就是try: do_something() except: pass這就是“讓錯(cuò)誤靜默傳遞”。看起來(lái)程序不崩了實(shí)際上錯(cuò)誤被吞掉后續(xù)數(shù)據(jù)可能是錯(cuò)的、連接可能沒(méi)關(guān)閉、用戶可能看到錯(cuò)誤結(jié)果卻不自知。等到排查問(wèn)題時(shí)你翻遍日志什么線索都找不到只能靠猜。正確做法是要么讓錯(cuò)誤拋出去要么在日志里記錄要么顯式表達(dá)“我知道這里可能出錯(cuò)但我選擇忽略并寫(xiě)上原因”。比如try: do_something() except TimeoutError: # 已知瞬時(shí)超時(shí)跳過(guò)并記錄日志等下次任務(wù)重試 logger.warning(do_something timeout, skip)那一行注釋就是 “explicitly silenced” 的意思。我后來(lái)甚至養(yǎng)成了一個(gè)習(xí)慣任何except塊里至少要有一行日志哪怕是logger.debug。因?yàn)椤皼](méi)有日志的異常捕獲”跟“把垃圾掃到地毯下”沒(méi)區(qū)別。2.6 面對(duì)歧義拒絕猜測(cè)“In the face of ambiguity, refuse the temptation to guess.”面對(duì)歧義拒絕猜測(cè)寫(xiě)代碼時(shí)經(jīng)常遇到歧義。比如某個(gè)接口的返回字段可能是data也可能是Data你不確定是哪一個(gè)。有人會(huì)快速試一下、跑一次發(fā)現(xiàn)能通就寫(xiě)死。這就是猜測(cè)是在給自己埋雷。我見(jiàn)過(guò)一個(gè)線上事故根源是“我以為”。某個(gè)上游服務(wù)返回的金額字段文檔里寫(xiě)的是amount結(jié)果實(shí)際返回Amount那位同事在本地試了一次發(fā)現(xiàn)也能取到值就上線了。直到某天上游結(jié)構(gòu)調(diào)整字段名統(tǒng)一成小寫(xiě)線上立刻報(bào)錯(cuò)。正確的做法是查文檔、看源碼、問(wèn)寫(xiě)接口的人確認(rèn)之后再寫(xiě)。實(shí)在確認(rèn)不了的時(shí)候?qū)幙蓲伄惓R膊灰乱粋€(gè)值然后悄悄用下去。2.7 最好只有一種明顯的方式“There should be one-- and preferably only one --obvious way to do it.”應(yīng)該有一種最好只有一種明顯的方法去做這是 Python 與 Perl 哲學(xué)最大的分水嶺。Perl 的理念是“條條大路通羅馬”鼓勵(lì)多種寫(xiě)法Python 則強(qiáng)調(diào)“最好只有一種明顯的方式”把認(rèn)知負(fù)擔(dān)降到最低。當(dāng)然后面那句 “Although that way may not be obvious at first unless youre Dutch”不過(guò)這種方式可能一開(kāi)始不那么明顯除非你是荷蘭人是給 Python 之父 Guido van Rossum 準(zhǔn)備的玩笑——他是荷蘭人。意思是這種最明顯的方式可能只有語(yǔ)言設(shè)計(jì)者本人才能一眼看穿。所以社區(qū)要靠 PEP 8、慣用法、風(fēng)格指南把“明顯方式”固化下來(lái)讓大家有章可循。2.8 時(shí)機(jī)動(dòng)手與等待的平衡“Now is better than never. Although never is often better thanrightnow.”現(xiàn)在做比永遠(yuǎn)不做要好盡管永遠(yuǎn)不做往往好過(guò)立刻做這兩句也得放在一起看。第一句是行動(dòng)派號(hào)召有想法趕緊實(shí)現(xiàn)不要一直拖。項(xiàng)目管理里的“小步快跑”“敏捷迭代”都是這個(gè)意思。第二句是剎車(chē)但如果方案本身爛、需求沒(méi)理解清楚、評(píng)審還沒(méi)通過(guò)那“現(xiàn)在立刻實(shí)現(xiàn)”還不如先不做。我曾經(jīng)接手一個(gè)緊急需求時(shí)間緊到?jīng)]空拆函數(shù)全塞進(jìn)一個(gè)兩千行的腳本里功能上線倒是很快第二周改需求時(shí)完全無(wú)法下手最后花了兩晚重構(gòu)。現(xiàn)在我的做法是再緊急也至少拆成兩三個(gè)函數(shù)哪怕命名倉(cāng)促上線后第一時(shí)間補(bǔ)注釋、補(bǔ)文檔、補(bǔ)類(lèi)型標(biāo)注。技術(shù)債有復(fù)利越拖越貴。2.9 可解釋性是最好的設(shè)計(jì)檢驗(yàn)“If the implementation is hard to explain, its a bad idea. If the implementation is easy to explain, it may still be a good idea.”如果實(shí)現(xiàn)很難解釋那是個(gè)壞主意如果實(shí)現(xiàn)很容易解釋那也可能是個(gè)好主意一個(gè)實(shí)現(xiàn)應(yīng)該能向同事講清楚。如果你在 code review 時(shí)花了十分鐘解釋為什么這么寫(xiě)那大概率能簡(jiǎn)化。舉個(gè)例子數(shù)據(jù)清洗時(shí)我見(jiàn)過(guò)這種復(fù)雜 lambda 嵌套df.apply(lambda x: x[a] if x[b] 0 else (x[a] * 2 if x[c] else 0))這行代碼不是不能跑但講起來(lái)要繞“b 大于 0 就取 a否則如果 c 為真就取 a 的兩倍否則取 0”。更好的做法是抽一個(gè)命名函數(shù)def compute_value(row): if row[b] 0: return row[a] if not row[c]: return 0 return row[a] * 2代碼行數(shù)多了幾行但說(shuō)明成本低了一個(gè)量級(jí)。至于第二句“容易解釋未必是好設(shè)計(jì)”我也深有體會(huì)——一個(gè)代碼庫(kù)拆成幾百個(gè)兩行小函數(shù)每個(gè)都能講清楚但整體依賴關(guān)系亂成一鍋粥。所以“可解釋”是必要不充分條件。2.10 命名空間高頻出現(xiàn)的“靈感”“Namespaces are one honking great idea -- lets do more of those!”命名空間是個(gè)絕妙的主意讓我們做更多這個(gè) “honking” 帶著一點(diǎn)“響徹云霄”的興奮感。命名空間是 Python 組織代碼的基石模塊、類(lèi)、函數(shù)都提供了命名空間避免全局變量污染。實(shí)際開(kāi)發(fā)中合理地拆模塊、建類(lèi)、寫(xiě)函數(shù)本質(zhì)就是在制造干凈的命名空間。一個(gè)模塊導(dǎo)出 30 個(gè)公開(kāi)函數(shù)和只導(dǎo)出 5 個(gè)精心設(shè)計(jì)的函數(shù)給人帶來(lái)的心智負(fù)擔(dān)完全不同。命名空間的邊界越清晰依賴關(guān)系就越可見(jiàn)改起來(lái)就越敢下手。3. 從詩(shī)到代碼落地到日常開(kāi)發(fā)的實(shí)操3.1 命名先讓名字說(shuō)話Python 之禪沒(méi)有專(zhuān)門(mén)講“怎么命名”但“可讀性優(yōu)先”和“明了勝于晦澀”直接決定了命名這一環(huán)。我給自己定了幾條命名原則變量名盡量描述“是什么”函數(shù)名盡量描述“做什么”。布爾變量用is_、has_、should_開(kāi)頭。循環(huán)里短命的迭代變量可以用i、j、x一旦邏輯變復(fù)雜就換成有語(yǔ)義的詞。不要用拼音縮寫(xiě)尤其是那種五六位、不知道具體是哪幾個(gè)詞的縮寫(xiě)。類(lèi)名用駝峰函數(shù)和變量用下劃線先按 PEP 8 走。舉個(gè)例子我剛開(kāi)始寫(xiě) Python 時(shí)喜歡用短變量名以為這樣簡(jiǎn)潔高效def f(x, y): r [] for i in range(x): if i % y 0: r.append(i) return r后來(lái)給隊(duì)友 review他說(shuō)“這函數(shù)是干嘛的f是啥r是啥x是啥”我啞口無(wú)言。改成下面這樣后不用問(wèn)也知道在干什么def multiples_below(limit, divisor): result [] for n in range(limit): if n % divisor 0: result.append(n) return result功能一模一樣但第二版讀代碼的人一眼就知道它在求小于limit的能被divisor整除的數(shù)。命名這件事前期多花 30 秒后期省 30 分鐘。3.2 控制流把嵌套換成衛(wèi)語(yǔ)句除了上面提到的“提前 return”有個(gè)常見(jiàn)技巧叫衛(wèi)語(yǔ)句guard clause。原則是不滿足前置條件就提前退出把核心邏輯放在后面避免一層套一層。# 反面條件套條件 if condition: # 一大段核心邏輯 pass # 正面不滿足條件立刻退出 if not condition: return # 一大段核心邏輯但這里有個(gè)度如果函數(shù)超過(guò) 30 行到處都是return又會(huì)走向另一個(gè)極端——出口太多流程難跟。這時(shí)更好的辦法是拆函數(shù)把一段段核心邏輯提取成有名字的子函數(shù)。另外一個(gè)踩過(guò)的坑是三目表達(dá)式嵌套。三目表達(dá)式適合短的二選一value yes if flag else no一旦開(kāi)始嵌套就崩了# 別這樣寫(xiě) value a if cond1 else (b if cond2 else c)改成普通分支可讀性立刻回升if cond1: value a elif cond2: value b else: value c3.3 異常處理什么該吞什么該拋“Errors should never pass silently”是原則但實(shí)際工作里確實(shí)有吞異常的場(chǎng)景。比如爬蟲(chóng)抓數(shù)據(jù)時(shí)某一頁(yè)臟數(shù)據(jù)導(dǎo)致解析失敗你不想讓整個(gè)任務(wù)崩掉這時(shí)“吞掉并記錄日志”就是合法的 “explicitly silenced”。我自己的處理套路是捕獲具體異常類(lèi)型不用裸except:。捕獲后至少logger.warning或logger.exception。如果確定要忽略寫(xiě)一行注釋說(shuō)明為什么。盡量用細(xì)粒度捕獲別在一個(gè)大函數(shù)外面包一層寬 try??匆粋€(gè)實(shí)際例子。我寫(xiě)爬蟲(chóng)抓數(shù)據(jù)時(shí)是按這個(gè)風(fēng)格處理網(wǎng)絡(luò)異常的try: data request_json(url) except requests.exceptions.Timeout as e: logger.warning(frequest {url} timeout: {e}) return EMPTY_RESULT except requests.exceptions.RequestException as e: logger.error(frequest {url} failed: {e}) raise超時(shí)是瞬時(shí)的可以返回空結(jié)果交給重試邏輯其他請(qǐng)求異常說(shuō)明問(wèn)題更大直接拋出去讓上層告警。這就是“有選擇地靜默”。3.4 先寫(xiě)清楚再優(yōu)化Python 之禪沒(méi)直接提性能但“實(shí)現(xiàn)很難解釋就是壞主意”跟性能優(yōu)化強(qiáng)綁定。業(yè)務(wù)代碼里最怕為了一點(diǎn)性能把邏輯寫(xiě)成亂麻。比如在量化交易策略腳本里有人為了“快”把一個(gè)簡(jiǎn)單的條件計(jì)算改成手動(dòng)指針式索引結(jié)果跑起來(lái)發(fā)現(xiàn)瓶頸根本不在這。我的做事順序是先寫(xiě)出清晰版本用cProfile或timeit量化確認(rèn)是瓶頸后再優(yōu)化。優(yōu)化時(shí)盡量保持結(jié)構(gòu)不變只換數(shù)據(jù)結(jié)構(gòu)和算法不重寫(xiě)控制流。舉個(gè)例子# 清晰版本 squares [n * n for n in numbers if n % 2 0]如果numbers有幾百萬(wàn)個(gè)元素且這段是熱點(diǎn)路徑可以考慮用 numpy 或生成器但前提是先證明這里是瓶頸再動(dòng)手。3.5 API 設(shè)計(jì)少即是多給別人設(shè)計(jì)接口時(shí)Python 之禪同樣適用。一個(gè)模塊如果提供了三種幾乎一樣又略有差異的函數(shù)用戶必然困惑# 用戶會(huì)問(wèn)這倆區(qū)別是啥 fetch_data(url) download_data(url)我設(shè)計(jì) API 時(shí)有幾個(gè)習(xí)慣函數(shù)職責(zé)單一一個(gè)函數(shù)只做一件事。參數(shù)語(yǔ)義一致同名參數(shù)盡量同類(lèi)型。少量參數(shù)用位置參數(shù)大量參數(shù)用關(guān)鍵字參數(shù)。謹(jǐn)慎使用**kwargs它會(huì)削弱可讀性和類(lèi)型檢查。好的函數(shù)簽名本身就是文檔。def fetch_data(url: str, timeout: int 30, retries: int 3)比def fetch_data(*args, **kwargs)清晰得多。用戶不用讀源碼就能知道這函數(shù)支持哪些配置。4. 容易走偏的坑與我的避坑實(shí)錄4.1 “簡(jiǎn)潔”不等于“一行流”“Simple is better than complex”不等于“越短越好”。我曾經(jīng)給一個(gè)同事 review 代碼他把五層業(yè)務(wù)邏輯壓成了一條 90 個(gè)字符的列表推導(dǎo)式還特意不換行。他覺(jué)得自己很酷但我說(shuō)這不可讀。后來(lái)我們達(dá)成共識(shí)列表推導(dǎo)式只要超過(guò)一層if或一層for就拆成普通循環(huán)或抽取函數(shù)。還有一種情況一行表達(dá)式本身很簡(jiǎn)單但結(jié)果依賴一串全局變量讀代碼的人根本不知道這些變量從哪來(lái)。所以我的判斷標(biāo)準(zhǔn)不是“多少行”而是“能不能順暢講清楚”。4.2 過(guò)度顯式也是病“Explicit is better than implicit”也有邊界。比如# 過(guò)度顯式 if result is True: pass # 直接用 result 即可 if result: passis True除了在極少數(shù)邊界情況比如區(qū)分1和True有意義外多數(shù)時(shí)候是廢話。顯式的目的是讓意圖清楚不是把所有隱含信息都展開(kāi)。Python 的with語(yǔ)句是另一種“隱式”的好例子。它隱式幫你關(guān)閉文件、釋放鎖。如果非要自己寫(xiě)try-finally才算顯式那反而是開(kāi)倒車(chē)。判斷標(biāo)準(zhǔn)是這個(gè)“隱式”行為是大眾預(yù)期內(nèi)的還是只有作者知道的。前者放心用后者要警惕。4.3 “一種明顯方式”在團(tuán)隊(duì)里的現(xiàn)實(shí)“明顯的方式”聽(tīng)起來(lái)很美但團(tuán)隊(duì)里照樣吵。因?yàn)椤懊黠@”取決于讀者的經(jīng)驗(yàn)和背景。經(jīng)驗(yàn)老到的人覺(jué)得dict.setdefault一目了然新手可能覺(jué)得是個(gè)黑魔法有人習(xí)慣collections.Counter做統(tǒng)計(jì)有人只會(huì)手寫(xiě)defaultdict。我的建議是用 linter比如 ruff、flake8統(tǒng)一風(fēng)格把常用慣用法整理成團(tuán)隊(duì)規(guī)范文檔當(dāng)代碼風(fēng)格出現(xiàn)爭(zhēng)議時(shí)參考項(xiàng)目歷史里怎么寫(xiě)的而不是個(gè)人審美。比如“字典計(jì)數(shù)”這個(gè)問(wèn)題用Counter是明顯的方式那就定了。這一類(lèi)小決策累積起來(lái)就是團(tuán)隊(duì)的“代碼審美”。4.4 靜默吞異常的教訓(xùn)“錯(cuò)誤靜默”這件事我踩過(guò)真坑。有一段時(shí)間我寫(xiě)的任務(wù)調(diào)度代碼catch 了一個(gè)寬泛的Exception之后什么都沒(méi)做導(dǎo)致數(shù)據(jù)庫(kù)里攢了大量臟數(shù)據(jù)任務(wù)狀態(tài)永遠(yuǎn)停留在“進(jìn)行中”。排查了三天才追到源頭異常在最里層的try中被吞掉外層根本不知道。從那以后我給自己立了規(guī)矩任何except里至少要有一行日志。能用具體異常就不用Exception。出現(xiàn)裸寫(xiě)except: pass代碼評(píng)審直接打回。后來(lái)我還引入了sentry-sdk做實(shí)時(shí)異常上報(bào)。日志可能沒(méi)人看但報(bào)警一定會(huì)有人處理。這個(gè)轉(zhuǎn)變算是把“錯(cuò)誤不要靜默傳遞”真正落到了生產(chǎn)環(huán)境。4.5 “現(xiàn)在做”與“好好做”的平衡我第二份工作的時(shí)候產(chǎn)品經(jīng)理給了一個(gè)“今晚就要上線”的需求。為了趕進(jìn)度我把所有邏輯塞進(jìn)一個(gè)函數(shù)全局變量滿天飛甚至連變量名都是臨時(shí)的a1、a2。功能倒是按時(shí)上了線但第二周需求變更時(shí)我根本不敢動(dòng)那段代碼——一改就崩。現(xiàn)在的處理方式不一樣了。緊急情況下我也至少拆成兩三個(gè)函數(shù)盡量不碰全局變量。上線后如果時(shí)間實(shí)在緊沒(méi)來(lái)得及優(yōu)化我就在任務(wù)系統(tǒng)里建一個(gè)“重構(gòu)技術(shù)債”的待辦兩周內(nèi)必須清償。這是我從“Now is better than never”和“never is often better than right now”之間找到的個(gè)人平衡動(dòng)手要快但爛代碼不能讓它在代碼庫(kù)里過(guò)冬。5. 延伸Python之禪對(duì)編程心智和AI時(shí)代的啟發(fā)5.1 和 Unix 哲學(xué)的呼應(yīng)Python 之禪不孤立。它和 Unix 哲學(xué)血脈相通小即是美、一個(gè)程序只做好一件事、用文本作為接口。Python 社區(qū)反復(fù)強(qiáng)調(diào)“簡(jiǎn)單、可組合、可讀”這和 Unix 工具鏈的設(shè)計(jì)理念一脈相承。理解了這一層你再看一些大牌的 Python 庫(kù)設(shè)計(jì)就能明白它們?yōu)槭裁词悄莻€(gè)樣子。比如 Flask 的路由設(shè)計(jì)、Django 的 ORM 封裝幾乎都能在 Python 之禪里找到對(duì)應(yīng)的取舍依據(jù)。5.2 對(duì)設(shè)計(jì)框架和 API 的指導(dǎo)作用做框架的人尤其需要 Python 之禪。一個(gè)框架如果文檔寫(xiě)得再詳細(xì)都解釋不清通常意味著設(shè)計(jì)有硬傷。反過(guò)來(lái)說(shuō)如果一個(gè)框架能快速給新手講明白“這個(gè)對(duì)象是什么、方法怎么調(diào)、參數(shù)怎么傳”那它的設(shè)計(jì)大概率是穩(wěn)的。我自己在寫(xiě)一個(gè)小的內(nèi)部工具庫(kù)時(shí)也會(huì)拿 Python 之禪當(dāng)設(shè)計(jì)評(píng)審清單用戶是不是只能想到一種調(diào)用方式異常路徑會(huì)不會(huì)被悄悄吞掉類(lèi)和方法層次是否扁平核心概念能不能用一句話說(shuō)明白這些問(wèn)題問(wèn)下來(lái)經(jīng)常能發(fā)現(xiàn)一些原本覺(jué)得很順的設(shè)計(jì)其實(shí)很繞。5.3 AI 編程時(shí)代禪意沒(méi)有過(guò)時(shí)現(xiàn)在用 AI 輔助寫(xiě)代碼越來(lái)越普遍。很多人直接讓 AI 生成整個(gè)功能模塊然后跑一下沒(méi)問(wèn)題就完事。但這樣很容易產(chǎn)生“連 AI 自己都解釋不清”的代碼更別提交給你的同事了。恰恰在這個(gè)時(shí)代Python 之禪里的可解釋性、可讀性、顯式優(yōu)于隱式變得更稀缺、更重要。我的個(gè)人習(xí)慣是讓 AI 生成代碼后逐行 review看不懂的行直接刪掉或重寫(xiě)。把關(guān)鍵業(yè)務(wù)邏輯寫(xiě)進(jìn) docstring這樣 AI 后續(xù)修改時(shí)能理解意圖不會(huì)亂改。用 Python 之禪做 review 清單時(shí)刻問(wèn)自己這段代碼我能解釋清楚嗎如果解釋不清就是壞主意。AI 生成代碼的門(mén)檻趨近于零代碼的可維護(hù)性反而成了最稀缺的資源。這時(shí)候“If the implementation is hard to explain, its a bad idea”這句話的權(quán)重是上升的不是下降的。寫(xiě)了這么多年 Python我越來(lái)越覺(jué)得 Python 之禪不是知識(shí)而是審美和取舍的經(jīng)驗(yàn)壓縮包。剛?cè)腴T(mén)你看到的是二十行英文格言老手看到的是無(wú)數(shù)個(gè)加班的晚上、無(wú)數(shù)次 code review 的爭(zhēng)論、無(wú)數(shù)回重構(gòu)之后沉淀下來(lái)的判斷力。我的建議很簡(jiǎn)單不用刻意背它把它貼到顯示器旁邊然后去寫(xiě)真實(shí)項(xiàng)目。遇到兩難決策時(shí)把那句話找出來(lái)對(duì)照一下想一想你為什么會(huì)猶豫。等積累夠了你自己也能總結(jié)出屬于自己的“編程之禪”。最后再分享一個(gè)小技巧在團(tuán)隊(duì)代碼評(píng)審里與其引用“Python 之禪第 X 條”當(dāng)論據(jù)不如把格言翻譯成業(yè)務(wù)場(chǎng)景問(wèn)一句“這種寫(xiě)法將來(lái)接手的人要花多久看懂” 這個(gè)問(wèn)法往往比爭(zhēng)論抽象原則更能讓雙方達(dá)成一致。禪意從來(lái)不是用來(lái)背的而是用來(lái)在真實(shí)問(wèn)題里做判斷的。