返回函数
我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
1 2 3 4 5 def calc_sum (*args ): ax = 0 for n in args: ax = ax + n return ax
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
1 2 3 4 5 6 7 def lazy_sum (*args ): def sum (): ax = 0 for n in args: ax = ax + n return ax return sum
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
1 2 3 >>> f = lazy_sum(1 , 3 , 5 , 7 , 9 )>>> f<function lazy_sum.<locals >.sum at 0x101c6ed90 >
在上面的例子中,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种结构就是所谓的闭包。
每次调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
1 2 3 4 >>> f1 = lazy_sum(1 , 3 , 5 , 7 , 9 )>>> f2 = lazy_sum(1 , 3 , 5 , 7 , 9 )>>> f1==f2False
f1()
和f2()
的调用结果互不影响。
变量作用域
通常,函数内部的变量无法被函数外部访问,但内部可以访问;类内部的变量无法被外部访问,但类的内部可以。通俗来讲,就是内部代码可以访问外部变量,而外部代码通常无法访问内部变量。
Python的作用域一共有4层,分别是:
L (Local) 局部作用域
E (Enclosing) 闭包函数外的函数中
G (Global) 全局作用域
B (Built-in) 内建作用域
1 2 3 4 5 6 7 x = int (2.9 ) global_var = 0 def outer (): out_var = 1 def inner (): inner_var = 2
Python以**L –> E –> G –>B
**的规则查找变量,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,最后去内建中找。如果这样还找不到,那就提示变量不存在的错误。
全局变量 定义在全局作用域,是在整个程序中可以访问的变量
局部变量 定义在函数内部,是在特定的作用域中可以访问的变量
自由变量 是未在局部作用域中绑定的变量。
观察下面的代码
1 2 3 4 5 6 7 8 >>> b = 6 >>> def f1 (a ):... print (a)... print (b)... >>> f1(3 )3 6
函数体外的b为全局变量,函数体内的b为自由变量。因为自由变量b绑定 到了全局变量,所以在函数f1()中能正确打印。
1 2 3 4 5 6 7 8 9 10 11 12 >>> b = 6 def f1 (a ):... print (a)... print (b) ... b = 9 ... >>> f1(3 )3 Traceback (most recent call last): File "<input>" , line 1 , in <module> File "<input>" , line 3 , in f1 UnboundLocalError: local variable 'b' referenced before assignment
由于Python不要求声明变量,而是假定在函数定义体中赋值的变量是局部变量 ,b 被声明为局部变量后,解释器便不会向下层级寻找全局作用域中的 b = 6
。
如果想让解释器把b当做全局变量,那么需要使用global声明:
1 2 3 4 5 6 7 8 9 10 >>> b = 6 >>> def f1(a): ... global b ... print(a) ... print(b) ... b = 9 ... >>> f1(3 ) 3 6
闭包
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个 商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
使用类实现的话:
1 2 3 4 5 6 7 8 9 10 11 12 class Averager (): def __init__ (self ): self.series = [] def __call__ (self, new_value ): self.series.append(new_value) total = sum (self.series) return total / len (self.series) avg = make_averager() avg(10 ) avg(11 ) avg(12 )
类实现不存在自由变量问题 ,因为self.series是类属性。
使用函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def make_averager (): series = [] def averager (new_value ): series.append(new_value) total = sum (series) return total / len (series) return averager avg = make_averager() avg(10 ) avg(11 ) avg(12 )
调用 make_averager 时,返回一个 averager 函数对象。每次调用 averager 时,它会 把参数添加到系列值中,然后计算当前平均值。
那么问题来了,调用完make_averager()然后 return 之后,make_averager 的局部作用域已经丢失,为什么之后 avg(10)依然能在内层函数使用 series呢?
正是由 Python 中神奇的闭包特性实现:
series 在外层函数声明,在内层被绑定在average()的局部作用域上 。
1 2 3 4 5 6 7 8 >>> avg.__code__.co_varnames('new_value' , 'total' ) >>> avg.__code__.co_freevars('series' ,) >>> avg.__closure__ (<cell at 0x107a44f78 : list object at 0x107a91a48 >,) >>> avg.__closure__[0 ].cell_contents[10 , 11 , 12 ]
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
nonlocal
在上面的例子中我们使用List 保存历史值, 如果只存储目前的总值和元素个数,然后使用这两个数计算均值:
1 2 3 4 5 6 7 8 9 10 11 def make_averager (): count = 0 total = 0 def averager (new_value ): count += 1 total += new_value return total / count return averager avg = make_averager() avg(10 )
问题是,当 count 是数字或任何不可变类型(向新对象赋值)时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。
但是对数字、字符串、元组等不可变类型 来说,只能读取,不能更新。如果尝试重新绑定,例 如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量 了,因此不会保存在闭包中。
Python 3 引入了 nonlocal 声明 。它的作用是把变量标记为自由变量, 即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。
1 2 3 4 5 6 7 8 9 def make_averager (): count = 0 total = 0 def averager (new_value ): nonlocal count, total count += 1 total += new_value return total / count return averager
装饰器
装饰器即在不改动原函数功能的要求下,增加原函数的功能
有以下口诀:客人空手来,还带请上楼(在内层函数添加功能),干啥都同意(执行原函数功能),有参给上楼
编写一个装饰器用来统计函数的运行时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def clock (func ): def cloked (): start = time.time() func() end = time.time() print (end-start) return cloked def orign (): print ("hahahahahaha" ) f2 = clock(orign) f2()
这行代码之所以可用,就是因为 clocked 函数的闭包中包含 func 自由变量。
功能已经完成了,但是这样写还是太麻烦。
Python中有一个语法糖:在原函数定义的上方加上@装饰器函数名称
,之后再使用orgin()就等同于执行以上两句。
1 2 3 4 5 @clock def orign (): print ("hahahahahaha" ) orign()
那么原函数中如果带有参数怎么办呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def metric (func ): def wrapper (*args ): start = time.time() func(*args) end = time.time() print (f"{func.__name__} runed {end-start:.8 f} s" ) return wrapper @metric def print_age (name, age ): print (f'{name} 今年{age} 岁' ) print_age('小明' ,18 )
需要传入参数的装饰器
假如我们所定义的装饰器需要传入参数,该怎么办呢?
答案是在装饰器函数增加返回函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import functoolsdef log (text ): def decorator (func ): @functools.wraps(func ) def wrapper (*args,**kwargs ): print (f'{text} {func.__name__} ' ) return func(*args, **kwargs) return wrapper return decorator @log('执行函数 ' ) def print_age (name,age ): print (f'{name} 今年{age} 岁' ) print_age("wang" ,23 )
@语法糖等同于以下三句
1 2 3 deco = log("执行函数 " ) f = deco(print_age) f('wang' ,22 )
而@functools.wraps(func)
效果等同于wrapper.__name__ = func.__name__
,防止有些依赖函数签名的代码执行就会出错。
可选参数的装饰器
如何定义一个装饰器,使其参数可选
如:
仍以上面打印日志的装饰器为例
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 def log (text ): def decorator (func ): @functools.wraps(func ) def wrapper (*args, **kwargs ): if type (text) is str : print (f'{text} {func.__name__} ' ) else : print (f'执行了 {func.__name__} ' ) return func(*args,**kwargs) return wrapper if type (text) is str : return decorator else : return decorator(text) @log('exe' ) def say_hello (word ): print (f'hello {word} !' ) @log def say_hi (word ): print (f'hi {word} !' ) say_hello("wang" ) say_hi("小明" )
@语法糖等同于
1 2 log('exe' )(say_hello)("wang" ) log(say_hi)("小明" )
参考:
《流畅的Python》
刘江的博客
廖雪峰的博客