Python的高級應用(一)
本章內容:
- 內置函數
- 生成器
- 迭代器
- 裝飾器
- JSON和PICKLE的簡單用法
- 軟件目錄結構規范
一、內置函數
1、數學運算類
abs(x) | 求絕對值 1、參數可以是整型,也可以是復數 2、若參數是復數,則返回復數的模 |
complex([real[, imag]]) | 創建一個復數 |
divmod(a, b) | 分別取商和余數 注意:整型、浮點型都可以 |
float([x]) | 將一個字符串或數轉換為浮點數。如果無參數將返回0.0 |
int([x[, base]]) | 將一個字符轉換為int類型,base表示進制 |
long([x[, base]]) | 將一個字符轉換為long類型 |
pow(x, y[, z]) | 返回x的y次冪 |
range([start], stop[, step]) | 產生一個序列,默認從0開始 |
round(x[, n]) | 四舍五入 |
sum(iterable[, start]) | 對集合求和 |
oct(x) | 將一個數字轉化為8進制 |
hex(x) | 將整數x轉換為16進制字符串 |
chr(i) | 返回整數i對應的ASCII字符 |
bin(x) | 將整數x轉換為二進制字符串 |
bool([x]) | 將x轉換為Boolean類型 |
2、集合類操作
basestring() | str和unicode的超類 不能直接調用,可以用作isinstance判斷 |
format(value [, format_spec]) | 格式化輸出字符串 格式化的參數順序從0開始,如“I am {0},I like {1}” |
unichr(i) | 返回給定int類型的unicode |
enumerate(sequence [, start = 0]) | 返回一個可枚舉的對象,該對象的next()方法將返回一個tuple |
iter(o[, sentinel]) | 生成一個對象的迭代器,第二個參數表示分隔符 |
max(iterable[, args...][key]) | 返回集合中的最大值 |
min(iterable[, args...][key]) | 返回集合中的最小值 |
dict([arg]) | 創建數據字典 |
list([iterable]) | 將一個集合類轉換為另外一個集合類 |
set() | set對象實例化 |
frozenset([iterable]) | 產生一個不可變的set |
str([object]) | 轉換為string類型 |
sorted(iterable[, cmp[, key[, reverse]]]) | 隊集合排序 |
tuple([iterable]) | 生成一個tuple類型 |
xrange([start], stop[, step]) | xrange()函數與range()類似,但xrnage()並不創建列表,而是返回一個xrange對象,它的行為與列表相似,但是只在需要時才計算列表值,當列表很大時,這個特性能為我們節省內存 |
3、邏輯判斷
all(iterable) | 1、集合中的元素都為真的時候為真 2、特別的,若為空串返回為True |
any(iterable) | 1、集合中的元素有一個為真的時候為真 2、特別的,若為空串返回為False |
cmp(x, y) | 如果x < y ,返回負數;x == y, 返回0;x > y,返回正數 |
4、反射
callable(object) | 檢查對象object是否可調用 1、類是可以被調用的 2、實例是不可以被調用的,除非類中聲明了__call__方法 |
classmethod() | 1、注解,用來說明這個方式是個類方法 2、類方法即可被類調用,也可以被實例調用 3、類方法類似於Java中的static方法 4、類方法中不需要有self參數 |
compile(source, filename, mode[, flags[, dont_inherit]]) | 將source編譯為代碼或者AST對象。代碼對象能夠通過exec語句來執行或者eval()進行求值。 1、參數source:字符串或者AST(Abstract Syntax Trees)對象。 2、參數 filename:代碼文件名稱,如果不是從文件讀取代碼則傳遞一些可辨認的值。 3、參數model:指定編譯代碼的種類。可以指定為 ‘exec’,’eval’,’single’。 4、參數flag和dont_inherit:這兩個參數暫不介紹 |
dir([object]) | 1、不帶參數時,返回當前范圍內的變量、方法和定義的類型列表; 2、帶參數時,返回參數的屬性、方法列表。 3、如果參數包含方法__dir__(),該方法將被調用。當參數為實例時。 4、如果參數不包含__dir__(),該方法將最大限度地收集參數信息 |
delattr(object, name) | 刪除object對象名為name的屬性 |
eval(expression [, globals [, locals]]) | 計算表達式expression的值 |
execfile(filename [, globals [, locals]]) | 用法類似exec(),不同的是execfile的參數filename為文件名,而exec的參數為字符串。 |
filter(function, iterable) | 構造一個序列,等價於[ item for item in iterable if function(item)] 1、參數function:返回值為True或False的函數,可以為None 2、參數iterable:序列或可迭代對象 |
getattr(object, name [, defalut]) | 獲取一個類的屬性 |
globals() | 返回一個描述當前全局符號表的字典 |
hasattr(object, name) | 判斷對象object是否包含名為name的特性 |
hash(object) | 如果對象object為哈希表類型,返回對象object的哈希值 |
id(object) | 返回對象的唯一標識 |
isinstance(object, classinfo) | 判斷object是否是class的實例 |
issubclass(class, classinfo) | 判斷是否是子類 |
len(s) | 返回集合長度 |
locals() | 返回當前的變量列表 |
map(function, iterable, ...) | 遍歷每個元素,執行function操作 |
memoryview(obj) | 返回一個內存鏡像類型的對象 |
next(iterator[, default]) | 類似於iterator.next() |
object() | 基類 |
property([fget[, fset[, fdel[, doc]]]]) | 屬性訪問的包裝類,設置后可以通過c.x=value等來訪問setter和getter |
reduce(function, iterable[, initializer]) | 合並操作,從第一個開始是前兩個參數,然后是前兩個的結果與第三個合並進行處理,以此類推 |
reload(module) | 重新加載模塊 |
setattr(object, name, value) | 設置屬性值 |
repr(object) | 將一個對象變幻為可打印的格式 |
slice() | |
staticmethod | 聲明靜態方法,是個注解 |
super(type[, object-or-type]) | 引用父類 |
type(object) | 返回該object的類型 |
vars([object]) | 返回對象的變量,若無參數與dict()方法類似 |
bytearray([source [, encoding [, errors]]]) | 返回一個byte數組 1、如果source為整數,則返回一個長度為source的初始化數組; 2、如果source為字符串,則按照指定的encoding將字符串轉換為字節序列; 3、如果source為可迭代類型,則元素必須為[0 ,255]中的整數; 4、如果source為與buffer接口一致的對象,則此對象也可以被用於初始化bytearray. |
zip([iterable, ...]) | 實在是沒有看懂,只是看到了矩陣的變幻方面 |
5、IO操作
file(filename [, mode [, bufsize]]) | file類型的構造函數,作用為打開一個文件,如果文件不存在且mode為寫或追加時,文件將被創建。添加‘b’到mode參數中,將對文件以二進制形式操作。添加‘+’到mode參數中,將允許對文件同時進行讀寫操作 1、參數filename:文件名稱。 2、參數mode:'r'(讀)、'w'(寫)、'a'(追加)。 3、參數bufsize:如果為0表示不進行緩沖,如果為1表示進行行緩沖,如果是一個大於1的數表示緩沖區的大小 。 |
input([prompt]) | 獲取用戶輸入 推薦使用raw_input,因為該函數將不會捕獲用戶的錯誤輸入 |
open(name[, mode[, buffering]]) | 打開文件 與file有什么不同?推薦使用open |
打印函數 | |
raw_input([prompt]) | 設置輸入,輸入都是作為字符串處理 |
二、生成器
通過列表生成式,我們可以直接創建一個列表。但是,受到內存限制,列表容量肯定是有限的。而且,創建一個包含100萬個元素的列表,不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數元素占用的空間都白白浪費了。所以,如果列表元素可以按照某種算法推算出來,那我們是否可以在循環的過程中不斷推算出后續的元素呢?這樣就不必創建完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱為生成器(Generator)。
創建一個簡單生成器
>>> L = [x * x for x in range(10)] >>> L [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> g = (x * x for x in range(10)) >>> g <generator object <genexpr> at 0x104feab40>
創建L和g的區別僅在於最外層的[]和(),L是一個list,而g是一個generator。
我們可以直接打印出list的每一個元素,但是如果我們要一個一個打印出生成器的內容,就需要通過generator的next()方法:
>>> g.next() 0 >>> g.next() 1 >>> g.next() 4 >>> g.next() 9 >>> g.next() 16 >>> g.next() 25 >>> g.next() 36 >>> g.next() 49 >>> g.next() 64 >>> g.next() 81 >>> g.next() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
通過上面的用法我們看到雖然用next方法可以一個一個輸出生成器內容,但是非常笨拙,所以我們一般用for循環來輸出生成器。
>>> g = (x * x for x in range(10)) >>> for n in g: ... print(n) ... 0 1 4 9 16 25 36 49 64 81
上面的例子只是一個實現簡單功能的生成器,如果我們實現一個復雜功能的生成器的話,就要用到函數了,我們如果把一個函數編程生成器呢,這里我們就引入了yield語句。
我們都知道著名的斐波那契數列(1,1,2,3,5,8......),形成原理就是從1,1開始,第三個數就是前兩個數的和,一次類推。
我們如何用生成器來實現斐波那契數列呢?
def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1
return "done"
這就是定義generator的另一種方法。如果一個函數定義中包含yield關鍵字,那么這個函數就不再是一個普通函數,而是一個generator:
>>> fib(6) <generator object fib at 0x104feaaa0>
這里,最難理解的就是generator和函數的執行流程不一樣。函數是順序執行,遇到return語句或者最后一行函數語句就返回。而變成generator的函數,在每次調用next()的時候執行,遇到yield語句返回,再次執行時從上次返回的yield語句處繼續執行。
執行結果:
D:\python3.5\python.exe E:/learn_python/day04/gen.py 1 1 2 3 5 8 13 21
我們發現最后會生成一個異常,而不返回正常的返回值,這里我需要用異常捕獲指令,try...except
>>> fib(5) <generator object fib at 0x02F8E180> >>> f = fib(5) >>> while True: try: x = next(f) print('g:', x) except StopIteration as e: print('Generator return value:', e.value) break g: 1 g: 1 g: 2 g: 3 g: 5 Generator return value: done
生成器的高級應用,單線程並發小程序:
#!/usr/bin/env python # -*- coding:utf-8 -*- #_*_coding:utf-8_*_ __author__ = 'Alex Li' import time def consumer(name): print("%s 准備吃包子啦!" %name) while True: baozi = yield print("包子[%s]來了,被[%s]吃了!" %(baozi,name)) def producer(name): c = consumer('A') c2 = consumer('B') c.__next__() c2.__next__() print("老子開始准備做包子啦!") for i in range(10): time.sleep(1) print("做了2個包子!") c.send(i) c2.send(i) producer("alex")
其中的send方法會把參數傳回生成器,並從生成器暫停的地方繼續執行。結果如下:
A 准備吃包子啦! B 准備吃包子啦! 老子開始准備做包子啦! 做了2個包子! 包子[0]來了,被[A]吃了! 包子[0]來了,被[B]吃了! 做了2個包子! 包子[1]來了,被[A]吃了! 包子[1]來了,被[B]吃了! 做了2個包子! 包子[2]來了,被[A]吃了! 包子[2]來了,被[B]吃了! 做了2個包子! 包子[3]來了,被[A]吃了! 包子[3]來了,被[B]吃了! 做了2個包子! 包子[4]來了,被[A]吃了! 包子[4]來了,被[B]吃了! 做了2個包子! 包子[5]來了,被[A]吃了! 包子[5]來了,被[B]吃了! 做了2個包子! 包子[6]來了,被[A]吃了! 包子[6]來了,被[B]吃了! 做了2個包子! 包子[7]來了,被[A]吃了! 包子[7]來了,被[B]吃了! 做了2個包子! 包子[8]來了,被[A]吃了! 包子[8]來了,被[B]吃了! 做了2個包子! 包子[9]來了,被[A]吃了! 包子[9]來了,被[B]吃了! Process finished with exit code 0
三、迭代器
迭代器是訪問集合內元素的一種方式。迭代器對象從集合的第一個元素開始訪問,直到所有的元素都被訪問一遍后結束。
迭代器不能回退,只能往前進行迭代。這並不是什么很大的缺點,因為人們幾乎不需要在迭代途中進行回退操作。
迭代器也不是線程安全的,在多線程環境中對可變集合使用迭代器是一個危險的操作。但如果小心謹慎,或者干脆貫徹函數式思想堅持使用不可變的集合,那這也不是什么大問題。
對於原生支持隨機訪問的數據結構(如tuple、list),迭代器和經典for循環的索引訪問相比並無優勢,反而丟失了索引值(可以使用內建函數enumerate()找回這個索引值,這是后話)。但對於無法隨機訪問的數據結構(比如set)而言,迭代器是唯一的訪問元素的方式。
迭代器的另一個優點就是它不要求你事先准備好整個迭代過程中所有的元素。迭代器僅僅在迭代至某個元素時才計算該元素,而在這之前或之后,元素可以不存在或者被銷毀。這個特點使得它特別適合用於遍歷一些巨大的或是無限的集合,比如幾個G的文件,或是斐波那契數列等等。這個特點被稱為延遲計算或惰性求值(Lazy evaluation)。
迭代器更大的功勞是提供了一個統一的訪問集合的接口。只要是實現了__iter__()方法的對象,就可以使用迭代器進行訪問。
一個對象,物理或者虛擬存儲的序列。list,tuple,strins,dicttionary,set以及生成器對象都是可迭代的,整型數是不可迭代的。
我們可以使用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
Python的Iterator
對象表示的是一個數據流,Iterator對象可以被next()
函數調用並不斷返回下一個數據,直到沒有數據時拋出StopIteration
錯誤。可以把這個數據流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過next()
函數實現按需計算下一個數據,所以Iterator
的計算是惰性的,只有在需要返回下一個數據時它才會計算。所以雖然string,list,dic等可以被for循環是可迭代的,但是他們並不能稱作迭代器。
實際上,string,list,dic等類型數據被for循環時,在底層也就是一個不斷調用next方法的過程,比如
for x in [1, 2, 3, 4, 5]: pass
等價於
# 首先獲得Iterator對象: it = iter([1, 2, 3, 4, 5]) # 循環: while True: try: # 獲得下一個值: x = next(it) except StopIteration: # 遇到StopIteration就退出循環 break
四、裝飾器
裝飾器簡單來講就是在不改變原函數的情況下為該函數新增功能,具體怎么實現的呢?
前置知識一:高階函數
我們知道一個函數都有一個函數名,在Python中這個函數名是可以當做一個參數被另一個函數調用的,而這個調用別的函數名做參數的函數就稱作高階函數。
舉個高階函數最經典的栗子:
def add(x,y,f): return f(x) + f(y) res = add(3,-6,abs) print(res)
在上面的例子中,函數f()就被函數add()當做參數調用了。
前置知識二:函數的嵌套
Python語言允許在定義函數的時候,其函數體內又包含另外一個函數的完整定義,這就是我們通常所說的嵌套定義,如下:
def foo(): #定義函數foo(), m=3 #定義變量m=3; def bar(): #在foo內定義函數bar() n=4 #定義局部變量n=4 print m+n #m相當於函數bar()的全局變量 bar()
在我們了解了上面兩個函數類型后就可以說說這個裝飾(逼)器是個什么鬼。。。
現在我們有一個現成的函數:
def f(): print '來裝飾我吧!!'
如果我們想在不改變函數f()的本體結構的情況下給他增加功能,我們怎么做?
我們先做一個裝飾器函數
def deco(func): def inner(): print("我是裝飾器!!") return func() return inner
我們可以看到,這個裝飾器函數deco(func)的參數就是給要被調用的函數名准備的參數,里面同時又內嵌了函數。那么這個裝飾器怎么用呢?
@deco def f(): print("來裝飾我吧!!")
這樣一用就把f()函數給裝飾了,結果是什么樣的呢?
#!/usr/bin/env python # -*- coding:utf-8 -*- def deco(func): def inner(): print("我是裝飾器!!") return func() return inner @deco def f(): print("來裝飾我吧!!") f() ###################################### D:\python3.5\python.exe E:/learn_python/day04/deco.py 我是裝飾器!! 來裝飾我吧!!
我們發現經過裝飾器裝飾后,直接執行f()出的結果包含了裝飾器里的語句,是不是很神奇?那這里面的具體流程是怎么樣的呢,f()的功能是怎么被改變的呢?
1、首先會執行載入裝飾器函數,同時把原函數作為參數調入裝飾器中
2、將執行完的裝飾器deco(func)的返回值inner()賦給@deco下面的函數,這樣就給f()函數賦予了新的功能。之后直接調用f()就可以運行裝飾后的新的功能。
被裝飾的函數帶參數的情況下,怎么寫裝飾器?
def deco(func): def inner(arg1,arg2,arg3): #新增功能 return func(arg1,arg2,arg3) return inner @deco def f(arg1,arg2,arg3): print 'f1'
處理n個參數的函數,並被多個裝飾器裝飾:
def deco1(func): def inner(*args,**kwargs): # 新增功能 return func(*args,**kwargs) return inner def deco2(func): def inner(*args,**kwargs): # 新增功能 return func(*args,**kwargs) return inner @deco1 @deco2 def f(arg1,arg2,arg3): print 'f1'
五、JSON和PICKLE的簡單用法
用於序列化的兩個模塊
- json,用於字符串 和 python數據類型間進行轉換
- pickle,用於python特有的類型 和 python的數據類型間進行轉換
Json模塊提供了四個功能:dumps、dump(序列化,存)、loads(反序列化,讀)、load
pickle模塊提供了四個功能:dumps、dump(序列化,存)、loads(反序列化,讀)、load (不僅可以序列化字典,列表...還可以把一個程序,一個類給序列化掉)
import pickle data = {'k1':123, 'k2':123} #dumps可以將數據類型轉換成只有python才認識的字符串 p_str = pickle.dumps(data) print(p_str)
結果
b'\x80\x03}q\x00(X\x02\x00\x00\x00k1q\x01K{X\x02\x00\x00\x00k2q\x02K{u.'
將數據轉換成只有Python認識的字符串,並寫入文件:
import pickle data = {'k1':123, 'k2':123} #打開文件,然后將data寫入 with open('data.pkl', 'wb') as f: pickle.dump(data, f) #同樣讀取的時候也需要打開文件 with open('data.pkl', 'rb') as f: data_1 = pickle.load(f) print(data_1)
結果
{'k1': 123, 'k2': 123}
json的用法和pickle是一樣的
import json data = {'k1':123, 'k2':123} p_str = json.dumps(data) print(p_str, type(p_str))
結果
{"k1": 123, "k2": 123} <class 'str'>
我們看到結果看起來是個字典,其實是字符串型的,因為json只能是字符串型。
import json data = {'k1':123, 'k2':123} #打開文件,然后將data寫入 with open('data.pkl', 'w') as f: json.dump(data, f) #同樣讀取的時候也需要打開文件 with open('data.pkl', 'r') as f: data_1 = json.load(f) print(data_1, type(data_1))
上面這段代碼,是寫入文件又讀取出來。看看執行結果
{'k2': 123, 'k1': 123} <class 'dict'>
那pickle和json有什么區別呢?
在上面兩段代碼中,pickle寫入和讀取文件時,用的是 ‘b’模式,而json沒有。
json是可以在不同語言之間交換數據的,而pickle只在python之間使用。
json只能序列化最基本的數據類型,而pickle可以序列化所有的數據類型,包括類,函數都可以序列化。
六、軟件目錄結構規范
為什么要設計好目錄結構?
"設計項目目錄結構",就和"代碼編碼風格"一樣,屬於個人風格問題。對於這種風格上的規范,一直都存在兩種態度:
- 一類同學認為,這種個人風格問題"無關緊要"。理由是能讓程序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
之類的。
參考文獻:http://www.cnblogs.com/alex3714/articles/5765046.html