逐步提升程序质量的演变过程示例

原文出处: 水钰   

如何编写高质量的程序呢? 在《Web服务端软件的的服务品质概要》阐述了程序的常见质量属性及实现策略方法,本文将通过一个 Python 实现的图片文件批量重命名工具来演示如何逐步提升程序质量。

图片文件批量重命名工具实现的功能是:将指定目录 /home/user/path/to/photos/(xxx.png,yyy.png) 下的图片批量重命名为 prefix0001.png, prefix0002.png, …

 

雏形

首先,可以编写出一个基本可用的程序 batchrename_basic.py 。这个程序并不完美,但是可以完成最初的任务。注意到 生成编号使用了闭包,这是为了将生成编号的过程抽离出来成为一个可复用的过程,而这个过程无法预知需要生成怎样的列表,因此每次仅返回一个编号;程序如下:

 

 

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行业人员进入!


你可能感兴趣的:(逐步提升程序质量的演变过程示例)