一. 軟件的開發規范
你現在包括之前寫的一些程序,所謂的'項目',都是在一個py文件下完成的,代碼量撐死也就幾百行,你認為沒問題,挺好。但是真正的后端開發的項目,系統等,少則幾萬行代碼,多則十幾萬,幾十萬行代碼,你全都放在一個py文件中行么?當然你可以說,只要能實現功能即可。咱們舉個例子,如果你的衣物只有三四件,那么你隨便堆在櫥櫃里,沒問題,咋都能找到,也不顯得特別亂,但是如果你的衣物,有三四十件的時候,你在都堆在櫥櫃里,可想而知,你找你穿過三天的襪子,最終從你的大衣口袋里翻出來了,這是什么感覺和心情......
軟件開發,規范你的項目目錄結構,代碼規范,遵循PEP8規范等等,讓你更加清晰滴,合理滴開發。
那么接下來我們以博客園系統的作業舉例,將我們之前在一個py文件中的所有代碼,整合成規范的開發。
首先我們看一下,這個是我們之前的目錄結構(簡化版):
py文件的具體代碼如下:

status_dic = { 'username': None, 'status': False, } flag = True def login(): i = 0 with open('register', encoding='utf-8') as f1: dic = {i.strip().split('|')[0]: i.strip().split('|')[1] for i in f1} while i < 3: username = input('請輸入用戶名:').strip() password = input('請輸入密碼:').strip() if username in dic and dic[username] == password: print('登錄成功') return True else: print('用戶名密碼錯誤,請重新登錄') i += 1 def register(): with open('register', encoding='utf-8') as f1: dic = {i.strip().split('|')[0]: i.strip().split('|')[1] for i in f1} while 1: print('\033[1;45m 歡迎來到注冊頁面 \033[0m') username = input('請輸入用戶名:').strip() if not username.isalnum(): print('\033[1;31;0m 用戶名有非法字符,請重新輸入 \033[0m') continue if username in dic: print('\033[1;31;0m 用戶名已經存在,請重新輸入 \033[0m') continue password = input('請輸入密碼:').strip() if 6 <= len(password) <= 14: with open('register', encoding='utf-8', mode='a') as f1: f1.write(f'\n{username}|{password}') status_dic['username'] = str(username) status_dic['status'] = True print('\033[1;32;0m 恭喜您,注冊成功!已幫您成功登錄~ \033[0m') return True else: print('\033[1;31;0m 密碼長度超出范圍,請重新輸入 \033[0m') def auth(func): def inner(*args, **kwargs): if status_dic['status']: ret = func(*args, **kwargs) return ret else: print('\033[1;31;0m 請先進行登錄 \033[0m') if login(): ret = func(*args, **kwargs) return ret return inner @auth def article(): print(f'\033[1;32;0m 歡迎{status_dic["username"]}訪問文章頁面\033[0m') @auth def diary(): print(f'\033[1;32;0m 歡迎{status_dic["username"]}訪問日記頁面\033[0m') @auth def comment(): print(f'\033[1;32;0m 歡迎{status_dic["username"]}訪問評論頁面\033[0m') @auth def enshrine(): print(f'\033[1;32;0m 歡迎{status_dic["username"]}訪問收藏頁面\033[0m') def login_out(): status_dic['username'] = None status_dic['status'] = False print('\033[1;32;0m 注銷成功 \033[0m') def exit_program(): global flag flag = False return flag choice_dict = { 1: login, 2: register, 3: article, 4: diary, 5: comment, 6: enshrine, 7: login_out, 8: exit_program, } while flag: print(''' 歡迎來到博客園首頁 1:請登錄 2:請注冊 3:文章頁面 4:日記頁面 5:評論頁面 6:收藏頁面 7:注銷 8:退出程序''') choice = input('請輸入您選擇的序號:').strip() if choice.isdigit(): choice = int(choice) if 0 < choice <= len(choice_dict): choice_dict[choice]() else: print('\033[1;31;0m 您輸入的超出范圍,請重新輸入 \033[0m') else: print('\033[1;31;0m 您您輸入的選項有非法字符,請重新輸入 \033[0m')
此時我們是將所有的代碼都寫到了一個py文件中,如果代碼量多且都在一個py文件中,那么對於代碼結構不清晰,不規范,運行起來效率也會非常低。所以我們接下來一步一步的修改:
-
程序配置.
你項目中所有的有關文件的操作出現幾處,都是直接寫的register相對路徑,如果說這個register注冊表路徑改變了,或者你改變了register注冊表的名稱,那么相應的這幾處都需要一一更改,這樣其實你就是把代碼寫死了,那么怎么解決? 我要統一相同的路徑,也就是統一相同的變量,在文件的最上面寫一個變量指向register注冊表的路徑,代碼中如果需要這個路徑時,直接引用即可。
-
划分文件。
一個項目的函數不能只是這些,我們只是舉個例子,這個小作業函數都已經這么多了,那么要是一個具體的實際的項目,函數會非常多,所以我們應該將這些函數進行分類,然后分文件而治。在這里我划分了以下幾個文件:
settings.py: 配置文件,就是放置一些項目中需要的靜態參數,比如文件路徑,數據庫配置,軟件的默認設置等等
類似於我們作業中的這個:
common.py:公共組件文件,這里面放置一些我們常用的公共組件函數,並不是我們核心邏輯的函數,而更像是服務於整個程序中的公用的插件,程序中需要即調用。比如我們程序中的裝飾器auth,有些函數是需要這個裝飾器認證的,但是有一些是不需要這個裝飾器認證的,它既是何處需要何處調用即可。比如還有密碼加密功能,序列化功能,日志功能等這些功能都可以放在這里。
src.py:這個文件主要存放的就是核心邏輯功能,你看你需要進行選擇的這些核心功能函數,都應該放在這個文件中。
start.py:項目啟動文件。你的項目需要有專門的文件啟動,而不是在你的核心邏輯部分進行啟動的,有人對這個可能不太理解,我為什么還要設置一個單獨的啟動文件呢?你看你生活中使用的所有電器基本都一個單獨的啟動按鈕,汽車,熱水器,電視,等等等等,那么為什么他們會單獨設置一個啟動按鈕,而不是在一堆線路板或者內部隨便找一個地方開啟呢? 目的就是放在顯眼的位置,方便開啟。你想想你的項目這么多py文件,如果src文件也有很多,那么到底哪個文件啟動整個項目,你還得一個一個去尋找,太麻煩了,這樣我把它單獨拿出來,就是方便開啟整個項目。
那么我們寫的項目開啟整個項目的代碼就是下面這段:
你把這些放置到一個文件中也可以,但是沒有必要,我們只需要一個命令或者一個開啟指令就行,就好比我們開啟電視只需要讓人很快的找到那個按鈕即可,對於按鈕后面的一些復雜的線路板,我們並不關心,所以我們要將上面這個段代碼整合成一個函數,開啟項目的''按鈕''就是此函數的執行即可。
這個按鈕要放到啟動文件start.py里面。
除了以上這幾個py文件之外還有幾個文件,也是非常重要的:
類似於register文件:這個文件文件名不固定,register只是我們項目中用到的注冊表,但是這種文件就是存儲數據的文件,類似於文本數據庫,那么我們一些項目中的數據有的是從數據庫中獲取的,有些數據就是這種文本數據庫中獲取的,總之,你的項目中有時會遇到將一些數據存儲在文件中,與程序交互的情況,所以我們要單獨設置這樣的文件。
log文件:log文件顧名思義就是存儲log日志的文件。日志我們一會就會講到,日志主要是供開發人員使用。比如你項目中出現一些bug問題,比如開發人員對服務器做的一些操作都會記錄到日志中,以便開發者瀏覽,查詢。
至此,我們將這個作業原來的兩個文件,合理的划分成了6個文件,但是還是有問題的,如果我們的項目很大,你的每一個部分相應的你一個文件存不下的,比如你的src主邏輯文件,函數很多,你是不是得分成:src1.py src2.py?
你的文本數據庫register這個只是一個注冊表,如果你還有個人信息表,記錄表呢? 如果是這樣,你的整個項目也是非常凌亂的:
3. 划分具體目錄
上面看着就非常亂了,那么如何整改呢? 其實非常簡單,原來你就是30件衣服放在一個衣櫃里,那么你就得分類裝,放外套的地方,放內衣的地方,放佩飾的地方等等,但是突然你的衣服編程300件了,那一個衣櫃放不下,我就整多個櫃子,分別放置不同的衣物。所以我們這可以整多個文件夾,分別管理不同的物品,那么標准版本的目錄結構就來了:
為什么要設計項目目錄結構?
"設計項目目錄結構",就和"代碼編碼風格"一樣,屬於個人風格問題。對於這種風格上的規范,一直都存在兩種態度:
-
一類同學認為,這種個人風格問題"無關緊要"。理由是能讓程序work就好,風格問題根本不是問題。
-
另一類同學認為,規范化能更好的控制程序結構,讓程序具有更高的可讀性。
我是比較偏向於后者的,因為我是前一類同學思想行為下的直接受害者。我曾經維護過一個非常不好讀的項目,其實現的邏輯並不復雜,但是卻耗費了我非常長的時間去理解它想表達的意思。從此我個人對於提高項目可讀性、可維護性的要求就很高了。"項目目錄結構"其實也是屬於"可讀性和可維護性"的范疇,我們設計一個層次清晰的目錄結構,就是為了達到以下兩點:
-
可讀性高: 不熟悉這個項目的代碼的人,一眼就能看懂目錄結構,知道程序啟動腳本是哪個,測試目錄在哪兒,配置文件在哪兒等等。從而非常快速的了解這個項目。
-
可維護性高: 定義好組織規則后,維護者就能很明確地知道,新增的哪個文件和代碼應該放在什么目錄之下。這個好處是,隨着時間的推移,代碼/配置的規模增加,項目結構不會混亂,仍然能夠組織良好。
所以,我認為,保持一個層次清晰的目錄結構是有必要的。更何況組織一個良好的工程目錄,其實是一件很簡單的事兒。
上面那個圖片就是較好的目錄結構。
二. 按照項目目錄結構,規范博客園系統
接下來,我就帶領大家把具體的代碼寫入對應的文件中,並且將此項目啟動起來,一定要跟着我的步驟一步一步去執行:
-
配置start.py文件
我們首先要配置啟動文件,啟動文件很簡答就是將項目的啟動執行放置start.py文件中,運行start.py文件可以成功啟動項目即可。 那么項目的啟動就是這個指令run() 我們把這個run()放置此文件中不就行了?
這樣你能執行這個項目么?肯定是不可以呀,你的starts.py根本就找不到run這個變量,肯定是會報錯的。
NameError: name 'run' is not defined 本文件肯定是找不到run這個變量也就是函數名的,不過這個難不倒我們,我們剛學了模塊, 另個一文件的內容我們可以引用過來。但是你發現import run 或者 from src import run 都是報錯的。為什么呢? 騷年,遇到報錯不要慌!我們說過你的模塊之所以可以引用,那是因為你的模塊肯定在這個三個地方:內存,內置,sys.path里面,那么core在內存中肯定是沒有的,也不是內置,而且sys.path也不可能有,因為sys.path只會將你當前的目錄(bin)加載到內存,所以你剛才那么引用肯定是有問題的,那么如何解決?內存,內置你是左右不了的,你只能將core的路徑添加到sys.path中,這樣就可以了。
import sys sys.path.append(r'D:\lnh.python\py project\teaching_show\blog\core') from src import run run()
這樣雖然解決了,但是你不覺得有問題么?你現在從這個start文件需要引用src文件,那么你需要手動的將src的工作目錄添加到sys.path中,那么有沒有可能你會引用到其他的文件?比如你的項目中可能需要引用conf,lib等其他py文件,那么在每次引用之前,或者是開啟項目時,全部把他們添加到sys.path中么?
sys.path.append(r'D:\lnh.python\py project\teaching_show\blog\core') sys.path.append(r'D:\lnh.python\py project\teaching_show\blog\conf') sys.path.append(r'D:\lnh.python\py project\teaching_show\blog\db') sys.path.append(r'D:\lnh.python\py project\teaching_show\blog\lib')
這樣是不是太麻煩了? 我們應該怎么做?我們應該把項目的工作路徑添加到sys.path中,用一個例子說明:你想找張三,李四,王五,趙六等人,這些人全部都在一棟樓比如在匯德商廈,那么我就告訴你匯德商廈的位置:北京昌平區沙河鎮匯德商廈。 你到了匯德商廈你在找具體這些人就可以了。所以我們只要將這個blog項目的工作目錄添加到sys.path中,這樣無論這個項目中的任意一個文件引用項目中哪個文件,就都可以找到了。所以:
import sys sys.path.append(r'D:\lnh.python\py project\teaching_show\blog') from core.src import run run()
上面還是差一點點,你這樣寫你的blog的路徑就寫死了,你的項目不可能只在你的電腦上,項目是共同開發的,你的項目肯定會出現在別人電腦上,那么你的路徑就是問題了,在你的電腦上你的blog項目的路徑是上面所寫的,如果移植到別人電腦上,他的路徑不可能與你的路徑相同, 這樣就會報錯了,所以我們這個路徑要動態獲取,不能寫死,所以這樣就解決了:
import sys import os # sys.path.append(r'D:\lnh.python\py project\teaching_show\blog') print(os.path.dirname(__file__)) # 獲取本文件的絕對路徑 # D:/lnh.python/py project/teaching_show/blog/bin print(os.path.dirname(os.path.dirname(__file__))) # 獲取父級目錄也就是blog的絕對路徑 # D:/lnh.python/py project/teaching_show/blog BATH_DIR = os.path.dirname(os.path.dirname(__file__)) sys.path.append(BATH_DIR) from core.src import run run()
那么還差一個小問題,這個starts文件可以當做腳本文件進行直接啟動,如果是作為模塊,被別人引用的話,按照這么寫,也是可以啟動整個程序的,這樣合理么?這樣是不合理的,作為啟動文件,是不可以被別人引用啟動的,所以我們此時要想到 __name__
了:
import sys import os # sys.path.append(r'D:\lnh.python\py project\teaching_show\blog') # print(os.path.dirname(__file__)) # 獲取本文件的絕對路徑 # D:/lnh.python/py project/teaching_show/blog/bin # print(os.path.dirname(os.path.dirname(__file__))) # 獲取父級目錄也就是blog的絕對路徑 # D:/lnh.python/py project/teaching_show/blog BATH_DIR = os.path.dirname(os.path.dirname(__file__)) sys.path.append(BATH_DIR) from core.src import run if __name__ == '__main__': run()
這樣,我們的starts啟動文件就已經配置成功了。以后只要我們通過starts文件啟動整個程序,它會先將整個項目的工作目錄添加到sys.path中,然后在啟動程序,這樣我整個項目里面的任何的py文件想引用項目中的其他py文件,都是你可以的了。
-
配置settings.py文件。
接下來,我們就會將我們項目中的靜態路徑,數據庫的連接設置等等文件放置在settings文件中。
我們看一下,你的主邏輯src中有這樣幾個變量:
status_dic = { 'username': None, 'status': False, } flag = True register_path = r'D:\lnh.python\py project\teaching_show\blog\register'
我們是不是應該把這幾個變量都放置在settings文件中呢?不是!setttings文件叫做配置文件,其實也叫做配置靜態文件,什么叫靜態? 靜態就是一般不會輕易改變的,但是對於上面的代碼status_dic ,flag這兩個變量,由於在使用這個系統時會時長變化,所以不建議將這個兩個變量放置在settings配置文件中,只需要將register_path放置進去就可以。
register_path = r'D:\lnh.python\py project\teaching_show\blog\register'
但是你將這個變量放置在settings.py之后,你的程序啟動起來是有問題,為什么?
with open(register_path, encoding='utf-8') as f1: NameError: name 'register_path' is not defined
因為主邏輯src中找不到register_path這個路徑了,所以會報錯,那么我們解決方式就是在src主邏輯中引用settings.py文件中的register_path就可以了。
這里引發一個問題:為什么你這樣寫就可以直接引用settings文件呢?我們在starts文件中已經說了,剛已啟動blog文件時,我們手動將blog的路徑添加到sys.path中了,這就意味着,我在整個項目中的任何py文件,都可以引用到blog項目目錄下面的任何目錄:bin,conf,core,db,lib,log這幾個,所以,剛才我們引用settings文件才是可以的。
-
配置common.py文件
接下來,我們要配置我們的公共組件文件,在我們這個項目中,裝飾器就是公共組件的工具,我們要把裝飾器這個工具配置到common.py文件中。先把裝飾器代碼剪切到common.py文件中。這樣直接粘過來,是有各種問題的:
所以我們要在common.py文件中引入src文件的這兩個變量。
可是你的src文件中使用了auth裝飾器,此時你的auth裝飾器已經移動位置了,所以你要在src文件中引用auth裝飾器,這樣才可以使用上。
OK,這樣你就算是將你之前寫的模擬博客園登錄的作業按照規范化目錄結構合理的完善完成了,最后還有一個關於README文檔的書寫。
關於README的內容
這個我覺得是每個項目都應該有的一個文件,目的是能簡要描述該項目的信息,讓讀者快速了解這個項目。
它需要說明以下幾個事項:
-
軟件定位,軟件的基本功能。
-
運行代碼的方法: 安裝環境、啟動命令等。
-
簡要的使用說明。
-
代碼目錄結構說明,更詳細點可以說明軟件的基本原理。
-
常見問題說明。
我覺得有以上幾點是比較好的一個README
。在軟件開發初期,由於開發過程中以上內容可能不明確或者發生變化,並不是一定要在一開始就將所有信息都補全。但是在項目完結的時候,是需要撰寫這樣的一個文檔的。
可以參考Redis源碼中Readme的寫法,這里面簡潔但是清晰的描述了Redis功能和源碼結構。