在用Python做Excel文件操作的时候,可能会需要对Excel表格做列的操作。这样就需要Excel列编号的序列即:A,B……AA,AB…..AAA,AAB…..。理论上来说这个序列是可以手工写死因为在EXCEL 2003中最大列数是256,EXCEL2007之后是1048576,是可以预先给定写死的。但这样ugly的方式实在不忍直视。决定还是编程实现自动生成。
一.测试先行,构建FT,让其Fail
import unittest
def create_excel_col():
return
class XlsCol(unittest.TestCase):
def test_xls_col_iter_1(self):
self.assertEquals(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),create_excel_col())
if __name__ == '__main__' :
XlsCol.run()
FT运行结果当然是测试失败,因为这里Excel列序号生成函数create_excel_col没有做任何实现:
AssertionError: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', [84 chars] 'Z'] != None
二.让用例在基本场景ugly Pass
我们分析一下,Excel列序号的最基本的情况就是24个英文字母,如果要求不要,create_excel_col直接返回24个英文字母组成的序列就大功告成了,于是代码和FT执行结果如下:
import unittest
def create_excel_col():
return list('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
class XlsCol(unittest.TestCase):
def test_xls_col_iter_1(self):
self.assertEquals(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),create_excel_col())
if __name__ == '__main__' :
XlsCol.run()
FT运行结果:
Ran 1 test in 0.001s
OK
如果需求简单,Excel存储数据量很小,按照简单设计原则,create_excel_col这个函数接口已经可以交付了。但现实并非如此,现实中要存放大量数据时,24个列往往是不够的,所以接口必须给出靠谱的实现。
三.让用例在基本场景真正的Pass
用数学的角度上来讲,EXCEL列可以看做从 24个英文字母中取[1-N]个字母的排列,并将排列得到的序列相加得到的序列。那么最基本的场景即从24个字母中取1个字母进行排列。Python的itertools库中提供了计算排列的接口,拿来用之,于是create_excel_col函数接口的实现就变成了:
from itertools import product
def create_excel_col():
return list(product(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), repeat =1))
这个时候FT运行还是失败的,因为create_excel_col函数接口返回的是元组的列表(这个和product接口的实现有关),FT与预期的是字符的列表。
AssertionError: Lists differ: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', '[83 chars] 'Z'] != [('A',), ('B',), ('C',), ('D',), ('E',), ([161 chars]Z',)]
First differing element 0:
'A'
('A',)
问题不大,我们只需要将元组转换为字符就行了,那么create_excel_col函数接口的实现就变成了:
from itertools import product
def create_excel_col():
lst = list(product(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), repeat =1))
for index in range(len(lst)):
lst[index] = ''.join(sorted(lst[index]))
return lst
FT通过:
Ran 1 test in 0.001s
OK
四.新增FT,实现复杂场景:
前面讲到了EXCEL列生成的原理,基本场景可以认为是24个字母取N个字母排列,并把排列结果相加的1次迭代,而复杂场景可以看做是迭代多次。2次是又多次的最基本情况,编写FT实现两次迭代的情况。
def create_excel_col():
lst = list(product(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), repeat =1)))
for index in range(len(lst)):
lst[index] = ''.join(sorted(lst[index]))
return lst
class XlsCol(unittest.TestCase):
def test_xls_col_iter_1(self):
self.assertEquals(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),create_excel_col())
def test_xls_col_iter_2(self):
self.assertEquals(['A', 'B', 'AA', 'AB', 'BA', 'BB'], create_excel_col(2))
FT的结果和预期一样,FAIL。
四.重构,让复杂场景PASS:
在这个步骤中将存在多次对create_excel_col的调试、重构和FT执行过程,直到当前的两个FT同时执行成功,避免啰嗦此处只呈现最终的结果
代码如下:
import unittest
from itertools import product
def create_excel_col(seed = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),iter_cnt =1):
col_lst = []
for index in range(1, iter_cnt + 1):
lst = list(product(seed, repeat = index)) #得到排列序元组序列
lst = map(lambda elem: ''.join(elem), lst) #将排列元组序列转成字符串序列
lst = list(set(lst)) #消除重复元素
lst = sorted(lst) #按字母ASCII的顺序进行排列
col_lst += lst
return col_lst
class XlsCol(unittest.TestCase):
def test_xls_col_iter_1(self):
self.assertEquals(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),create_excel_col())
def test_xls_col_iter_2(self):
self.assertEquals(['A', 'B', 'AA', 'AB', 'BA', 'BB'] create_excel_col(seed = ['A','B'],iter_cnt=2))
if __name__ == '__main__' :
XlsCol.run()
FT测试结果PASS
Ran 2 tests in 0.002s
OK
说明一下:
test_xls_col_iter_2用例,为了便于测试代码的编写,我将测试24个英文字母缩减到了2个,并将字母的序列作为计算最终Excel列的seed传递给create_excel_col接口。
五.确保FT PASS的情况下继续重构:
我们看到create_excel_col的实现还有优化空间,比如:冗余的中间变量col_lst 和lst。在函数式编程的思想中认为函数的中间变量越多,引入缺陷的风险就越大,应该尽可能的杜绝中间变量。但函数式编程的问题是过分追求代码紧凑,牺牲了代码的可读性,有时候有故意炫技装X的嫌疑,关键是找到一个平衡吧,所以create_excel_col最终重构成这样:
from fn import F
from itertools import product
from functools import reduce
def create_excel_col(seed = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),iter_cnt=2):
get_col_lst = F() << sorted << set << F(map,lambda _:''.join(_)) << product
return reduce(lambda col_lst, iter: col_lst + get_col_lst(seed,repeat = iter), range(1, iter_cnt + 1),[])
解读:
1.通过F定义列了一个处理序列get_col_lst ,序列之间传递的是列表:
先通过product生成当前迭代的排列序列(元组列表),将列表传递给F(map,lambda :”.join())将其转换为字符串列表,再讲列表传递给set函数进行消重,最后进行排序。这样整个过程就显得很紧凑,消除了中间变量。
2.reduce 也是函数式编程中常用函数,控制迭代过程:
每次迭代,将生成的列表累加到col_lst,最后返回。这样写的好处是紧凑,消除中间变量,将程序的循环结构编程顺序结构,降低复杂度。
3.适可而止:
代码写到这里,实际上还可以继续重构,将两行代码压缩到一行。适可而止吧,再压缩整个逻辑就显得不那么清晰了。单行代码太长也不cleancode,不利于理解。
六.继续增加FT验证更复杂的场景
新增FT验证迭代3次的场景:
>
class XlsCol(unittest.TestCase):
def test_xls_col_iter_1(self):
self.assertEquals(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'),create_excel_col(iter_cnt=1))
def test_xls_col_iter_2(self):
self.assertEquals(['A', 'B', 'AA', 'AB', 'BA', 'BB'], create_excel_col(seed = ['A','B'],iter_cnt=2))
def test_xls_col_iter_3(self):
self.assertEquals(['A', 'B', 'C', 'AA', 'AB', 'AC', 'BA', 'BB', 'BC', 'CA', 'CB', 'CC'],
create_excel_col(seed=['A', 'B', 'C'], iter_cnt=2))
self.assertEquals(['A', 'B', 'C', 'AA', 'AB', 'AC', 'BA', 'BB', 'BC', 'CA', 'CB', 'CC', 'AAA',
'AAB', 'AAC', 'ABA', 'ABB', 'ABC', 'ACA', 'ACB', 'ACC', 'BAA', 'BAB', 'BAC',
'BBA', 'BBB', 'BBC', 'BCA', 'BCB', 'BCC', 'CAA', 'CAB', 'CAC', 'CBA', 'CBB',
'CBC', 'CCA', 'CCB', 'CCC'],
create_excel_col(seed=['A', 'B', 'C'], iter_cnt=3))
FT测试结果为PASS
Ran 3 tests in 0.002s
OK
七.要继续测试吗?
任何测试都是有成本的,也是不可能穷尽的。从目前3个FT的规律上来看create_excel_col接口是正确的。从风险上来讲,24个英文字母迭代3次后产生的列序号范围足够当前使用,没有必要再继续测试下去。
八.总结体会:
1.TDD的过程中要保持好固有的节奏:
红灯->黄灯->绿灯->绿灯,也就是上文描述的从FT Fail ->FT ugly Pass(最简实现/假实现) -> Pass(实现) ->Pass(重构) ,按照这个规律持续迭代。
2.TDD的过程就像做数学题:
TDD的过程就像解数学题一样,逐步推导,最终得到自己的想要的结果,只不过TDD的结果是预知的。
3.函数式编程的度要把握好。
不要过度迷恋技巧而过多的丢失代码可读性,代码毕竟是要给除了你之外其他人看的。
4.TDD也是要讲测试策略的。
(这个话题可以后续继续讨论)