Python基礎4
本節內容
1.叠代器 & 生成器
2.裝飾器
3.Json & pickle 數據序列化
4.軟件目錄結構規範
一、列表生成式, 叠代器 & 生成器
列表生成式
首先, 看列表[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 現在讓列表裏面的每個值都加1, 如何實現?
a = [0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9] b = [] for i in a:b.append(i+1) a = b >>>>>>>>a的結果: [1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 10]
a = [0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9] for index,i in enumerate(a): a[index]+=1 >>>>>>a 的結果為: [1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 10]
a = [0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9] a = map(lambda x:x+1, a) print(a) for i in a: print(i) >>>>>>輸出: <map object at 0x0000021EA94291D0> 1 2 3 4 5 6 7 8 8 9 10
還有種寫法如下:
a = [i + 1 for i in range (10)] print(a) >>>>>>輸出: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
上面的例子就叫做列表生成.
生成器
通過列表生成式,就可以直接創建一個列表。但是,受到內存限制,列表容量肯定是有限的。而且,創建一個包含100萬個元素的列表,不僅占用很大的內存存儲空間,如果我們僅僅需要訪問前面幾個元素,那後面絕大多數元素占用的空間都白白浪費了。
所以,如果列表元素可以按照某種算法推算出來,那我們是否可以在循環的過程中不斷推算出後續的元素呢?這樣就不必創建完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱為生成器(generator)。
要創建一個generator,有很多種方法。第一種方法很簡單,只要把一個列表生成式的[]
改成()
,就創建了一個generator:
a = [i + 1 for i in range (10)] >>>>>>>>>> b = (i + 1 for i in range (10)) print(b) >>>>>>>>>>輸出結果為: <generator object <genexpr> at 0x0000028FFC57FFC0>
創建a, b的區別僅在於最外層的[] 和(), a是一個list,b是一個generator.
可以直接打印出list a的元素, 那如何打印出生成器generator b裏面的元素呢?
如果想要一個個打印出來的話, 可以通過帶哦用next()函數來獲取下一個返回值:
>>> a = (x * x for x in range(10)) >>> next(a) 0 >>> next(a) 1 >>> next(a) 4 >>> next(a) 9 >>> next(a) 16 >>> next(a) 25 >>> next(a) 36 >>> next(a) 49 >>> next(a) 64 >>> next(a) 81 >>> next(a) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
generator保存的其實是算法,每次的調用next(a), 就計算出a的下一個值, 知道計算到最後一個元素, 沒有更多的元素時, 拋出StopIteration的報錯。
當然, 如上的不斷調用實在是太尷尬了, 正確的方法是使用for循環,因為generator也是可叠代對象:
>>> a = (x * x for x in range(10)) >>> for i in a: ... print(i) ... 0 1 4 9 16 25 36 49 64 81
創建一個生成器後, 一般不會調用next, 而是用for循環來叠代它, 並且不需要關心StopIteration
的錯誤。
generator非常強大。如果推算的算法比較復雜,用類似列表生成式的for
循環無法實現的時候,還可以用函數來實現。
比如,著名的斐波拉契數列,除了第一個和第二個數外,任意一個數都是前2個數的和:
1, 1, 2, 3, 5, 8, 13, 21, 34, ......
斐波拉契數列用列表生成式寫不出來, 但是用函數是可以解決的:
def fib(max): n, a, b = 0, 0, 1 while n < max : print(b) a, b = b, a + b n += 1 return ‘done‘ fib(10) >>>>>>> 1 1 2 3 5 8 13 21 34 55
註意上面的 a, b = b, a + b
並不是想象中的:
a = b b = a+b
而是相當於:
t = (b, a + b)# t是一個tuple a = t[0] b = t[1]
但不必顯示的寫出臨時變量t就可以賦值。
可以看出, fib函數實際上是定義了斐波拉契的推算規則, 從而得到後續元素, 跟generator的邏輯很相似。
其實上面的函數跟generator僅一步之遙。只需要把print(b)改成yield(b)就可以轉化了。
def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n +=1 return ‘done‘ f = fib(5) print(f)
輸出結果:<generator object fib at 0x01D87180>
generator跟函數的運行流程不一樣。韓式是順序執行,遇到return語句或者最後一行函數語句就返回。而變成生成器後通過next()調用,遇到yield語句返回,再次執行從上次返回的yield語句繼續執行。
f = fib(5) print(f) print(f.__next__()) print(f.__next__()) print("做點其他的事") print(f.__next__()) print(f.__next__()) <generator object fib at 0x00A27180> 1 1 做點其他的事 2 3
在上面的fib的例子中,我們在循環過程中不斷調用yield,就會不斷中斷。當然要給循環設計一個退出條件,不然會產生一個無限數列。
其實,同樣的, 函數改為generator後,我們也可以用for循環來叠代而不是next調用。
f = fib(5) for i in f: print(i) 1 1 2 3 5
調用for循環後,會發現無法得到函數返回值,如果想拿到它,必須捕獲StopIteration錯誤,返回值包含在StopIteration的value中:
f = fib(5) while True: try: x = next(f) print(‘F:‘,x) except StopIteration as e: print("Generator return value:", e.value) break 結果: F: 1 F: 1 F: 2 F: 3 F: 5 Generator return value: done
還可通過yield實現在單線程的情況下實現並發運算的效果
#!/user/bin/env python # -*- coding: utf-8 -*- import time def consumer(name): print("%s準備吃包子啦!" % name) while True: baozi = yield print("包子[%s]來了,被[%s]吃了!"%(baozi, name)) def producer(): c1 = consumer(‘Dandy‘) c2 = consumer(‘Claire‘) c1.__next__() c2.__next__() print(‘ 師傅開始準備做包子啦!‘) for i in range(10): time.sleep(1) print(‘做了一個包子,分2半。‘) c1.send(i) c2.send(i) producer()
Dandy準備吃包子啦! Claire準備吃包子啦! 師傅開始準備做包子啦! 做了一個包子,分2半。 包子[0]來了,被[Dandy]吃了! 包子[0]來了,被[Claire]吃了! 做了一個包子,分2半。 包子[1]來了,被[Dandy]吃了! 包子[1]來了,被[Claire]吃了! 做了一個包子,分2半。 包子[2]來了,被[Dandy]吃了! 包子[2]來了,被[Claire]吃了! 做了一個包子,分2半。 包子[3]來了,被[Dandy]吃了! 包子[3]來了,被[Claire]吃了!
叠代器
我們已經知道,可以直接作用於for銷魂還得額數據類型有以下幾種:一類是集合數據類型,入list, list, dict, set, str等。另一類是generator, 包括生成器和帶yield的generator function。
上述可以直接作用於for循環的對象,統一稱為可叠代對象:Iterable。
可以用isinstance()判斷一個對象是否是Iterable對象。
>>> from collections import Iterable >>> isinstance([],Iterable) True >>> isinstance({},Iterable) True >>> isinstance(‘abc‘,Iterable) True >>> isinstance((x for x in range(10)),Iterable) True >>> isinstance(100,Iterable) False
生成器都是Iterator對象,但是list,dict,str雖然是Iterable,但卻不是Iterator。
把list,dict,str等Iterable變成Iterator可以用iter()函數:
>>> isinstance(iter([]),Iterator) True >>> isinstance(iter(‘abc‘),Iterator) True >>>
你可能會困惑,為什麽list,dict,str等不是Iterator?
這是因為python的Iterator對象表示的是一個數據流,Iterator對象可以被next()函數調用並不斷返回下一個數據,直到沒有數據拋出StopIteration。可以把這個數據流看成是一個有序的序列,
但我們卻不能提前知道序列的長度,只能不斷的通過next()的調用實現計算出下一個數,所以Iterator的計算是惰性的,自由在需要返回下一個數據時才會計算。
Iterator甚至可以表示一個無限大的數據流,但是list等卻永遠不可能存儲整體的自然數。
小結
凡是可作用於for循環的對象都是Iterable類型(可叠代對象);
凡是可作用於next()函數的對象都是Iterator類型,它表示一個惰性計算的序列;
集合數據類型例如 list,dict,str等都是Iterable但不是Iterator,不過可以通過iter()函數獲得一個Iterator對象。
Python 的for循環其實本質上可以說,就是一個不斷的調用next()函數的過程,例如
for x in [1, 2, 3, 4]: pass
完全等價於:
# 首先獲得Iterator對象 it = iter([1, 2, 3, 4]) #循環 while True: try: #獲取下一個值: x = next(it) except StopIteration: #遇到StopIteration就退出循環 break
二、裝飾器
模擬一種情況,假如現在你手上的網站現在有以下幾個模塊
def home(): print("---首頁----") def america(): print("----歐美專區----") def japan(): print("----日韓專區----") def dalu(): print("----大陸專區----")
現在需要對這幾個受歡迎的版塊進行改版,比如大陸跟日韓專區需要收費,拿到這個需求後,先要讓其進行用戶認證,認證通過後再盤點是不是VIP,付費會員就可以觀看。需求其實很簡單,因為需要對多個板塊進行認證,就需要寫一個獨立並且通用的模塊,然後每個版塊調用它。
#!/user/bin/env python # -*- coding: utf-8 -*- user_status = False def login(): _username = ‘dandy‘ # 假裝是DB抓取的用戶信息 _password = ‘password.1‘ global user_status if user_status == False: username = input("user:") password = input("password:") if username == _username and password == _password: print("Welcome login...") user_status = True else: print("Wrong username or password !") else: print("用戶已登陸,驗證通過...") def home(): print("---首頁----") def america(): print("----歐美專區----") def japan(): login()#驗證 print("----日韓專區----") def dalu(): login()#驗證 print("----大陸專區----")
這時候你可能信心滿滿的去提交審核,沒想到幾分鐘就被打回來。雖然代碼是實現了功能,但是需要加認證在各個模塊代碼,這直接違反了軟件開發的一個原則“開放-封閉”,簡單來說,就是規定了已經實現的功能代碼不允許被修改,但可以擴展,即:
· 封閉:已實現的功能代碼
· 開放:對擴展開放
這時候我們可以利用高階函數,把一個函數當作一個參數傳給另外一個函數。這樣只需要寫一個認證方法,每次調用需要驗證的時候直接把功能的函數名當作參數傳給需要驗證的模塊,就可以實現功能了。
1 #!/user/bin/env python 2 # -*- coding: utf-8 -*- 3 4 user_status = False 5 6 7 def login(func): 8 _username = ‘dandy‘ # 假裝是DB抓取的用戶信息 9 _password = ‘password.1‘ 10 global user_status 11 12 if user_status == False: 13 username = input("user:") 14 password = input("password:") 15 16 if username == _username and password == _password: 17 print("Welcome login...") 18 user_status = True 19 else: 20 print("Wrong username or password !") 21 22 if user_status == True: 23 func()#只要驗證通過就會調用相應的功能模塊 24 25 26 def home(): 27 print("---首頁----") 28 29 30 def america(): 31 print("----歐美專區----") 32 33 34 def japan(): 35 # login()#驗證 36 print("----日韓專區----") 37 38 39 def dalu(): 40 # login()#驗證 41 print("----大陸專區----") 42 43 home() 44 login(america) 45 login(dalu)View Code
很開心,美滋滋。終於實現了老板的要求,不改變原功能代碼的前提下,給功能加上驗證。但是這時候經理看到後發現你又犯了大忌,什麽大忌?
改變了調用方式!現在所有需要認證的模塊都需要重新調用你的login()方法並把自己的函數名傳給你,別人之前可不是這麽調用的,如果有100個模塊這樣調用是不是100個模塊都要更改調用方式,可能模塊跟方法是分開封裝的,怎麽去找那些模塊呢?
現在讓我們提起來一個學過的知識:匿名函數(lambda)
def plus(n): return n+2 plus2 = lambda x:x+2
上面的2種寫法是不是代表同樣的意思?
給 lambda x:x+2 取了個名字叫plus2 , 是不是相當於def plus2(x)
給函數賦值變量名就像def func_name 是一樣的效果,如下面的plus(n)函數,你調用時可以用plus名,還可以再起個其他名字,如
calc = plus calc(n)
之前的驗證登陸的代碼:
home() login(japan) login(dalu)
之所以改變了調用方式,是因為用戶每次調用時需要執行login(參數), 可以稍微改一下。
home() japan= login(japan) dalu = login(dalu)
這樣調用dalu的時候,其實相當於調用了login(dalu),通過login裏面的驗證後自動調用dalu的功能。
那就應該這樣寫
home() japan= login(japan)#這裏其實替換了japan函數 dalu= login(dalu) #那用戶調用時依然寫 japan()
但是,問題在於內部已經有func(), japan= login(japan) 還沒等用戶調用,你就會內部執行這句的時候顯把america執行了。應該是用戶調用的時候再調用啊?
這裏我們又可以用到嵌套函數,想實現一開始寫 japan= login(japan) 不觸發你函數的執行只需要在login裏面再定義一層函數,第一次調用 japan= login(japan)值調用到外層的login,這個login雖然會執行但是不會觸發認證,因為認證的所有的代碼都被封裝在login裏層的新定義的函數裏, login只返回了裏層的函數名,這樣再執行japan()時,就會調用裏層的函數。
可能大家也有點暈了,直接上代碼:
def login(func): # 把要執行的模塊從這裏傳進來 def inner(): # 再定義一層函數 _username = ‘dandy‘ # 假裝是DB抓取的用戶信息 _password = ‘password.1‘ global user_status if user_status == False: username = input("user:") password = input("password:") if username == _username and password == _password: print("Welcome login...") user_status = True else: print("Wrong username or password !") if user_status == True: func() # 只要驗證通過就會調用相應的功能模塊 return inner # 用戶調用login的時候就會返回inner的內存地址,下次調用加上()才會執行inner函數
按照python對於裝飾器的定義,我們這時候可以把這個代碼去掉:
japan= login(japan) #你在這裏相當於把japan這個函數替換了
只需要在需要裝飾的函數上面加上下面的代碼:
def home(): print("---首頁----") def america(): print("----歐美專區----") @login def japan(): # login()#驗證 print("----日韓專區----") @login def dalu(): # login()#驗證 print("----大陸專區----")
效果一樣。太棒了。
你開心的看著這一串代碼,突然又想試試能不能傳參給版塊:
@login def japan(): # login()#驗證 print("----日韓專區----") japan("3p")
結果發現報錯了。難道這個裝飾器不能傳參數麽?
當然!如果不能傳參數,還介紹了幹嘛
def login(func): # 把要執行的模塊從這裏傳進來 def inner(arg1): # 再定義一層函數 _username = ‘dandy‘ # 假裝是DB抓取的用戶信息 _password = ‘password.1‘ global user_status if user_status == False: username = input("user:") password = input("password:") if username == _username and password == _password: print("Welcome login...") user_status = True else: print("Wrong username or password !") if user_status == True: func(arg1) # 只要驗證通過就會調用相應的功能模塊 return inner # 用戶調用login的時候就會返回inner的內存地址,下次調用加上()才會執行inner函數
我們給inner函數加了個arg1參數func的地方也加了個arg1,這樣調用會傳到相應的版塊。
再試一次呢?
def login(func): # 把要執行的模塊從這裏傳進來 def inner(arg1): # 再定義一層函數 _username = ‘dandy‘ # 假裝是DB抓取的用戶信息 _password = ‘password.1‘ global user_status if user_status == False: username = input("user:") password = input("password:") if username == _username and password == _password: print("Welcome login...") user_status = True else: print("Wrong username or password !") if user_status == True: func(arg1) # 只要驗證通過就會調用相應的功能模塊 return inner # 用戶調用login的時候就會返回inner的內存地址,下次調用加上()才會執行inner函數
就這樣我們完成了裝飾器與參數的調用。。。神奇吧。
下面貼上所有的代碼:
1 #!/user/bin/env python 2 # -*- coding: utf-8 -*- 3 4 user_status = False 5 6 7 def login(func): # 把要執行的模塊從這裏傳進來 8 9 def inner(*args, **kwargs): # 再定義一層函數 10 _username = ‘dandy‘ # 假裝是DB抓取的用戶信息 11 _password = ‘password.1‘ 12 global user_status 13 14 if user_status == False: 15 username = input("user:") 16 password = input("password:") 17 18 if username == _username and password == _password: 19 print("Welcome login...") 20 user_status = True 21 else: 22 print("Wrong username or password !") 23 24 if user_status == True: 25 func(*args, **kwargs) # 只要驗證通過就會調用相應的功能模塊 26 27 return inner # 用戶調用login的時候就會返回inner的內存地址,下次調用加上()才會執行inner函數 28 29 30 def home(): 31 print("---首頁----") 32 33 34 def america(): 35 print("----歐美專區----") 36 37 38 @login 39 def japan(style): 40 # login()#驗證 41 print("----日韓專區----") 42 43 44 @login 45 def dalu(): 46 # login()#驗證 47 print("----大陸專區----") 48 49 50 japan(‘3p‘)View Code
你很開心的帶著代碼去提交給經理。經理侃了也很滿意。
第二天,經理又提出了新的需求,需要允許用戶選擇用qq\weixin\weibo 認證。這時候對於你來說一切就沒那麽難了。
#!/user/bin/env python # -*- coding: utf-8 -*- user_status = False def login(auth_type): # 把要執行的模塊從這裏傳進來 def auth(func): def inner(*args, **kwargs): # 再定義一層函數 if auth_type == ‘qq‘: _username = ‘dandy‘ # 假裝是DB抓取的用戶信息 _password = ‘password.1‘ global user_status if user_status == False: username = input("user:") password = input("password:") if username == _username and password == _password: print("Welcome login...") user_status = True else: print("Wrong username or password !") if user_status == True: func(*args, **kwargs) # 只要驗證通過就會調用相應的功能模塊 else: print(‘wrong qq auth.‘) return inner # 用戶調用login的時候就會返回inner的內存地址,下次調用加上()才會執行inner函數 return auth def home(): print("---首頁----") def america(): print("----歐美專區----") @login(‘qq‘) def japan(style): # login()#驗證 print("----日韓專區----") @login(‘weixin‘) def dalu(): # login()#驗證 print("----大陸專區----") japan(‘3p‘) dalu()View Code
三、Json & Pickle 數據序列化
用於序列化的兩個模塊
· Json,用於字符串 和python數據類型間的轉換
· Pickle,用於python特有類型和python的數據類型間的轉換
Json模塊提供了四個功能:dumps、dump、loads、load
Pickle模塊提供了四個功能:dumps、dump、loads、load
其實Python對於Json所寫的封裝功能已經很完美了,基本沒什麽好講的了上代碼吧:
#!/user/bin/env python # -*- coding: utf-8 -*- import json dict_new = {‘name‘: ‘dandy‘, ‘age‘: 22, ‘salary‘: 30000 } with open("view2", ‘w‘, encoding=‘utf-8‘) as fs: json.dump(dict_new, fs)#寫入文件
我們看下文件
然後
with open(‘view2‘, ‘r‘, encoding=‘utf-8‘) as fs: dict_new = json.load(fs) print(dict_new) 結果為: {‘name‘: ‘dandy‘, ‘age‘: 22, ‘salary‘: 30000}
Pickle會加字符串轉換成二進制:
import pickle dict_new = {‘name‘: ‘dandy‘, ‘age‘: 22, ‘salary‘: 30000 } p_str = pickle.dumps(dict_new,) print(p_str) 結果為一串二進制數: b‘\x80\x03}q\x00(X\x04\x00\x00\x00nameq\x01X\x05\x00\x00\x00dandyq\x02X\x03\x00\x00\x00ageq\x03K\x16X\x06\x00\x00\x00salaryq\x04M0uu.‘ #寫入文件二進制 with open(‘view3‘,‘bw‘)as fp: pickle.dump(dict_new, fp) #讀取文件 with open(‘view3‘,‘br‘)as fp: dict_new = pickle.load(fp) print(dict_new) 結果: {‘name‘: ‘dandy‘, ‘age‘: 22, ‘salary‘: 30000}
四、軟件目錄結構規範
為什麽要設計好結構目錄?
"設計項目目錄結構",就和"代碼編碼風格"一樣,屬於個人風格問題。對於這種風格上的規範,一直都存在兩種態度:
- 一類同學認為,這種個人風格問題"無關緊要"。理由是能讓程序work就好,風格問題根本不是問題。
- 另一類同學認為,規範化能更好的控制程序結構,讓程序具有更高的可讀性。
我是比較偏向於後者的,因為我是前一類同學思想行為下的直接受害者。我曾經維護過一個非常不好讀的項目,其實現的邏輯並不復雜,但是卻耗費了我非常長的時間去理解它想表達的意思。從此我個人對於提高項目可讀性、可維護性的要求就很高了。"項目目錄結構"其實也是屬於"可讀性和可維護性"的範疇,我們設計一個層次清晰的目錄結構,就是為了達到以下兩點:
- 可讀性高: 不熟悉這個項目的代碼的人,一眼就能看懂目錄結構,知道程序啟動腳本是哪個,測試目錄在哪兒,配置文件在哪兒等等。從而非常快速的了解這個項目。
- 可維護性高: 定義好組織規則後,維護者就能很明確地知道,新增的哪個文件和代碼應該放在什麽目錄之下。這個好處是,隨著時間的推移,代碼/配置的規模增加,項目結構不會混亂,仍然能夠組織良好。
所以,我認為,保持一個層次清晰的目錄結構是有必要的。更何況組織一個良好的工程目錄,其實是一件很簡單的事兒。
目錄組織方式
關於如何組織一個較好的Python工程目錄結構,已經有一些得到了共識的目錄結構。在Stackoverflow的這個問題上,能看到大家對Python目錄結構的討論。
這裏面說的已經很好了,我也不打算重新造輪子列舉各種不同的方式,這裏面我說一下我的理解和體會。
假設你的項目名為foo, 我比較建議的最方便快捷目錄結構這樣就足夠了:
Foo/
|-- bin/
| |-- foo
|
|-- foo/
| |-- tests/
| | |-- __init__.py
| | |-- test_main.py
| |
| |-- __init__.py
| |-- main.py
|
|-- docs/
| |-- conf.py
| |-- abc.rst
|
|-- setup.py
|-- requirements.txt
|-- README
簡要解釋一下:
bin/
: 存放項目的一些可執行文件,當然你可以起名script/
之類的也行。foo/
: 存放項目的所有源代碼。(1) 源代碼中的所有模塊、包都應該放在此目錄。不要置於頂層目錄。(2) 其子目錄tests/
存放單元測試代碼; (3) 程序的入口最好命名為main.py
。docs/
: 存放一些文檔。setup.py
: 安裝、部署、打包的腳本。requirements.txt
: 存放軟件依賴的外部Python包列表。README
: 項目說明文件。
除此之外,有一些方案給出了更加多的內容。比如LICENSE.txt
,ChangeLog.txt
文件等,我沒有列在這裏,因為這些東西主要是項目開源的時候需要用到。如果你想寫一個開源軟件,目錄該如何組織,可以參考這篇文章。
下面,再簡單講一下我對這些目錄的理解和個人要求吧。
關於README的內容
這個我覺得是每個項目都應該有的一個文件,目的是能簡要描述該項目的信息,讓讀者快速了解這個項目。
它需要說明以下幾個事項:
- 軟件定位,軟件的基本功能。
- 運行代碼的方法: 安裝環境、啟動命令等。
- 簡要的使用說明。
- 代碼目錄結構說明,更詳細點可以說明軟件的基本原理。
- 常見問題說明。
我覺得有以上幾點是比較好的一個README
。在軟件開發初期,由於開發過程中以上內容可能不明確或者發生變化,並不是一定要在一開始就將所有信息都補全。但是在項目完結的時候,是需要撰寫這樣的一個文檔的。
可以參考Redis源碼中Readme的寫法,這裏面簡潔但是清晰的描述了Redis功能和源碼結構。
關於requirements.txt和setup.py
setup.py
一般來說,用setup.py
來管理代碼的打包、安裝、部署問題。業界標準的寫法是用Python流行的打包工具setuptools來管理這些事情。這種方式普遍應用於開源項目中。不過這裏的核心思想不是用標準化的工具來解決這些問題,而是說,一個項目一定要有一個安裝部署工具,能快速便捷的在一臺新機器上將環境裝好、代碼部署好和將程序運行起來。
這個我是踩過坑的。
我剛開始接觸Python寫項目的時候,安裝環境、部署代碼、運行程序這個過程全是手動完成,遇到過以下問題:
- 安裝環境時經常忘了最近又添加了一個新的Python包,結果一到線上運行,程序就出錯了。
- Python包的版本依賴問題,有時候我們程序中使用的是一個版本的Python包,但是官方的已經是最新的包了,通過手動安裝就可能裝錯了。
- 如果依賴的包很多的話,一個一個安裝這些依賴是很費時的事情。
- 新同學開始寫項目的時候,將程序跑起來非常麻煩,因為可能經常忘了要怎麽安裝各種依賴。
setup.py
可以將這些事情自動化起來,提高效率、減少出錯的概率。"復雜的東西自動化,能自動化的東西一定要自動化。"是一個非常好的習慣。
setuptools的文檔比較龐大,剛接觸的話,可能不太好找到切入點。學習技術的方式就是看他人是怎麽用的,可以參考一下Python的一個Web框架,flask是如何寫的: setup.py
當然,簡單點自己寫個安裝腳本(deploy.sh
)替代setup.py
也未嘗不可。
requirements.txt
這個文件存在的目的是:
- 方便開發者維護軟件的包依賴。將開發過程中新增的包添加進這個列表中,避免在
setup.py
安裝依賴時漏掉軟件包。 - 方便讀者明確項目使用了哪些Python包。
這個文件的格式是每一行包含一個包依賴的說明,通常是flask>=0.10
這種格式,要求是這個格式能被pip
識別,這樣就可以簡單的通過 pip install -r requirements.txt
來把所有Python包依賴都裝好了。具體格式說明: 點這裏。
關於配置文件的使用方法
註意,在上面的目錄結構中,沒有將conf.py
放在源碼目錄下,而是放在docs/
目錄下。
很多項目對配置文件的使用做法是:
- 配置文件寫在一個或多個python文件中,比如此處的conf.py。
- 項目中哪個模塊用到這個配置文件就直接通過
import conf
這種形式來在代碼中使用配置。
這種做法我不太贊同:
- 這讓單元測試變得困難(因為模塊內部依賴了外部配置)
- 另一方面配置文件作為用戶控制程序的接口,應當可以由用戶自由指定該文件的路徑。
- 程序組件可復用性太差,因為這種貫穿所有模塊的代碼硬編碼方式,使得大部分模塊都依賴
conf.py
這個文件。
所以,我認為配置的使用,更好的方式是,
- 模塊的配置都是可以靈活配置的,不受外部配置文件的影響。
- 程序的配置也是可以靈活控制的。
能夠佐證這個思想的是,用過nginx和mysql的同學都知道,nginx、mysql這些程序都可以自由的指定用戶配置。
所以,不應當在代碼中直接import conf
來使用配置文件。上面目錄結構中的conf.py
,是給出的一個配置樣例,不是在寫死在程序中直接引用的配置文件。可以通過給main.py
啟動參數指定配置路徑的方式來讓程序讀取配置內容。當然,這裏的conf.py
你可以換個類似的名字,比如settings.py
。或者你也可以使用其他格式的內容來編寫配置文件,比如settings.yaml
之類的。
附上一份作業:
作業需求:
模擬實現一個ATM + 購物商城程序
- 額度 15000或自定義
- 實現購物商城,買東西加入 購物車,調用信用卡接口結賬
- 可以提現,手續費5%
- 每月22號出賬單,每月10號為還款日,過期未還,按欠款總額 萬分之5 每日計息
- 支持多賬戶登錄
- 支持賬戶間轉賬
- 記錄每月日常消費流水
- 提供還款接口
- ATM記錄操作日誌
- 提供管理接口,包括添加賬戶、用戶額度,凍結賬戶等。。。
- 用戶認證用裝飾器
示例代碼 https://github.com/triaquae/py3_training/tree/master/atm
簡易流程圖:https://www.processon.com/view/link/589eb841e4b0999184934329
Python基礎4