函数高级
# 变量的可见性与引用
学到这里,我们再深层次的剖析下UnboundLocalError错误,在前文的阐述中有这么一段文字 "到这一步a变量已经定性为一个局部变量啦,但是此a变量还未完成绑定", 这涉及到python的 可见性与绑定.
参考链接: https://www.cnblogs.com/yssjun/p/9873689.html
目标: 也就是我们想弄清楚, 满足什么条件变量名在局部命名空间中就是可见的..
现阶段,不要纠结底层怎么实现可见性的,那是需要经过阅读源码进行分析的..饶过我吧...修行不够,看不懂源码.
def func():
a += 4 # -- UnboundLocalError:local variable 'a' referenced before assignment
return a
a = 1
func()
2
3
4
5
6
# 命名空间与作用域
作用域即代码块!!
scope空间里的变量作用的范围就是所对应的代码块,这些变量在该代码块中是可见的
作用域关系LEGB/函数里引用变量的搜索路径/依次从LEGB作用域对应的scope中查找!
从嵌套的里层往外层查找
作用域关系 查找顺序LEGB,所以跟 代码块的嵌套 有关, 查的是LEGB对应的scope,所以跟代码块的环境有关!
# 专业名词解释
说明: python中的函数、类、模块都是代码块, 只不过在此处讨论的重点是函数的命名空间和作用域,所以特此声明下以下阐述中的代码块统统指函数..
code block
我们可以对代码进行分块code block. 举个例子,定义了一个 类,这个类里面又定义了函数B、函数C, 函数C里嵌套了函数D, 那么 是一个代码块,里面包含B、C两个代码块..C又包含D这个代码块.
scope
程序开始运行后, python解释器会从上到下执行代码,执行到某个代码块时,会为其一对一分配命名空间scope (是真实的内存空间) , 用于存放代码块里面 绑定 的变量名(变量名与对象之间的关系)..
需要注意的是, 代码块看起来是嵌套的(A包含B和C,B包含D),但代码块对应的scope是相互独立的..
block’s environment
该code block中所有scope中可见的name的集合构成block的环境 称作该代码块的环境.
local variable,global variable
scope命名空间也有很多分类,我们接触最多的是全局命名空间和局部命名空间.
整个py模块/执行文件以及py执行文件里使用 import
语句加载的任何模块都会对应一个全局命名空间...
一个个code block代码块对应的是一个个的局部命名空间..
代码块 ----- 局部命名空间 ; import加载的模块、py文件 ------ 全局命名空间
&nbsfp; 在全局scope和局部scope之中定义的变量分别叫做 local variable局部变量和 global variable全局变量..
查阅资料时,经常会看到,"在全局作用域里定义的变量叫做全局变量.."说的是全局、局部作用域而不是全局、局部scope命名空间.. 这些说法都没毛病..
我们这样想就想通了, 作用域与命名空间依托于/作用于同一块代码块,这个代码块中的绑定操作中的变量归命名空间管;这个代码块中的引用变量归作用域管,按照作用域关系LEGB的规则在scope中调用被引用的变量..
作用域是代码块静态的体现,scope命名空间是代码块动态的体现
Free variable
Free variable是一个比较重要的概念, 闭包那里会详细阐述, 这里简单提一嘴, 在闭包中引用的父函数中的局部变量是一个自由变量,而且该自由变量被存放在一个cell对象中..
bind name
scope用于存放code block里定义的变量,具体来说,将某一个对象与变量进行绑定,并将这绑定关系存放在命名空间中. code block中的哪些代码在执行时需要进行 bind name 绑定操作呢?
1> 函数的形参a、b def func(a,b):pass
2> py文件中的import import numpy
3> 直接赋值的操作 a = 1
4> for 循环 中的 i for i in range(10):pass
5> 异常处理except后的名字、文件处理 with open(..) as f
as后面的f 等
名词 | 解释 |
---|---|
代码块 code block | 作为一个单元(Unit)被执行的一段python程序文本.eg: 一个模块、函数体和类的定义等 |
命名空间 scope | 将block代码块中的变量bind绑定到该block对应的命名空间中 |
局部变量 local variable | 在一个block中被绑定(eg 函数中)的变量 |
全局变量 global variable | 在一个module中被绑定(eg py模块)的变量 |
自由变量 free variable | 在某个block中被引用,但没有在该代码块中被定义的变量 |
# 结合官方文档分析
代码块环境
"The set of all such scopes visible to a code block is called the block’s environment."
LEGB
"When a name is used in a code block, it is resolved using the nearest enclosing scope"
这段话告诉我们当一个name被引用时,它会在其最近的scope中寻找被引用name的定义
变量的可见性
"The local variables of a code block can be determined by scanning the entire text of the block for name binding operations."
代码块(eg 函数里的代码)开始执行啦,解释器会为其分配内存空间scope. 但在真正执行之前会先扫瞄函数体里的代码,若存在绑定操作的代码,对象绑定的变量在scope空间便具有了可见性...该变量就定性成了局部变量; 扫描完后,开始真正执行,完成绑定操作..变量便可以被引用.
自由变量
"If a variable is used in a code block but not defined there, it is a free variable."
注意: 函数嵌套,A函数包含B函数..A执行时会扫描B函数的代码,有两点,其一B函数里绑定操作的变量不会存入A的scope中.其二扫描时会记录B中未在B里定义却引用A中可见的变量..当A执行完释放,这些局部变量就成了自由变量,但这些自由变量能否被B成功引用取决于这些自由变量是否完成绑定..
Python在作用域里对变量的赋值操作规则
若这个变量在该作用域里不存在, 则将这次赋值视为对这个变量的定义..若存在,则对其绑定新的对象;
假设代码块(一个函数)在真正执行之前,扫描到函数中依次有两个直接赋值的操作 a = 1
和 a="Hello"
, a变量便具有了可变性,只要其中有一个操作完成了执行,a变量就完成了定义/绑定,就可以被引用.. 若对a变量被多次执行赋值操作,a变量就会经历绑定新的对象的过程..
UnboundLocalError报错
"If the name refers to a local variable that has not been bound, a UnboundLocalError
exception is raised. "
若引用的变量是 局部变量 (代码块中的变量) ,但还未完成绑定,就会报错 UnboundLocalError
..
注意 引用的是局部变量!! 自由变量跟局部变量是两个不同的概念!!
# 函数报错分析
★ 排错思路 : 先扫描,根据绑定操作定性局部变量; 引用变量时根据LEGB规则查找.若引用的局部变量只有可见性..UnboundLocalError
1> Python中要想引用一个name, 该name必须要可见而且是完成绑定了的!
2> 但凡执行的代码块里有绑定操作,绑定的变量在所在代码块对应的scope就具有可见性
具备可见性的变量将会被定性为局部变量.绑定操作真正执行后,可见性变量晋升为可被引用的变量.
(不管代码块里绑定操作的代码是否会执行,因为对代码块中代码的扫描会发生在代码块真正执行之前)
3> 若引用了某个变量,此变量在各个作用域里都找不到(LEGB),就会报错 NameError
;
若引用的变量是 局部变量 ,但还未完成绑定,就会报错 UnboundLocalError
..
4> 若这个变量在该作用域(函数)里不存在, 则将这次赋值视为对这个变量的定义.. 若存在,则对其绑定新的对象;
# 四大案例实践
▲ 案例一分析
"""分析
在outer_func中我们定义了变量loc_var,因为赋值是一种绑定操作,因此loc_var具有可见性,并且完成了绑定,可以被引用. 但根据报错,在outer_func中定义的函数inner_func并不能引用变量loc_var.. 这就让人很疑惑.
这报错结果与代码块的环境block’s environment的概念相矛盾了.因为block’s environment告诉我们函数中的scope是可以扩展到其内定义的所有scope中的.也就意味着outer_func函数中的loc_var变量是可以被inner_func函数所引用的.. 问题出在哪里呢?继续往下看.
当我们执行clo_func()后,inner_func函数的函数体代码开始执行,先扫描,扫描到了loc_var += " in inner func"这个赋值/绑定操作,inner_func函数中的loc_var变量便具有了可见性.扫描完毕后开始真正执行代码.
该赋值操作可以换个写法 loc_var = loc_var + " in inner func"
"="号右边的loc_var变量会先被引用,根据变量的引用规则(LEGB),会先在loc_var变量所在的inner_func函数对应的scope查找loc_var,发现在此scope中,loc_var是可见的,所以不会使用outer_func中定义的loc_var..
但此scope中loc_var变量还没有完成绑定就被引用啦,所以报错UnboundLocalError!!
"""
def outer_func():
loc_var = "local variable" # -- inner_func中没有引用此处的loc_var
def inner_func():
# 该行报错 --UnboundLocalError:local variable 'loc_var' referenced before assignment
loc_var += " in inner func"
return loc_var
return inner_func
clo_func = outer_func()
clo_func()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
▲ 案例二分析
"""分析
flag为False `sel_res = 'Do select name = %s' % name`赋值语句不会执行
is_format为true 在执行`return sel_res if is_format else name`语句时会引用sel_res变量
因为get_select_desc执行之前会扫描代码块代码,绑定操作中的sel_res变量就具有了可见性;
但在return语句中引用了还未完成绑定的局部变量sel_res,所以报错UnboundLocalError..
"""
def get_select_desc(name, flag, is_format=True):
if flag:
sel_res = 'Do select name = %s' % name
# -- UnboundLocalError: local variable 'sel_res' referenced before assignment
return sel_res if is_format else name
get_select_desc('Error', False, True)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
▲ 案例三分析
此案例主要是针对free variable的引用
"""
在创建闭包inner_func时,loc_var1和loc_var2作为父函数outer_func中的两个local variable.具有可见性.
返回闭包函数inner_func之后,执行该闭包函数,可以发现在闭包中引用了outer_func中的local variable.
被引用的local variable被称为一个free variable.
但是闭包中的free variable可不可以被引用取决于它们有没有被绑定到具体的对象,引用有两个前提--可见+执行绑定.
"""
def outer_func(out_flag):
# -- outer_func中的局部变量loc_var1和loc_var2若被inner_func引用,
# 则被引用的局部变量称为free variable自由变量
if out_flag: # True
loc_var1 = 'local variable with flag'
else:
loc_var2 = 'local variable without flag'
def inner_func(in_flag):
# -- NameError:
# free variable 'loc_var2' referenced before assignment in enclosing scope
# loc_var1可以被引用,loc_var2因为没有完成绑定所以不能被引用!
return loc_var1 if in_flag else loc_var2
return inner_func
clo_func = outer_func(True)
print(clo_func(False))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
▲ 案例四分析
"""
代码从上往下执行,sys先是全局变量
add_path函数准备执行,先扫描,发现了绑定操作import sys,sys被定性为了局部变量
add_path函数真正开始执行, path_list = sys.path 引用了还未完成绑定的局部变量sys
"""
import sys
def add_path(new_path):
# UnboundLocalError:local variable 'sys' referenced before assignment
path_list = sys.path
if new_path not in path_list:
import sys
sys.path.append(new_path)
add_path('./')
# --- --- --- 同理 简化版本
i = 10
def func():
# UnboundLocalError:local variable 'i' referenced before assignment
print(i)
for i in range(10):
print(i)
func()
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
# 闭包函数
# 函数代码执行过程!!
参考链接:
https://www.cnblogs.com/traditional/p/13544103.html
https://www.icode9.com/content-1-1334531.html
PyCodeObject:
代码对象, 是一段代码编译后形成的对象. 在函数中对应的就是函数体的代码编译结果. 静态的
PyFunctionObject:
函数对象, 它是对PyCodeObject的封装. 相当于PyCodeObject + 函数def
定义这一行代码.
在PyCodeObject基础上增加了函数的名称、所属模块、参数默认值、globals、builtins.
PyFrameObject:
函数执行时对应的栈帧, 它用于承载PyFunctionObject在执行时所需要的动态信息. 动态的
包括函数的实参、函数执行时所需的栈、全局变量、局部变量、当前执行到的指令的编号.
# -- 虚拟机从上到下执行字节码
name = "夏色祭"
age = -1
# pia, 出现了一个def,来到了一个新的代码块,会对应创建一个新的PyCodeObject对象
# 接着会将PyCodeObject对象封装成PyFunctionObject
# 当执行完def语句之后,一个函数就被创建了,放在当前的local空间中
# 注意:PyFunctionObject在执行到函数定义指令MAKE_FUNCTION时生成,生成后是静态不变的.
# 也就是说,一个函数一旦定义,其函数名参数默认值、函数绑定的globals和builtins信息(都是封装的内容)不再变化.
def foo():
pass
# 函数的类型是<class 'function'>, 当然这个类Python没有暴露给我们
# 当我们调用函数foo的时候, 会从local空间中取出符号"foo"对应的PyFunctionObject对象
# 然后根据这个PyFunctionObject对象创建PyFrameObject对象, 也就是为函数创建一个栈帧 (命名空间)
# 然后将执行权交给新创建的栈帧, 在新创建的栈帧中执行字节码
# PyFrameObject是动态可变的,其包含两层含义:
# 1>对同一个函数的每一次调用,都会生成一个新的PyFrameObject;
# 2>每个PyFrameObject在其生命周期内是不断发生变化的,PyFrameObject承载着函数执行时所需要的所有动态信息.
print(locals()) # {......, 'foo': <function foo at 0x000001B299FAF3A0>}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 传统函数 vs 闭包
不严谨的解释( 最 常见的闭包): 函数嵌套, 内部函数引用了外部函数的参数或变量, 并将这个内部函数返回. 返回的内部函数就称为闭包函数!
"闭"代表函数是内部的,"包"代表函数外’包裹’着对外层作用域的引用..
因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量!
闭包不是传统意义上定义的函数,我们所认识的函数大概是这样的:
1> 程序被加载到内存执行时,函数定义的代码被存放在代码段中
2> 函数被调用时,会在栈上创建其执行环境,初始化其中定义的变量和外部传入的形参 -- scope 栈区
3> 当函数执行完成并返回函数结果后 -- return
函数栈帧便会被销毁掉,函数中的临时变量以及存储的中间计算结果都不会保留 -- 内存回收
4> 下次调用时唯一发生变化的就是函数传入的形参可能会不一样,函数栈帧会重新初始化函数的执行环境
def func(a):
b = 1
return a+b
func(2)
2
3
4
5
6
7
8
9
10
维基百科中对闭包的解释:
"一个函数定义中引用了函数外定义的变量, 该函数可以在其定义环境外被执行."
俺斗胆举个栗子辅助分析下,A嵌套B. B引用A的局部变量, 在A的生命周期结束后, 该变量才称作自由变量,内部函数B才称作闭包函数; B函数 可以 在外部函数A之外运行.
成为闭包函数的 两个前提条件:
1> 函数嵌套; 2> 内部函数引用外部函数( E 内嵌作用域 )中的参数或变量..
成为闭包函数的 必要触发条件:
外部函数执行完成,生命周期结束..内部函数才可称为闭包函数.
成为闭包函数的 不必要条件:
一旦外部函数将内部函数作为函数结果返回..闭包函数就可以在外部函数之外运行
# 闭包初探
# -- demo.py
def outer_func():
my_list = []
def inner_func(name):
my_list.append(len(my_list) + 1)
print('%s my_list = %s' % (name, my_list))
return inner_func
closure_1 = outer_func()
closure_1('cls1_instance_1') # -- cls1_instance_1 my_list = [1]
closure_1('cls1_instance_2') # -- cls1_instance_2 my_list = [1, 2]
closure_1('cls1_instance_3') # -- cls1_instance_3 my_list = [1, 2, 3]
closure_2 = outer_func()
closure_2('cls2_instance_1') # -- cls2_instance_1 my_list = [1]
closure_1('cls1_instance_4') # -- cls1_instance_4 my_list = [1, 2, 3, 4]
closure_2('cls2_instance_2') # -- cls2_instance_2 my_list = [1, 2]
"""
调用两次outer_func,分别return返回了closure_1和closure_2两个闭包函数.
闭包函数跟普通函数的调用没啥区别,可以对闭包函数多次调用.
1> 观察闭包函数closure_1的多次调用,my_list在变化:
对同一个闭包函数多次调用,每次调用对其自由变量的修改会被传递到下一次的调用.(有点默认参数那味)
2> 可以观察闭包函数closure_1的调用和闭包函数closure_2的调用,并不会互相干扰.
闭包中引用的自由变量只和具体的闭包有关联.
"""
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
函数开始执行,会先扫描函数体的代码,包括内部函数inner_func的代码,记录inner_func中的引用变量.
记录的引用变量应满足两点,其一,在inner_func里未定义,其二,在outer_func中是可见的.
当outer_func函数结束,inner_func从内部函数进化成为闭包函数.. my_list从局部变量变为自由变量.
闭包中引用的父函数中的局部变量是一个自由变量, 自由变量存到inner_func的cell对象中
# 自由变量的存储
Q: 函数inner如何存储变量var呢?
A: inner.__code__.co_freevars
记录了自由变量的名字
在函数编译过程中内部函数会有一个闭包的特殊属性__closure__
, 若内部函数中不包含对外部函数变量的引用,__closure__
属性是不存在的, 该属性记录了自由变量的值,它是一个由cell对象组成的元组
g_var = 1
def outer():
e_var_x = []
e_var_y = 6
def inner():
e_var_x.append(g_var + e_var_y)
print(e_var_x, e_var_y)
return inner
a = outer()
# ('e_var_x', 'e_var_y') -- inner引用了全局变量g_var,但g_var不算作自由变量!
print(a.__code__.co_freevars)
# (<cell at 0x7fe0a0e7cac0: list object at 0x7fe0a0e8b940>, <cell at 0x7fe0a0e7c6a0: int object at 0x100aceb60>)
print(a.__closure__)
print(a.__closure__[0].cell_contents) # [] -- 此时闭包还没运行,自由变量的值最初状态
print(a.__closure__[1].cell_contents) # 6
a() # [7] 6
g_var = 8
a() # [7, 14] 6 -- 同一个闭包函数多次运行,对自由变量的修改会传递 [7]->[7, 14]
print(a.__closure__[0].cell_contents) # [7,14]
print(a.__closure__[1].cell_contents) # 6
g_var = 11
b = outer()
b() # [17] 6 -- 闭包a、b引用的自由变量互不干扰
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
# id不同缘由
补充: 请解释下面的id相同与不同的缘由.. 这个问题纠结了我一天!!!(∩_∩) 堆区水太深,道行太浅,勿涉
`Two objects with non-overlapping lifetimes may have the same id() value.`
两个生存周期不相交的不同对象,可能有一个地址.
判断对象相同与否用is,不要用id去比较,is由于不能被重载,因此不用查哈希表,会更快一些!!!
"""
1> 每次执行def语句,都会生成一个不同的PyFunctionObjec对象
每次调用执行函数,都会生成一个不同的PyFrameObjec对象,也就是命名空间
2> outer函数执行完后,命名空间里的变量名会与具体的对象断开连接 释放命名空间
若堆区的对象引用计数为0,会进行gc回收
也就是说,outer函数执行完,inner函数/变量名与PyFunctionObject断开连接了
`print(id(a), id(b))` 值相同
返回了PyFunctionObject对象并赋值给了某个全局变量
So,堆区中的PyFunctionObject对象得以保存,引用计数为1,不会被gc回收
调用执行两次outer函数,在outer函数的函数体里执行了两次相同的def语句
def语句相同,但都会生成一个不同的PyFunctionObjec对象
`print(id(outer()), id(outer()))` 值不同
你会发现,这里同样调用执行了两次outer函数,在outer函数的函数体里执行了两次相同的def语句
为啥id相同呢?因为第一次的PyFunctionObjec对象的引用计数为0,优化机制,第二次的def直接拿来用
"""
print(id(a), id(b)) # 140650857597392 140650857597248
print(id(outer()), id(outer())) # 140650857596672 140650857596672
# -- 举个简单例子辅助理解 模拟`def inner`生成PyFunctionObjec对象
def func():
pass
print(id(func)) # 140402907130320
del func
def func():
pass
print(id(func)) # 140402907130320
def func():
pass
print(id(func)) # 140690837776848
temp = func # !!!!!!
del func
def func():
pass
print(id(func)) # 140690837776704
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
# 闭包陷阱
返回闭包中不要引用任何循环变量, 或者后续会发生变化的变量
因为在返回闭包前,闭包中引用的父函数中定义变量的值可能会发生不是我们期望的变化
自由变量的名称解析发生于运行时而不是编译时
i = 10
def f():
print(i)
i = 42
f()
2
3
4
5
程序的结果并不是我们想象的结果0,1,4. 实际结果全部是4. 为啥?
"""
在函数outer返回前其内部定义的函数并不是闭包函数,只是一个内部定义的函数
这个内部函数引用的父函数中定义的变量也不是自由变量,而只是当前block中的一个local variable
在返回闭包列表clo_list之前for循环的变量的值已经发生改变了,而且这个改变会影响到所有引用它的内部定义的函数
函数outer一旦返回,其内部定义的函数inner便是一个闭包,其中引用的变量i成为一个只和具体闭包相关的自由变量..
"""
def outer():
clo_list = []
for i in range(3):
# -- 等同于inner = lambda : i * i
def inner():
return i * i
clo_list.append(inner)
return clo_list
clo1, clo2, clo3 = outer()
print(clo1()) # 4
print(clo2()) # 4
print(clo3()) # 4
# -- 该代码与上面的分析逻辑是一样的
def outer():
clo_list = []
m = 0
for i in range(3):
def inner():
return m * m
clo_list.append(inner)
m = 3
return clo_list
clo1, clo2, clo3 = outer()
print(clo1(),clo2(),clo3()) # 9 9 9
# -- 正解
# outer结束,自由变量i会作为一个cell对象存储在inner函数的__closure__属性里
# 不使用自由变量即可,使用默认参数解决 默认形参保存了当前的i值,在对应闭包中以局部变量体现
def outer():
clo_list = []
for i in range(3):
# -- 添加一个默认函数,其值设置为每次循环的i值
# 对形参的不同赋值会保留在当前函数定义中,不会对其他函数有影响
inner = lambda _i = i : _i * _i
clo_list.append(inner)
return clo_list
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
# nonlocal
nonlocal关键字 实现在对自由变量的引用同时,修改自由变量
# -- 计算平均值
def make_averager():
total = 0 # -- 元素值总计
count = 0 # -- 共多少元素参与
"""
`nonlocal count, total`语句,指明averager中使用的count和total变量是在引用make_averager中的变量
count = count + 1 右侧引用内嵌作用域的count,左侧对内嵌作用域中的count重新赋值(绑定对象)
这样的话有效的避免了UnboundLocalError报错
当make_averager返回后,count和total变量就变成了自由变量!
注意前面案例里提到过的一点:同一个闭包函数的多次调用可以将自由变量的修改进行传递!
自由变量若是可变类型,eg:列表 闭包函数可以通过append等内置函数修改
若自由变量是不可变类型
eg:数字 闭包函数使用赋值语句来改变 相当于在闭包函数内部创建个同名的局部变量,就不会使用自由变量啦、
nonlocal关键字可以 实现在对自由变量的引用同时,修改自由变量
"""
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
avg = make_averager()
print(avg(10))
print("---:> ", avg.__closure__[0].cell_contents, avg.__closure__[1].cell_contents)
print(avg(11))
print("---:> ", avg.__closure__[0].cell_contents, avg.__closure__[1].cell_contents)
"""
10.0
---:> 1 10
10.5
---:> 2 21
"""
# --- --- ---
def outer():
x = 5
def inner():
nonlocal x
x += 1
return x
return inner() # -- 返回的是一个确定的值,不是函数!!
a = outer()
print(a) # 6
print(a) # 6
b = outer()
print(b) # 6
print(b) # 6
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
# 为函数体传值
# 参数形式
import requests
"""
★ --使用参数的形式
"""
def get(url='https://www.baidu.com/'):
res = requests.get(url)
if res.status_code == 200:
print(res.text)
# -- 多次调用,每次都写url很麻烦
get('https://www.python.org')
get('https://www.python.org')
# -- 默认参数应用场景:绝大多数情况下都是这个值
get()
get()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 包给函数/惰性计算
"""
★ --包给函数 应用领域:延迟计算
"""
def outer(url):
def get():
res = requests.get(url)
if res.status_code == 200:
print(res.text)
return get
baidu = outer('https://www.baidu.com/') # -- 将指定的url地址包给了函数
python = outer('https://www.python.org')
baidu()
baidu()
python()
python()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 函数装饰器
装饰器是用来为被装饰对象添加额外功能的工具/函数
装饰器和被装饰者都可以是任意可调用的对象.
# 开放封闭原则
软件的维护应该遵循 开放封闭 原则!即软件一旦上线之后对修改源代码是封闭的,对扩展功能是开放的.
因而装饰器的实现必须遵循两大原则: (在遵循1和2的前提下为被装饰对象添加新功能)
1.不修改被装饰对象的源代码
2.不修改被装饰对象的调用方式
需求: 给index函数加上统计运行时间的功能..
import time
def index():
print('welcome to index!')
time.sleep(3)
"""
★ --方案一: 改变了源代码
"""
def index():
start = time.time()
print('welcome to index!')
time.sleep(3)
stop = time.time()
print(f'run time is {stop-start}')
index()
"""
★ --方案二: 不具备通用性,给其他函数添加此功能会重复写代码
"""
def index():
print('welcome to index!')
time.sleep(3)
start = time.time()
index()
stop = time.time()
print(f'run time is {stop-start}')
"""
★ --方案三: 解决了重复写,但改变了被装饰对象的调用方式
"""
def timmer(func):
start = time.time()
func()
stop = time.time()
print(f'run time is {stop-start}')
timmer(index)
"""
★ --方案四: 使用装饰器
"""
def timmer(func):
def wrapper():
start = time.time()
func()
stop = time.time()
print(f'run time is {stop-start}')
return wrapper
index = timmer(index) # -- index = wrapper
index()
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
# 无参装饰器
import functools
def dec(func):
@functools.wraps(func) # -- 保留被装饰函数的文档和函数名属性
def wrapper(*args,**kwargs): # -- *args,**kwargs 接收传给被装饰函数的实参
return func(*args,**kwargs) # -- 注意哦,不写return 返回的是None
# wrapper.__doc__ = func.__doc__
# wrapper.__name__ = func.__name__
return wrapper
"""
@dec 等同于 func = dec(func)
dec(func) -- wrapper So,func = wrapper
"""
@dec
def func():pass
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 有参装饰器
import functools
def func_name(param1,param2)
def dec(func):
@functools.wraps(func)
def wrapper(*args,**kwargs):
# -- param1、param2、func都是wrapper函数的自由变量!
res = func(*args,**kwargs)
return res
return wrapper
return dec
"""
在handler函数定义完后,开始执行@func_name("","")
@func_name("","")等同于执行语句 handler = func_name("","")(handler)
func_name("","")(handler) -- deco(handler) -- wrapper
So,handler = wrapper
handler()语句调用执行时,是在调用wrapper()
是这么回事:一开始`def handler`生成的PyFunctionObject对象,
也与闭包函数wrapper的自由变量func进行了绑定
然后将闭包函数wrapper的内存地址传递给全局变量handler,进行了覆盖
"""
@func_name("","")
def handler():pass
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 计时程序
import time
def timing(status):
def dec(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
print(f"[{status}] use time:{time.time() - start}")
return res
return wrapper
return dec
# -- 训练
@timing('train')
def training():
time.sleep(3)
# -- c测试
@timing('test')
def testing():
time.sleep(2)
training()
testing()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 登录用户认证
import time
current_user = {'user':None} # -- 当前登录用户
def auth(way='file'):
def dec(func):
def wrapper(*args,**kwargs):
if current_user['user']:
return func(*args,**kwargs)
user = input('请输入用户名>>:')
pwd = input('请输入密码>>:')
if way == "file": # -- 基于文件的验证
if user == 'egon' and pwd == '123': # -- 模拟从文件中取数据
current_user['user'] = user
return func(*args,**kwargs)
else:
print("login fail")
elif way == "ldap":
pass # -- 基于ldap的认证
else:
print('无法识别验证来源')
return wrapper
return dec
@auth('file')
def index():
print(f"welcome to index")
time.sleep(1)
@auth()
def home(name):
print(f"welcome home,{name}")
time.sleep(0.5)
index()
home('dc')
print(index.__code__.co_freevars) # ('func', 'way')
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
# 将函数装到字典中
灵活使用语法糖@
func_dic = {}
def make_route(name):
def dec(func):
# -- 通常是要再嵌套一层 def wrapper(*args,**kwargs)用于被装饰函数的运行以及加额外功能
# -- 但这里不需要被装饰函数的运行.. 但需要将函数名对应的pyfunctionobject装在字典中
func_dic[name] = func
return func # -- 添加后,就不会影响被装饰函数原来的运行啦
# 若不写这行语句 全局变量f1 = None
return dec
@make_route('select') # -- f1 = dec(f1) dec内部执行语句func_dic['select'] = f1
def f1():
print('This is select func.')
@make_route('update')
def f2():
print('This is update func.')
# {'select': <function f1 at 0x7fe14c72d9d0>, 'update': <function f2 at 0x7fe14c72db80>}
print(func_dic)
func_dic['update']() # This is update func. -- 可通过字典调用函数
f2() # This is update func. -- 原函数的调用没有影响
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 多个装饰器
多个@语法糖
被装饰函数定义阶段, 从下到上调用装饰器函数
被装饰函数调用阶段, 从上到下执行装饰器里的闭包函数 (特别要注意 fun是啥!! fun是下一次要调用的闭包函数对象,若接下来没有闭包函数调用了,就是fun定义时生成的PyFunctionObject对象!)
实验验证证明上方的结论!!! 鲁迅先生说过,实践是检验真理的唯一办法.
import functools
import time
def timing(status='train'):
print('this is timing') # -- 0
def dec3(func):
print('this is dec3 in timing') # -- 1
@functools.wraps(func)
def wrapper3(*args,**kwargs):
start = time.time()
res = func(*args,**kwargs)
print('[%s] time: %.3f s '%(status,time.time()-start)) # -- 6
return res
return wrapper3
return dec3
def dec1(func):
print('this is dec1') # -- 3
@functools.wraps(func)
def wrapper1(*args,**kwargs):
print('The wrapper in dec1') # -- 4
return func(*args,**kwargs)
return wrapper1
def dec2(func):
print('this is dec2') # -- 2
@functools.wraps(func)
def wrapper2(*args,**kwargs):
print('The wrapper in dec2') # -- 5
return func(*args,**kwargs)
return wrapper2
@dec1
@dec2
@timing(status='test')
def fun():pass
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
# 被装饰函数定义阶段
注意三个装饰器的形参都是func!
第一步执行timing('Test')(fun)
该语句里的func就是def fun
定义时全局变量fun绑定的PyFunctionObject对象;
第一步执行完后返回闭包函数wrapper3, wrapper3作为第二步装饰器函数dec2调用时的形参.
同理,第二步执行完后返回的闭包函数wrapper2作为第三步装饰器函数dec1调用时的形参.
第三步完成后,返回了闭包函数wrapper1.. 赋值给了全局变量fun (全局变量fun重新进行了绑定!)
# -- fun = dec1(dec2(timing('Test')(fun))) 先运行里层括号的内容
# 类似于 str(3+1) '4' 会将括号里的内容先进行运算,将结果作为实参传给str函数的行参
# 根据结果,dec1(dec2(timing('Test')(fun)))从里到外/从下到上依次调用执行了dec3、dec2、dec1装饰器
# 返回的结果依次为wrapper3、wrapper2、wrapper1
# 全局命名空间里的fun变量被覆盖了,重新指向了闭包函数wrapper1的内存地址
@dec1
@dec2
@timing(status='test')
def fun():
time.sleep(2)
"""
this is timing
this is dec3 in timing
this is dec2
this is dec1
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 被装饰函数调用阶段
调用时,要注意!! 在最外层(全局作用域里),fun=wrapper1
; 在wrapper1
的作用域内 fun=wrapper2
; 在wrapper2
的作用域内fun=wrapper3
@dec1
@dec2
@timing(status='test')
def fun():
time.sleep(2)
# -- 实验结果能充分表明
# 先执行的闭包函数wrapper1,里面遇到func()时,实则调用的是闭包函数wrapper2..以此类推.
print("--- 我是分隔符 ---")
fun()
"""
this is timing
this is dec3 in timing
this is dec2
this is dec1
--- 我是分隔符 ---
The wrapper in dec1
The wrapper in dec2
[test] time: 2.001 s
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20