Python 工匠:一個關於模塊的小故事

前言

這是 「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.pyoop

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 仔細分析了一下錯誤,發現錯誤是由於 usersmarketing 模塊之間產生的環形依賴關係致使的。

當程序在 notify_users.py 文件導入 fancy_site.users 模塊時,users 模塊發現本身須要從 marketing 模塊那裏導入 send_sms 函數。而解釋器在加載 marketing 模塊的過程當中,又反過來發現本身須要依賴 users 模塊裏面的 User 對象。

如此一來,整個模塊依賴關係成爲了環狀,程序天然也就無法執行下去了。

modules_before

不過,沒有什麼問題可以難倒一個能夠正常訪問 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 拒絕了。

小 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
複製代碼

新的模塊依賴關係以下圖所示:

modules_afte

在新的模塊結構中,整個項目被整齊的分爲三層,模塊間的依賴關係也變得只有單向流動。以前在函數內部 import 的「延遲導入」技巧,天然也就沒有用武之地了。

小 R 修改後的代碼得到了你們的承認,很快就被合併到了主分支。故事暫告一段落,那麼這個故事告訴了咱們什麼道理呢?

總結

模塊間的循環依賴是一個在大型 Python 項目中很常見的問題,越複雜的項目越容易碰到這個問題。當咱們在參與這些項目時,若是對模塊結構、分層、抽象缺乏應有的重視。那麼項目很容易就會慢慢變得複雜無比、難以維護。

因此,合理的模塊結構與分層很是重要。它能夠大大下降開發人員的心智負擔和項目維護成本。這也是我爲何要和你分享這個簡單故事的緣由。「在函數內延遲 import」 的作法固然沒有錯,但咱們更應該關注的是:整個項目內的模塊依賴關係與分層是否合理。

最後,讓咱們再嘗試從 小 R 的故事裏強行總結出幾個道理吧:

  • 合理的模塊結構與分層能夠下降項目的開發維護成本
  • 合理的模塊結構不是一成不變的,應該隨着項目發展調整
  • 遇到問題時,不要選**「簡單但有缺陷」的那個方案,要選「麻煩但正確」**的那個
  • 整個項目內的模塊間依賴關係流向,應該是單向的,不能有環形依賴存在

看完文章的你,有沒有什麼想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。

附錄

系列其餘文章:

相關文章
相關標籤/搜索