這是 「Python 工匠」系列的第 9 篇文章。[查看系列全部文章]python
模塊(Module)是咱們用來組織 Python 代碼的基本單位。不少功能強大的複雜站點,都由成百上千個獨立模塊共同組成。git
雖然模塊有着不可替代的用處,但它有時也會給咱們帶來麻煩。好比,當你接手一個新項目後,剛展開項目目錄。第一眼就看到了攀枝錯節、難以理解的模塊結構,那你確定會想: 「這項目也太難搞了。」 😂程序員
在這篇文章裏,我準備了一個和模塊有關的小故事與你分享。github
小 R 是一個剛從學校畢業的計算機專業學生。半個月前,他面試進了一家互聯網公司作 Python 開發,負責一個與用戶活動積分有關的小項目。項目的主要功能是查詢站點活躍用戶,併爲他們發送有關活動積分的通知: 「親愛的用戶,您好,您當前的活動積分爲 x」。面試
項目主要由 notify_users.py
腳本和 fancy_site
包組成,結構與各文件內容以下:函數
├── fancy_site
│ ├── __init__.py
│ ├── marketing.py # 與市場活動有關的內容
│ └── users.py # 與用戶有關的內容
└── notify_users.py # 腳本:發送積分通知
複製代碼
文件 notify_users.py
:工具
from fancy_site.users import list_active_users
from fancy_site.marketing import query_user_points
def main():
"""獲取全部的活躍用戶,將積分狀況發送給他們"""
users = get_active_users()
points = list_user_points(users)
for user in users:
user.add_notification(... ...)
# <... 已省略 ...>
複製代碼
文件 fancy_site/users.py
:oop
from typing import List
class User:
# <... 已省略 ...>
def add_notification(self, message: str):
"""爲用戶發送新通知"""
pass
def list_active_users() -> List[User]:
"""查詢全部活躍用戶"""
pass
複製代碼
文件:fancy_site/marketing.py
:測試
from typing import List
from .users import User
def query_user_points(users: List[User]) -> List[int]:
"""批量查詢用戶活動積分"""
def send_sms(phone_number: str, message: str):
"""爲某手機號發送短信"""
複製代碼
只要在項目目錄下執行 python notify_user.py
,就能實現給全部活躍用戶發送通知。spa
但有一天,產品經理找過來講,光給用戶發站內信通知還不夠,容易被用戶忽略。除了站內信之外,咱們還須要同時給用戶推送一條短信通知。
琢磨了五秒鐘後,小 R 跟產品經理說:「這個需求能夠作!」。畢竟給手機號發送短信的 send_sms()
函數早就已經有人寫好了。他只要先給 add_notification
方法添加一個可選參數 enable_sms=False
,當傳值爲 True
時調用 fancy_site.marketing
模塊裏的 send_sms
函數就行。
一切聽上去根本沒有什麼難度可言,十分鐘後,小 R 就把 user.py
改爲了下面這樣:
# 導入 send_sms 模塊的發送短信函數
from .marketing import send_sms
class User:
# <...> 相關初始化代碼已省略
def add_notification(self, message: str, enable_sms=False):
"""爲用戶添加新通知"""
if enable_sms:
send_sms(user.mobile_number, ... ...)
複製代碼
可是,當他修改完代碼,再次執行 notify_users.py
腳本時,程序卻報錯了:
Traceback (most recent call last):
File "notify_users.py", line 2, in <module>
from fancy_site.users import list_active_users
File .../fancy_site/users.py", line 3, in <module>
from .marketing import send_sms
File ".../fancy_site/marketing.py", line 3, in <module>
from .users import User
ImportError: cannot import name 'User' from 'fancy_site.users' (.../fancy_site/users.py)
複製代碼
錯誤信息說,沒法從 fancy_site.users
模塊導入 User
對象。
小 R 仔細分析了一下錯誤,發現錯誤是由於 users
與 marketing
模塊之間產生的環形依賴關係致使的。
當程序在 notify_users.py
文件導入 fancy_site.users
模塊時,users
模塊發現本身須要從 marketing
模塊那裏導入 send_sms
函數。而解釋器在加載 marketing
模塊的過程當中,又反過來發現本身須要依賴 users
模塊裏面的 User
對象。
如此一來,整個模塊依賴關係成爲了環狀,程序天然也就無法執行下去了。
不過,沒有什麼問題可以難倒一個能夠正常訪問 Google 的程序員。小 R 隨便上網一搜,發現這樣的問題很好解決。由於 Python 的 import 語句很是靈活,他只須要 把在 users 模塊內導入 send_sms 函數的語句挪到 add_notification
方法內,延緩 import 語句的執行就行啦。
class User:
# <...> 相關初始化代碼已省略
def add_notification(self, message: str, send_sms=False):
"""爲用戶添加新通知"""
# 延緩 import 語句執行
from .marketing import send_sms
複製代碼
改動一行代碼後,大功告成。小 R 簡單測試後,發現一切正常,而後把代碼推送了上去。不太小 R 還沒來得及爲本身點個贊,意料以外的事情發生了。
這段明明幾乎完美的代碼改動在 Code Review 的時候被審計人小 C 拒絕了。
小 R 的同事小 C 是一名有着多年經驗的 Python 程序員,他對小 R 說:「使用延遲 import,雖然能夠立刻解決包導入問題。但這個小問題背後隱藏了更多的信息。好比,你有沒有想過 send_sms 函數,是否是已經不適合放在 marketing 模塊裏了?」
被小 C 這麼一問,聰明的小 R 立刻意識到了問題所在。要在 users
模塊內發送短信,重點不在於用延遲導入解決環形依賴。而是要以此爲契機,發現當前模塊間依賴關係的不合理,拆分/合併模塊,建立新的分層與抽象,最終消除環形依賴。
認識清楚問題後,他很快提交了新的代碼修改。在新代碼中,他建立了一個專門負責通知與消息類的工具模塊 msg_utils
,而後把 send_sms
函數挪到了裏面。以後 users
模塊內就能夠毫無困難的從 msg_utils
模塊中導入 send_sms
函數了。
from .msg_utils import send_sms
複製代碼
新的模塊依賴關係以下圖所示:
在新的模塊結構中,整個項目被整齊的分爲三層,模塊間的依賴關係也變得只有單向流動。以前在函數內部 import
的「延遲導入」技巧,天然也就沒有用武之地了。
小 R 修改後的代碼得到了你們的承認,很快就被合併到了主分支。故事暫告一段落,那麼這個故事告訴了咱們什麼道理呢?
模塊間的循環依賴是一個在大型 Python 項目中很常見的問題,越複雜的項目越容易碰到這個問題。當咱們在參與這些項目時,若是對模塊結構、分層、抽象缺乏應有的重視。那麼項目很容易就會慢慢變得複雜無比、難以維護。
因此,合理的模塊結構與分層很是重要。它能夠大大下降開發人員的心智負擔和項目維護成本。這也是我爲何要和你分享這個簡單故事的緣由。「在函數內延遲 import」 的作法固然沒有錯,但咱們更應該關注的是:整個項目內的模塊依賴關係與分層是否合理。
最後,讓咱們再嘗試從 小 R 的故事裏強行總結出幾個道理吧:
看完文章的你,有沒有什麼想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。
系列其餘文章: