Python使用闭包结合配置自动生成函数

背景###

在构建测试用例集时,常常需要编写一些函数,这些函数接受基本相同的参数,仅有一个参数有所差异,并且处理模式也非常相同。可以使用Python闭包来定义模板函数,然后通过参数调节来自动化生产不同的函数。

示例###

看如下代码:

def commonGenerate(startTime, endTime, field, values):
    reqs = []
    for val in values:
        requestId = str(startTime) + "_" + str(endTime) + "_" + val
        baseReq = json.loads(baseExportReqStr)
        baseReq['osp']['start_time'] = startTime
        baseReq['osp']['end_time'] = endTime
        baseReq['osp'][field] = [val]
        baseReq['request_id'] = requestId
        reqs.append(json.dumps(baseReq))
    return reqs

def generateReqByState(startTime, endTime):
    states = ["S1", "S2", "S3", "S4", "S5", "S6"]
    return commonGenerate(startTime, endTime, 'state', states)

def generateReqByOrderType(startTime, endTime):
    orderTypes = ["T1", "T2", "T3"]
    return commonGenerate(startTime, endTime, 'type', orderTypes)

def generateReqByExpressType(startTime, endTime):
    expressTypes = ["E1", "E2", "E3"]
    return commonGenerate(startTime, endTime, 'expr_type', expressTypes)

def generateReqByFeedback(startTime, endTime):
    feedbacks = ["F1", "F2"]
    return commonGenerate(startTime, endTime, 'fb', feedbacks)

def getGenerateFuncs():
    gvars = globals()
    return [ gvars[var] for var in gvars if var.startswith('generateReq')  ]

caseGenerateFuncs = getGenerateFuncs()
print caseGenerateFuncs

这里已经抽离出通用函数 commonGenerate ,在此基础上定义了多个 generateReqByXXX ,这些函数的模式基本相同,无非是传一个字段名及值列表,然后生成一个不一样的函数。 那么,是否可以做成可配置化呢: 只要给定一个 map[字段名,值列表], 就能自动生成这些函数 ?

使用闭包可以达到这个目标。见如下代码所示:

def commonGenerator(startTime, endTime, field, values):
    def generateReqInner(startTime, endTime):
        reqs = []
        for val in values:
            requestId = str(startTime) + "_" + str(endTime) + "_" + val
            baseReq = json.loads(baseExportReqStr)
            baseReq['osp']['start_time'] = startTime
            baseReq['osp']['end_time'] = endTime
            baseReq['osp'][field] = [val]
            baseReq['request_id'] = requestId
            reqs.append(json.dumps(baseReq))
        return reqs
    return generateReqInner

def generateGenerators(startTime, endTime, configs):
    gvars = globals()
    for (field, values) in configs.iteritems():
        gvars['generateReqBy' + field] = commonGenerator(startTime, endTime, field, values)

configs = {"state": ["S1", "S2", "S3", "S4", "S5", "S6"], \
           "type": ["T1", "T2", "T3"], \
           "expr_type": ["E1", "E2", "E3"], \
           "fb": ["F1", "F2"]
           }

def getGenerateFuncs():
    gvars = globals()
    return [ gvars[var] for var in gvars if var.startswith('generateReq')  ]

generateGenerators(startTime, endTime, configs)
caseGenerateFuncs = getGenerateFuncs()
print caseGenerateFuncs

这里函数 commonGenerator 对 commonGenerate 做了一点改动,不再直接返回值列表,而是根据不同的参数返回一个处理不同的函数,这个函数会返回值列表; 然后 generateGenerators 根据指定配置 configs, 调用 commonGenerator 来批量生产generateReqByXXX函数。妙不妙,生产函数的函数 !

闭包###

理解####

按维基的解释: 闭包是引用了自由变量的函数。在例子中,闭包就是 generateReqInner , 引用了传入的自由变量 field, values, 从而在 commonGenerator 调用结束之后,generateReqInner 依然存在能够被访问,且功能效果等同于 generateReqInner(startTime, endTime, field, values) 。

知乎上有句话很有启发性: 闭包就是一种特殊性质的数据,只不过这种数据恰好是携带了数据的代码块,是一个潜伏起来的随时待执行的完整的对象体(数据-行为绑定的执行体)。从这个角度来说,也可以理解闭包的实现:

  • 要件一: 闭包必定存在于一个封闭的作用域 D; 例子中这个 D 就是 commonGenerator 函数的作用域;
  • 要件二: 处于封闭作用域的代码块访问了在代码块作用域之外的封闭作用域里的自由变量。

若只访问自身里的参数及局部变量,就是普通代码块;当封闭作用域结束后,里面的一切都会被销毁; 如果这个代码块,除了访问自身的参数及局部变量,还访问在它之外的封闭作用域里的变量,那么,这个普通代码块就升级为闭包,其访问的自由变量和这个代码块将会共同保存并独立于封闭作用域的存在; 当封闭作用域结束后,这个闭包不会一同消亡,而是继续独立存在。 例子中,generateReqInner 访问了其作用域之外的封闭作用域里的参数 field, values, 从而变成了独立于commonGenerator 的闭包。

一个简单而经典的例子如下:

def outer():
    def inner():
        count = [1]
        print 'inner: ', count[0]
        count[0] += 1
    return inner

def outer2():
    count = [1]
    def inner2():
        print 'inner2: ', count[0]
        count[0] += 1
    return inner2

def outer3(alist):
    inners = []
    for e in alist:
        def inner3():
            print 'inner3: ', e
        inners.append(inner3)
    return inners

def outer4(alist):
    inners = []
    for e in alist:
        def inner4(g):
            def inner():
                print 'inner4: ', g
            return inner
        inners.append(inner4(e))
    return inners

if __name__ == '__main__':
    inner = outer()
    inner()
    inner()
    inner2 = outer2()
    inner2()
    inner2()

    for outer in [outer3, outer4]:
        inners = outer([1,2,3])
        for inner in inners:
            inner()

''' output
inner:  1
inner:  1
inner2:  1
inner2:  2
inner3:  3
inner3:  3
inner3:  3
inner4:  1
inner4:  2
inner4:  3
'''

在 inner 中,只访问了自己的局部变量;当调用 inner = outer() 后, inner 是一个普通函数,每次调用时 count 都会重新创建为 count = [1] ; 而在 inner2 中,访问了outer2 的变量 count, 这个变量在 inner2 的作用域外,成为了一个闭包。 当调用 inner2 = outer2() 后,inner2 存储了自由变量 count ,并在每次调用后都会增加 count[0],从而使得每次打印的值都不同。 注意,如果每次都这样调用 outer2()() ,其效果与 outer()() 是一样的,count 不会变化。因此,从某种意义来说,闭包更像是个动态的执行体,而不是静态的。

outer3 展示了使用闭包的一个注意事项,虽然也采用了闭包,但是闭包存的是循环结束后的最终值;如果要每个函数分别存储循环变量的每个值,就需要将循环变量作为封闭作用域的参数传给闭包。

应用####

闭包的一大应用是作为函数工厂,可以批量生产函数,模拟柯里化效果。柯里化的基本介绍可参阅博文:“函数柯里化(Currying)示例”。闭包 closure(x,y) = closure(x)(y) ,当传入不同的 y 时,就能生产不同的函数。比如幂次方求和函数 p(n,m) = 1^m + 2^m + ... + n^m ;p(n,1) 就是列表求和;p(n,2) 就是平方和; p(n,3) 就是立方和。 代码如下所示:

def p(alist,m):
    return sum(map(lambda x: x**m, alist))

def pclosure(alist, m):
    if m:
        return lambda l: sum(map(lambda x: x**m, l))
    if alist:
        return lambda n: sum(map(lambda x: x**n, alist))
    return lambda l,n: sum(map(lambda x: x**n, l))

def getlist(n):
    return map(lambda x:x+1, range(n))

msum = pclosure([], 1)
print 'sum([1-3]^1) = ', msum(getlist(3))
print 'sum([1-5]^1) = ', msum(getlist(5))

msum = pclosure([], 2)
print 'sum([1-3]^2) = ', msum(getlist(3))
print 'sum([1-5]^2) = ', msum(getlist(5))

mpower = pclosure(getlist(10), None)
print 'sum([1-10]^1) = ', mpower(1)
print 'sum([1-10]^3) = ', mpower(3)

plain = pclosure(None, None)
print 'sum([1-8]^1) = ', plain(getlist(8), 1)
print 'sum([1-8]^2) = ', plain(getlist(8), 2) 

''' output
sum([1-3]^1) =  6
sum([1-5]^1) =  15
sum([1-3]^2) =  14
sum([1-5]^2) =  55
sum([1-10]^1) =  55
sum([1-10]^3) =  3025
sum([1-8]^1) =  36
sum([1-8]^2) =  204
'''   

p 是一个普通的实现,每次都必须指定一个列表alist和一个数值m ;与之对应的是 pclosure 的实现。如果没有提供列表而提供了幂次 M,就返回一个函数,这个函数接受列表,求指定幂次的和 pclosure(alist, m) = pclosure(alist, M) , M 已指定; 如果提供了列表 LIST 而没有提供幂次 m ,就返回一个函数,这个函数接受一个幂次,对列表的指定幂次求和 pclosure(alist, m) = pclosure(LIST, m) , LIST 已指定;如果列表和幂次都没有提供,就退回到一个普通的二元函数p(alist,m) 分别指定不同的参数,就能生成不同种类的一类函数。是不是很有意思?

小结###

通过Python闭包结合配置自动生成函数,使得代码表达能力更强大了。结合函数式编程,其威力可拭目以待。

posted @ 2017-12-22 20:20  琴水玉  阅读(1962)  评论(0编辑  收藏  举报