如何编写高质量的程序呢? 在《Web服务端软件的的服务品质概要》阐述了程序的常见质量属性及实现策略方法,本文将通过一个 Python 实现的图片文件批量重命名工具来演示如何逐步提升程序质量。
图片文件批量重命名工具实现的功能是:将指定目录 /home/user/path/to/photos/(xxx.png,yyy.png) 下的图片批量重命名为 prefix0001.png, prefix0002.png, ...
雏形
首先,可以编写出一个基本可用的程序 batchrename_basic.py 。这个程序并不完美,但是可以完成最初的任务。注意到 生成编号使用了闭包,这是为了将生成编号的过程抽离出来成为一个可复用的过程,而这个过程无法预知需要生成怎样的列表,因此每次仅返回一个编号;程序如下:
# -*- 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在网络信号很差的地方会无法使用、需要配置文件的程序找不到指定的配置、通信连接中断、查询不到指定的数据、接收的参数不合法或非法等。此时,程序的行为是能够预估到这些情景并优雅处理和返回,还是嘎地中断,就体现了程序的健壮和编程者的素养。一般情况下,对于能够预估的很可能产生的情形,比如参数不合法、指定数据查询不到,可以通过条件来判断、识别和处理;而对于无法预料的情形,比如网络中断,就捕获异常处理。在此例中,需要指定一个目录路径。当目录路径不存在时,就会抛出异常:
Traceback (most recent call last): File "batchrename_basic.py", line 57, inbatchrename(dir_path, prefix="beauty_") File "batchrename_basic.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):
def getDirFiles(dir_path): try: return os.listdir(dir_path) except OSError, err: print 'No Such Directory: %s, exit.' % dir_path os._exit(1)
实际上,这种解法是滥用异常的例子。 异常应该是无法预料的情形才去捕获,确保万无一失。而指定目录路径不存在,完全是一个可以预料的情景。正确的做法是: 在调用 batchrename(dir_path, prefix="beauty_") 之前先判断路径 dir_path 是否存在且合法。 如果存在且合法,就进行后续的动作;否则退出程序。 不过话说回来,如果判断路径是否存在且合法的系统库调用会抛出异常,那么这里还是需要捕获异常。另外,为了代码更清晰些,有些开发者倾向于无论什么错误全部抛出异常,然后在高层的某个地方做统一处理。怎么处理错误是个仁者见仁智者见智的问题,但是捕获并处理错误是一个达成共识的做法。
可定制性
当程序上线后,遇到的回应可能是:是否可以根据客户的实际需要做特定的修改和处理,即可定制。
如果用户想指定路径和前缀,就必须在程序里修改并重新部署,显然是比较“僵硬”的。控制台程序通常要加上命令行参数,而实际应用则使用配置文件。下面通过使用 argparse 模块给该程序添加命令行参数,使之具备可定制性。添加一个 parseArgs 方法, 并修改 main 即可。注意到,使用了元组来清晰表达所希望返回的参数格式,便于主程序使用; 魔数均用字符串常量来表达,保证可维护性。
使用方式: $ python batchrename_robust_customized.py /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ -p fz2 -m NUM 1 5
-p, -m 都是可选的。默认只需要指定目录路径。
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))
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)
可追踪性
可追踪性体现了程序运行过程的可知性和可监控性。程序总是会潜藏或多或少的BUG。当用户数据出现问题需要排查时,有效的日志会是非常好的帮手。有效的日志通常指记录程序运行中的关键状态和关键路径。在此例中,要将文件重命名的具体信息记录下来,简便起见,程序中只是打印一下:
os.rename(old_filename,PathUtil.join(dir_path,newname)) print '%s rename to %s.' % (filename, newname) # should be info log
安全性
安全性是一个性质非常敏感的质量属性,很容易造成严重的故障, 不可不察。
安全性通常表达三层含义: 1. 程序绝对不能破坏用户的数据; 2. 某个用户的数据不能未经授权地被其他用户获取到; 3. 程序必须防止其它程序破坏用户数据或窥探用户隐私。其中第一条是不可触犯的。当我们重复运行 $ python batchrename_robust_customized.py /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ 时,会惊讶地发现,重命名后文件变少了!当运行足够次后,文件可能只剩下一个! 这是怎么回事呢? 运行若干次之后,截取一次结果如下:
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:
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) 获取文件列表: 根据给定目录获取目录下的所有文件名称列表;
(3) 获取文件后缀: 根据文件名称获取文件后缀;
(4) 生成编号: 根据指定编号生成方式,生成符合条件的重命名编号;
(5) 批量重命名: 将旧的文件名重命名为新的重命名名称。
此例正是遵循可复用性原则来编写程序,使得每次改动仅涉及一小部分。
可移植性
写程序是为了更好更广泛地使用。可移植性需要:1. 检测操作平台; 2. 将特定操作系统的符号和特定操作系统的行为替换成平台无关的。在本例中,要将路径分隔符 / 修改为 os.sep. Windows下的使用方式: D:\>python batchrename.py -d F:\pic\fuzhuang\fz2 -p fz2 -m NUM 1 6
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
# -*- 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)
易用性
易用性的讨论可参见文章 《程序与软件的易用性》。在本例的命令行程序中,适用的法则是: 1. 若用户输入 -h, --help, 展示该程序的具体用法和选项;2. 若用户直接输入程序名称,那么提示用户必须输入目录名称,并提示该程序的具体用法和选项。
性能成本
程序员有追求高效的强迫症。想象这是一个 web 服务, 性能成本通常体现在响应速度和吞吐量。响应速度是用户可感知的,影响到用户体验;吞吐量是用户不可感知的,影响到服务成本。此例中可以考虑百万个文件的重命名;影响效率的因素有两个: 1. 文件名排序时间; 2. rename 系统调用时间。对于前者,使用快速排序,或者使用更精细的方法在 batchrename 函数中解决 os.rename 默认覆盖已存在文件的问题(这样会降低可维护性); 对于后者,如果编程平台或系统调用提供了更高效的批量重命名接口,则可批量调用该接口来完成任务。
结语
提高程序质量并非一蹴而就,而是可以通过渐进的方式来实现。当实现了一个基本可用的程序时,还处于一个起点,有必要问问自己:
1. 健壮性: 程序需要怎样的运行环境和输入参数? 如果运行环境不满足或输入参数不合法,程序该如何应对?
2. 可定制性: 程序有哪些参数或特性是可定制的? 切忌在代码里写死;
3. 可追踪性: 程序有哪些关键运行状态和关键运行路径? 使用 info 和 error 日志记录下来;
4. 安全性: 程序在何种情况下可能破坏用户的数据? 程序如何禁止非法程序破坏或窥探用户数据?
5. 可复用性: 模块划分是否正交清晰? 函数方法的实现是否臃肿,可以从中抽离出可复用的子过程?
6. 可扩展性: 程序可能有哪些变化的潜在合理的需求?
7. 可测试性: 关键函数和方法是否有充分的单元测试?
8. 易用性: 程序是否容易使用,能让用户迅速理解和正确使用?
9. 性能成本: 响应速度是否在用户接受范围内?是否可以在不降低可维护性的前提下优化局部,提高整体吞吐量? 对于大数据量,程序是否可以应对? 程序的吞吐量极限是多少?
这九大质量属性是程序上线后能成为有价值的工具和产品服务所应当努力追求的。当然,很难达到所有质量属性的完善程度。在软件发展的各个阶段,始终面临着各个质量属性的权衡。
最初上线: 覆盖基本场景的健壮性 + 可接受的性能成本 + 基本的安全性 + 基本的可追踪性 + 符合大众习惯的易用性 + 基本的可复用性 + 基本的可扩展性 + 基本的测试
成长: 从产品上优化易用性 + 从业务发展和功能设计上提升可扩展性 + 覆盖更多场景的健壮性 + 适当的测试提升 + 安全性提升
发展: 更优的可扩展性 + 更优的性能成本 + 兼容原有功能设计与体验 + 提升测试效率和质量 + 安全性提升
成熟: 高度的安全性 + 更优的性能 + 完善的可运维可监控 + 适度的可扩展性 + 完善的测试链
转载请注明出处。谢谢 :)