原文出處: 水鈺 html
如何編寫高質量的程序呢? 在《Web服務端軟件的的服務品質概要》闡述了程序的常見質量屬性及實現策略方法,本文將經過一個 Python 實現的圖片文件批量重命名工具來演示如何逐步提高程序質量。python
圖片文件批量重命名工具實現的功能是:將指定目錄 /home/user/path/to/photos/(xxx.png,yyy.png) 下的圖片批量重命名爲 prefix0001.png, prefix0002.png, …jquery
雛形程序員
首先,能夠編寫出一個基本可用的程序 batchrename_basic.py 。這個程序並不完美,可是能夠完成最初的任務。注意到 生成編號使用了閉包,這是爲了將生成編號的過程抽離出來成爲一個可複用的過程,而這個過程沒法預知須要生成怎樣的列表,所以每次僅返回一個編號;程序以下:web
1編程 2安全 3網絡 4閉包 5架構 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# -*- coding: cp936 -*- import os import os.path as PathUtil
def createDesignator(num, bits): return str(num).zfill(bits)
def number_generator(start_num=0, bits=4): start = [] start.append(start_num) def inner(): start[0] = start[0] + 1 return createDesignator(start[0], bits) return inner
def batchrename(dir_path, prefix="IMG_",generator_func=number_generator()): ''' rename files (such as xxx.[jpg, png, etc]) in the directory specified by dir_path to [prefix][designator].[jpg, png, etc], designator is generated by generator_func ''' names = os.listdir(dir_path) for filename in names: old_filename = PathUtil.join(dir_path,filename) if PathUtil.isfile(old_filename)==True: newname=prefix.upper() + generator_func() + '.' + getFileSuffix(filename) os.rename(old_filename,PathUtil.join(dir_path,newname))
def getFileSuffix(filename): try: sep_ind = filename.index('.') return filename[sep_ind+1:] except ValueError: return None
def testGetFileSuffix(): assert getFileSuffix("good.jpg") == "jpg" assert getFileSuffix("good") is None print "testGetFileSuffix Passed."
def testNumberGenerator(): geneNums = [] generator = number_generator() for i in range(10): geneNums.append(generator()) assert geneNums[0] == '0001' assert geneNums[1] == '0002' assert geneNums[9] == '0010' print 'testNumberGenerator Passed.'
if __name__ == '__main__':
testGetFileSuffix() testNumberGenerator()
dir_path = '/home/lovesqcc/setupdir/scitools/pic/mmnet/beauty' batchrename(dir_path, prefix="beauty_") |
健壯性
健壯性體現了程序應對錯誤的能力。一個須要網絡鏈接的 APP 在網絡正常的狀況下運行流暢,若是沒有網絡呢? 就必須告知用戶先鏈接到網絡才行。或者採用輸入自動糾錯。好比在搜索引擎裏搜索 jquery, 不當心寫成了 jqeury 。搜索引擎會提示是否須要搜索的是 jquery。在此例中,當路徑不存在時,就會報錯。
1 2 3 4 5 6 |
Traceback (most recent call last): File "batchrename_robust.py", line 57, in batchrename(dir_path, prefix="beauty_") File "batchrename_robust.py", line 21, in batchrename names = os.listdir(dir_path) OSError: [Errno 2] No such file or directory: '/home/lovesqcc/setupdir/scitools/pic/mmnet/beauty' |
解決方法很簡單: 將 names = os.listdir(dir_path) 抽離出來,寫成一個函數並進行異常捕獲,而後該行改寫成 names = getDirFiles(dir_path):
1 2 3 4 5 6 |
def getDirFiles(dir_path): try: return os.listdir(dir_path) except OSError, err: print 'No Such Directory: %s, exit.' % dir_path os._exit(1) |
可定製性
若是用戶想指定路徑和前綴,就必須在程序裏修改並從新部署,顯然是比較「僵硬」的。控制檯程序一般要加上命令行參數,而實際應用則使用配置文件。下面經過使用 argparse 模塊給該程序添加命令行參數,使之具有可定製性。添加一個 parseArgs 方法, 並修改 main 便可。注意到,使用了元組來清晰表達所但願返回的參數格式,便於主程序使用; 魔數均用字符串常量來表達,保證可維護性。
使用方式: $ python batchrename_robust_customized.py /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ -p fz2 -m NUM 1 5
-p, -m 都是可選的。默認只須要指定目錄路徑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import argparse
DEFAULT_PREFIX = 'IMG_' DEFAULT_START_NUM = 1 DEFAULT_BITS = 4 NUM_METHOD = 'NUM'
def parseArgs(): description = 'This program is used to batch rename files in the given DIRECTORY to PREFIX_GeneratedDesignator. GeneratedDesignator is a BITS number counting from START_NUM to the number of files (etc. PREFIX0001,PREFIX0002,...) in the given DIRECTORY with leading zero if necessary.' parser = argparse.ArgumentParser(description=description) parser.add_argument('DIRECTORY', help='Given directory name is required') parser.add_argument('-p','--prefix',nargs='?', default="IMG_", help='Given renamed prefix') parser.add_argument('-m','--method',nargs='*',help='method to generate designator, etc --m [NUM [START_NUM [BITS]]]') args = parser.parse_args() dir_path = args.DIRECTORY
if args.prefix: prefix = args.prefix else: prefix = DEFAULT_PREFIX
if not args.method: method = NUM_METHOD start_num = DEFAULT_START_NUM bits = DEFAULT_BITS return (dir_path, prefix, (method, start_num, bits))
if type(args.method) == list: if len(args.method) == 0: method = NUM_METHOD start_num = DEFAULT_START_NUM bits = DEFAULT_BITS elif args.method[0] ==NUM_METHOD: method = NUM_METHOD if len(args.method) == 1: start_num = DEFAULT_START_NUM bits = DEFAULT_BITS elif len(args.method) == 2: start_num = int(args.method[1]) bits = DEFAULT_BITS elif len(args.method) == 3: start_num = int(args.method[1]) bits = int(args.method[2])
return (dir_path, prefix, (method, start_num-1, bits)) |
1 2 3 4 5 6 7 8 9 |
if __name__ == '__main__':
testGetFileSuffix() testNumberGenerator()
(dir_path, prefix, (method, start_num, bits)) = parseArgs() if method == NUM_METHOD: number_generator = number_generator(start_num, bits) batchrename(dir_path, prefix, number_generator) |
可追蹤性
可追蹤性體現了程序運行過程的可知性和可監控性。記錄程序運行中的關鍵狀態和關鍵路徑,也很是有利於出現錯誤時進行調試。在此例中,要將文件重命名的具體信息記錄下來,簡便起見,程序中只是打印一下:
1 2 |
os.rename(old_filename,PathUtil.join(dir_path,newname)) print '%s rename to %s.' % (filename, newname) # should be info log |
安全性
安全性一般表達兩層含義: 1. 程序絕對不能破壞用戶的數據; 2. 程序必須防止其它程序破壞用戶數據或窺探用戶隱私。其中第一條是不可觸犯的。當咱們重複運行 $ python batchrename_robust_customized.py /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ 時,會驚訝地發現,重命名後文件變少了!當運行足夠次後,文件可能只剩下一個! 這是怎麼回事呢? 運行若干次以後,截取一次結果以下:
1 2 3 4 5 |
IMG_0006.png rename to IMG_0002.png. IMG_0003.jpg rename to IMG_0003.jpg. IMG_0002.png rename to IMG_0004.png. IMG_0005.jpg rename to IMG_0005.jpg. IMG_0007.png rename to IMG_0006.png. |
稍做分析便可知道, Python os.rename 在 UnixSystem 上會默認覆蓋已存在的文件,而 os.listdir 輸出的結果是無序的! 解決方案也很簡單:先將 os.listdir 輸出的結果排序後再重命名,即要修改 getDirFiles:
1 2 3 4 5 6 7 8 |
def getDirFiles(dir_path): try: filenames = os.listdir(dir_path) filenames.sort() return filenames except OSError: print 'No Such Directory: %s, exit.' % dir_path os._exit(1) |
可複用性
可複用性的關鍵是單一職責原則和接口定義正交。單一職責原則指一個函數或方法僅作一件小事,望名知義;接口定義正交是說每一個函數、類接口定義的事情沒有重疊,能夠組合實現很是靈活的功能。若是程序具有較好的可複用性,那麼,在擴展程序時也會得到益處,將改動影響局部化。在編寫程序時應時時考慮抽離出可複用的過程和方法。可複用性也有助於編寫更有效的單元測試。此例中正是遵循可複用性原則來編寫程序,使得每次改動僅涉及一小部分。
可移植性
寫程序是爲了更好更普遍地使用。可移植性須要:1. 檢測操做平臺; 2. 將特定操做系統的符號和特定操做系統的行爲替換成平臺無關的。在本例中,要將路徑分隔符 / 修改成 os.sep. Windows下的使用方式: D:>python batchrename.py -d F:picfuzhuangfz2 -p fz2 -m NUM 1 6
1 |
batchrename(dir_path+ os.sep +filename, prefix, generator_func) |
可擴展性
可擴展性體現了程序應對需求變化的能力。對於此例,可擴展性體如今四點: 1. 要對目錄的子目錄遞歸重命名; 2. 要對多個目錄使用不一樣前綴進行批量重命名;3. 支持不一樣的編號生成方式;4. 對於非圖片文件的批量重命名。 對於第一點,只須要修改 batchrename 方法便可,檢測到若是是目錄,則遞歸調用 batchrename ; 對於第二點,則須要修改命令行參數格式,增長 -d 參數,參數個數至少一個;修改 -p 參數,參數可爲零到多個。若是給定目錄數大於給定前綴,則使用最後一個前綴將前綴數補足;若給定目錄數小於前綴數,則將從後數多餘的前綴忽略。要修改 parseArgs 和 main;對於第三點,則要將生成編號的方式抽離成可複用的過程,使得每次僅返回一個編號;對於第四點,因爲沒有對文件類型作判斷,所以也是適合於非圖片文件的。最終的程序以下所示, 使用方式:
$ python batchrename_robust_customized_extended.py -d /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz1 -p fz2 fz1 -m NUM 1 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# -*- coding: cp936 -*- import os import os.path as PathUtil import argparse
DEFAULT_PREFIX = 'IMG_' DEFAULT_START_NUM = 1 DEFAULT_BITS = 4 NUM_METHOD = 'NUM'
def parseArgs(): description = 'This program is used to batch rename files in the given DIRECTORY to PREFIX_GeneratedDesignator. GeneratedDesignator is a BITS number counting from START_NUM to the number of files (etc. PREFIX0001,PREFIX0002,...) in the given DIRECTORY with leading zero if necessary.' parser = argparse.ArgumentParser(description=description) parser.add_argument('-d','--directories', nargs='+', help='Given directory name is at least one required') parser.add_argument('-p','--prefix',nargs='*', help='Given renamed prefix') parser.add_argument('-m','--method',nargs='*',help='method to generate designator, etc --m [NUM [START_NUM [BITS]]]') args = parser.parse_args() dir_path_list = args.directories dir_num = len(args.directories)
if not args.prefix or len(args.prefix) == 0: prefix_list = [DEFAULT_PREFIX] * dir_num prefix_num = dir_num else: prefix_list = args.prefix prefix_num = len(args.prefix)
if prefix_num > dir_num: prefix_list = prefix_list[0:dir_num] else: prefix_list.extend([prefix_list[prefix_num-1]]*(dir_num-prefix_num))
if not args.method: method = NUM_METHOD start_num = DEFAULT_START_NUM bits = DEFAULT_BITS return (dir_path_list, prefix_list, (method, start_num, bits))
if type(args.method) == list: if len(args.method) == 0: method = NUM_METHOD start_num = DEFAULT_START_NUM bits = DEFAULT_BITS elif args.method[0] ==NUM_METHOD: method = NUM_METHOD if len(args.method) == 1: start_num = DEFAULT_START_NUM bits = DEFAULT_BITS elif len(args.method) == 2: start_num = int(args.method[1]) bits = DEFAULT_BITS elif len(args.method) == 3: start_num = int(args.method[1]) bits = int(args.method[2])
return (dir_path_list, prefix_list, (method, start_num-1, bits))
def createDesignator(num, bits): return str(num).zfill(bits)
def number_generator(start_num=0, bits=4): start = [] start.append(start_num) def inner(): start[0] = start[0] + 1 return createDesignator(start[0], bits) return inner
def getDirFiles(dir_path): try: filenames = os.listdir(dir_path) filenames.sort() return filenames except OSError: print 'No Such Directory: %s, exit.' % dir_path os._exit(1)
def batchrename(dir_path, prefix=DEFAULT_PREFIX ,generator_func=number_generator()): ''' rename files (such as xxx.[jpg, png, etc]) in the directory specified by dir_path to [prefix][designator].[jpg, png, etc], designator is generated by generator_func ''' names = getDirFiles(dir_path) for filename in names: old_filename = PathUtil.join(dir_path,filename) if PathUtil.isfile(old_filename)==True: newname=prefix.upper() + generator_func() + '.' + getFileSuffix(filename) os.rename(old_filename,PathUtil.join(dir_path,newname)) print '%s rename to %s.' % (filename, newname) # should be info log else: batchrename(dir_path+os.sep+filename, prefix, generator_func)
def getFileSuffix(filename): try: sep_ind = filename.index('.') return filename[sep_ind+1:] except ValueError: return None
def testGetFileSuffix(): assert getFileSuffix("good.jpg") == "jpg" assert getFileSuffix("good") is None print "testGetFileSuffix Passed."
def testNumberGenerator(): geneNums = [] generator = number_generator() for i in range(10): geneNums.append(generator()) assert geneNums[0] == '0001' assert geneNums[1] == '0002' assert geneNums[9] == '0010' print 'testNumberGenerator Passed.'
if __name__ == '__main__':
testGetFileSuffix() testNumberGenerator()
(dir_path_list, prefix_list, (method, start_num, bits)) = parseArgs()
dir_num = len(dir_path_list) for i in range(dir_num): if method == NUM_METHOD: number_generator_func = number_generator(start_num, bits) batchrename(dir_path_list[i], prefix_list[i], number_generator_func) |
性能成本
程序員有追求高效的強迫症。想象這是一個 web 服務, 性能成本一般體如今響應速度和吞吐量。響應速度是用戶可感知的,影響到用戶體驗;吞吐量是用戶不可感知的,影響到服務成本。此例中能夠考慮百萬個文件的重命名;影響效率的因素有兩個: 1. 文件名排序時間; 2. rename 系統調用時間。對於前者,使用快速排序,或者使用更精細的方法在 batchrename 函數中解決 os.rename 默認覆蓋已存在文件的問題(這樣會下降可維護性); 對於後者,若是編程平臺或系統調用提供了更高效的批量重命名接口,則可批量調用該接口來完成任務。
結語
提升程序質量並不是一蹴而就,而是能夠經過漸進的方式來實現。當實現了一個基本可用的程序時,還處於一個起點,有必要問問本身:
1. 健壯性: 程序須要怎樣的運行環境和輸入參數? 若是運行環境不知足或輸入參數不合法,程序該如何應對?
2. 可定製性: 程序有哪些參數或特性是可定製的? 切忌在代碼裏寫死;
3. 可追蹤性: 程序有哪些關鍵運行狀態和關鍵運行路徑? 使用 info 日誌記錄下來;
4. 安全性: 程序在何種狀況下可能破壞用戶的數據? 程序如何禁止非法程序破壞或窺探用戶數據?
5. 可擴展性: 程序可能有哪些變化的潛在合理的需求?
6. 可複用性: 函數方法是否臃腫,能夠從中抽離出可複用的子過程?
7. 可測試性: 關鍵函數和方法是否有充分的單元測試?
8. 性能成本: 響應速度是否在用戶接受範圍內?是否能夠在不下降可維護性的前提下優化局部,提升總體吞吐量? 對於大數據量,程序是否能夠應對? 程序的吞吐量極限是多少?
問啊-定製化IT教育平臺,牛人一對一服務,有問必答,開發編程社交頭條 官方網站:www.wenaaa.com
QQ羣290551701 彙集不少互聯網精英,技術總監,架構師,項目經理!開源技術研究,歡迎業內人士,大牛及新手有志於從事IT行業人員進入!