《Python学习笔记本》第四章 函数 笔记以及摘要(完结)
定义
函数因减少依赖关系,具备良好的可测试性和可维护性,这是性能优化的关键所在。另外,我们还应遵循一个基本元祖,就是专注于左一件事,不受外在干扰和污染。
函数要短而精,使用最小作用域。如有可能,应确保其行为的一致性。如果逻辑受参数影响而有所不同,那应该将更多个逻辑分支分别重构成独立函数,使其从'变'转为'不变'.
创建
函数由两部分组成:代码对象持有的字节码和指令元数据,负责执行;函数对象则为上下文提供调用实例,并管理所需的状态数据.
In [180]: def test(x, y=10): ...: x += 100 ...: print(x, y) ...: In [181]: test # 函数对象 Out[181]: <function __main__.test(x, y=10)> In [182]: test.__code__ # 代码对象 Out[182]: <code object test at 0x1126c7150, file "<ipython-input-180-7d663f3145ec>", line 1> In [183]:
记住函数对象有__dict__属性
代码对象的相关属性由编译器生成,为只读模式。存储指令运行所需的相关信息,诸如原码行、指令操作数、以及参数和变量名
In [186]: test.__code__.co_varnames Out[186]: ('x', 'y') In [187]: test.__code__.co_consts Out[187]: (None, 100) In [188]:
In [188]: dis.dis(test.__code__) 2 0 LOAD_FAST 0 (x) 2 LOAD_CONST 1 (100) 4 INPLACE_ADD 6 STORE_FAST 0 (x) 3 8 LOAD_GLOBAL 0 (print) 10 LOAD_FAST 0 (x) 12 LOAD_FAST 1 (y) 14 CALL_FUNCTION 2 16 POP_TOP 18 LOAD_CONST 0 (None) 20 RETURN_VALUE In [189]:
与代码对象只关注执行不同,函数对象作为外部实例存在,复制管理运行期状态。
In [192]: test.__defaults__ Out[192]: (10,) In [193]: test.__defaults__ = (1234,) In [194]: test(1) 101 1234 In [195]: test.abc = 'nihao' In [196]: test.__dict__ Out[196]: {'abc': 'nihao'} In [197]: vars(test) Out[197]: {'abc': 'nihao'} In [198]:
事实上,def使运行期指令。以代码对象为参数,创建函数实例,并在当前上下文环境中与指定的名字相关联
In [198]: dis.dis(compile('def test():...','','exec')) 1 0 LOAD_CONST 0 (<code object test at 0x110ce6660, file "", line 1>) 2 LOAD_CONST 1 ('test') 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (test) 8 LOAD_CONST 2 (None) 10 RETURN_VALUE Disassembly of <code object test at 0x110ce6660, file "", line 1>: 1 0 LOAD_CONST 0 (None) 2 RETURN_VALUE In [199]:
正因为如此,可用def以单个代码对象为模板创建多个函数实例
In [199]: def make(n): ...: res = [] ...: ...: for i in range(n): ...: def test(): ...: print('hello') ...: print(id(test), id(test.__code__)) ...: res.append(test) ...: return res ...: In [200]: make(3) 4585832176 4614607616 4598915728 4614607616 4597777328 4614607616 Out[200]: [<function __main__.make.<locals>.test()>, <function __main__.make.<locals>.test()>, <function __main__.make.<locals>.test()>] In [201]:
一套代码对象,给三个函数实例使用。
函数作为第一类对象,可以作为参数和返回值传递。
嵌套
支持函数嵌套,其设置可于外层函数同名
内外层函数名字虽然相同,单分属于不同层次的名字空间
匿名函数
lambda
相比较普通函数,匿名函数的内容只能是单个表达式,而不能使用语句,也不能提供默认函数名。
In [201]: x = lambda x=1:x In [202]: x Out[202]: <function __main__.<lambda>(x=1)> In [203]: x() Out[203]: 1 In [204]: x.__name__ Out[204]: '<lambda>' In [205]: x.__defaults__ Out[205]: (1,) In [206]:
lambda函数比较可怜,没有自己的名字
原码分析创建过程也是'路人甲'待遇
In [206]: dis.dis(compile('def test():pass','','exec')) 1 0 LOAD_CONST 0 (<code object test at 0x1126cf660, file "", line 1>) 2 LOAD_CONST 1 ('test') 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (test) 8 LOAD_CONST 2 (None) 10 RETURN_VALUE Disassembly of <code object test at 0x1126cf660, file "", line 1>: 1 0 LOAD_CONST 0 (None) 2 RETURN_VALUE In [207]: dis.dis(compile('lamdba : None','','exec')) 1 0 SETUP_ANNOTATIONS 2 LOAD_CONST 0 (None) 4 LOAD_NAME 0 (__annotations__) 6 LOAD_CONST 1 ('lamdba') 8 STORE_SUBSCR 10 LOAD_CONST 0 (None) 12 RETURN_VALUE In [208]:
但lambda用起来很方便
In [208]: m = map(lambda x:x**2, range(3)) In [209]: m Out[209]: <map at 0x11155afd0> In [210]: list(m) Out[210]: [0, 1, 4]
lambda同样支持嵌套与闭包
In [212]: test = lambda x: (lambda y: x+y) In [213]: madd= test(4) In [214]: madd(5) Out[214]: 9 In [215]: madd(10) Out[215]: 14
x就成为了闭包的参数
记住括号的使用
In [216]: (lambda x:print(x+'lambda'))('hello') hellolambda In [217]:
参数
参数可分为位置和键值两类
不管实参是名字、引用、还是指针,其都以值复制方式传递,随后的形参变化不会影响实参。当然,对该指针或应用目标的修改,于此无关。
传参一般用的比较熟,这里介绍一种keyword_only的键值参数类型(该变量必须以关键字参数的方式传参)
满足以下条件
1 以星号与位置参数列表分割边界
2普通keyword-only参数,零到多个
3有默认值的keyword_only参数,零个到多个
4双星号键值收集参数,仅一个
无默认值的keyword_only必须显式命名传参,否则会被视为普通位置参数
In [218]: def test(a,b,*,c): ...: print(locals()) ...: In [219]: test(1,2,3) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-219-3cf409ba8ac0> in <module> ----> 1 test(1,2,3) TypeError: test() takes 2 positional arguments but 3 were given In [220]: test(1,2,3,4) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-220-0c4be6dad9c5> in <module> ----> 1 test(1,2,3,4) TypeError: test() takes 2 positional arguments but 4 were given In [221]: test(1,2,c=3) {'a': 1, 'b': 2, 'c': 3} In [222]:
即便没有位置参数,keyword-only也必须按关键字传参
In [222]: def text(*,x): ...: print(locals()) ...: In [225]: text(1) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-225-61eb59d0069f> in <module> ----> 1 text(1) TypeError: text() takes 0 positional arguments but 1 was given In [226]: text(x=1) {'x': 1} In [227]:
一个传参里面只能出现一个*与一个**,而且不能对收集参数名传参,就是args=xx, kwargs=xx这种
默认值
In [236]: def test(a,x=[1,2]): ...: x.append(a) ...: print(x) ...: In [237]: test.__defaults__ Out[237]: ([1, 2],) In [238]: dis.dis(compile('def test(a, x=[1,2]):pass','','exec')) 1 0 LOAD_CONST 0 (1) 2 LOAD_CONST 1 (2) 4 BUILD_LIST 2 # 构建默认值对象 6 BUILD_TUPLE 1 # 构建参数 8 LOAD_CONST 2 (<code object test at 0x11248b810, file "", line 1>) 10 LOAD_CONST 3 ('test') 12 MAKE_FUNCTION 1 # 参数1表示包含缺省参数 14 STORE_NAME 0 (test) 16 LOAD_CONST 4 (None) 18 RETURN_VALUE Disassembly of <code object test at 0x11248b810, file "", line 1>: 1 0 LOAD_CONST 0 (None) 2 RETURN_VALUE In [239]: test(3) [1, 2, 3] In [240]: test.__defaults__ Out[240]: ([1, 2, 3],) In [241]:
所以在选择默认参数的时候要用None或者不可变参数
In [241]: def test(a,x = None): ...: x = x or [] ...: x.append(a) ...: return x ...: In [242]: test(1,[3]) Out[242]: [3, 1] In [243]: test(1) Out[243]: [1] In [244]: test(1,[54,34]) Out[244]: [54, 34, 1] In [245]:
书中有一个很骚的写法,也是很骚的想法,通过函数的自身的属性赋值,来实现计数功能。
In [245]: def test():
# 最傻的就是这局赋值语句,利用的短路原则的属性赋值与写入,骚实在是骚 ...: test.__count__ = hasattr(test,'__count__') and test.__count__ + 1 or 1 ...: print(test.__count__) ...: In [246]: test() 1 In [247]: test() 2 In [248]: test() 3 In [249]: test() 4 In [250]:
形参赋值
解释器对形参赋值的过程如下
1.按顺序对外置参数赋值
2.按命名方式对指定参数赋值
3.收集多于的位置参数
4.收集多于的键值参数
5.为没有赋值的参数设置默认值
6.检查参数列表,确保非收集参数都已赋值。
对应形参的顺序,实参也有一些基本规则
无默认值参数,必须有实参传入
键值参数总是以命名方式传入
不能对同一参数重复传值
4.3返回值
函数具体返回什么,都由你说了算,用return
这一章比较简单,不写了,多个返回值,返回的是元祖
4.4作用域
在函数内访问变量,会以特定顺序依次查找不同层次的作用域
高手写的LEGB
In [250]: import builtins In [251]: builtins.B = "B" In [252]: G = "G" In [253]: def enclosing(): ...: E = "E" ...: def test(): ...: L= "L" ...: print(L,E,G,B) ...: return test ...: In [254]: enclosing()() L E G B In [255]:
内存结构
函数每次调用,都会新建栈帧(stack frame),用于局部变量和执行过程的存储。等执行结束,栈帧内存被回收,同时释放相关对象。
In [254]: enclosing()() L E G B In [255]: def test(): ...: print(id(locals())) ...: In [256]: test() 4607482768 In [257]: test() 4607766192 In [258]:
locals()我们看到以字典实现的名字空间,虽然灵活,但存在访问效率底下等问题。这对于使用频率低的模块名字空间尚可,可对于有性能要求的函数调用,显然就是瓶颈所在
为此,解释器划出专门的内存空间,用效率最快的数组替代字典。在函数指令执行签,先将包含参数在内的所有局部变量,以及要使用的外部变量复制(指针)到该数组。
基于作用域不同,此内存区域可简单分作两部分:FAST和DEREF
如此,操作指令只需要用索引既可立即读取或存储目标对象,这远比哈希查找过程高效很多。从前面的反汇编开始,我们就看到了大量类似于LOAD_FAST的指令,其参数就是索引号
In [258]: def enclosing(): ...: E= 'E' ...: def test(a,b): ...: c = a+b ...: print(E, c) ...: return test ...: In [259]: t = enclosing() # 返回test函数 In [260]: t.__code__.co_varnames # 局部变量列表(含参数)。与索引号对应 Out[260]: ('a', 'b', 'c') In [261]: t.__code__.co_freevars # 所引用的外部变量列表。与索引号对应 Out[261]: ('E',) In [262]:
In [262]: dis.dis(t) 4 0 LOAD_FAST 0 (a) # 从FAST区域,以索引号访问并载入 2 LOAD_FAST 1 (b) 4 BINARY_ADD 6 STORE_FAST 2 (c) # 将结果写入FAST区域 5 8 LOAD_GLOBAL 0 (print) 10 LOAD_DEREF 0 (E) # 从DEREF区域,访问并载入外部变量 12 LOAD_FAST 2 (c) 14 CALL_FUNCTION 2 16 POP_TOP 18 LOAD_CONST 0 (None) 20 RETURN_VALUE In [263]:
FAST和DEREF数组大小是统计参数和变量得来的,对应的索引值也是编译期确定。所以不能在运行期扩张。前面曾提及,global关键字可向全局名字空间新建名字,但nonlocal不允许。
其原因就是nonlocal代表外层函数,无法动态向其FAST数组插入或追加新元素。
另外LEGB的E已被保存到DEREF数组,相应的查询过程也被优化,无须费时费力去迭代调用堆栈。所以LEGB是针对原码的说法,而非内部实现。
名字空间
问题是,为何locals函数返回的是字典类型,实际上,除非调用该函数,否则函数执行期间,根本不会创建所谓名字空间字典。也就是说,函数返回的字典是按需延迟创建,并从FAST区域复制相关信息得来的。
In [270]: def test(): ...: locals()['x'] = 100 ...: print(x) ...:
In [272]: test()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-272-fbd55f77ab7c> in <module>
----> 1 test()
<ipython-input-270-db1f3adf1c2c> in test()
1 def test():
2 locals()['x'] = 100
----> 3 print(x)
4
NameError: name 'x' is not defined
In [273]: dis.dis(test) 2 0 LOAD_CONST 1 (100) 2 LOAD_GLOBAL 0 (locals) 4 CALL_FUNCTION 0 6 LOAD_CONST 2 ('x') 8 STORE_SUBSCR 3 10 LOAD_GLOBAL 1 (print) 12 LOAD_GLOBAL 2 (x) # 编译时确定,从全局而非FAST载入 14 CALL_FUNCTION 1 16 POP_TOP 18 LOAD_CONST 0 (None) 20 RETURN_VALUE In [274]:
所以名字使用静态作用域。运行期间,对此并无影响。而另一方面,所谓的locals名字空间不过是FAST的复制品,对齐变更不会同步到FAST区域
In [276]: def test(): ...: x = 100 ...: locals()['x'] = 999 # 新建字典,进行赋值。对复制品的修改不会影响FAST ...: print('fast.x=', x) ...: print('loacls.x=',locals()['x']) # 从FAST刷新,修改丢失 ...: ...: In [277]: test() fast.x= 100 loacls.x= 100 In [278]:
至于globals能新建全局变量,并影响外部环境,是因为模块直接以字典实现名字空间,没有类似FAST的机制。
py2可通过插入exec语句影响名字作用域的静态绑定,但对py3无效
栈帧会缓存locals函数锁返回的字典,以避免每次均新建。如此,可用它存储额外的数据,比如向后续逻辑提供上下文状态等。但请注意,只有再次调用locals函数,才会刷新新字典。
In [282]: def test(): ...: x = 1 ...: d = locals() ...: print(d is locals()) # 每次返回同一个字典对象 ...: d['context'] = 'hello' # 可以存储额外数据 ...: print(d) ...: x=999 # 修改FAST时,不会主动刷新local字典 ...: print(d) # 依旧输出上次的结果 ...: print(locals()) # 刷新操作locals()操作 ...: print(d) ...: print(d is locals()) # 判断是不是同一个对象,是的 ...: print(context) # 但额外存储的数据是不能在FAST读取的 ...: ...: In [283]: test() True {'x': 1, 'd': {...}, 'context': 'hello'} {'x': 1, 'd': {...}, 'context': 'hello'} {'x': 999, 'd': {...}, 'context': 'hello'} {'x': 999, 'd': {...}, 'context': 'hello'} True --------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-283-fbd55f77ab7c> in <module> ----> 1 test() <ipython-input-282-c4ef0e734fb1> in test() 10 print(d) 11 print(d is locals()) ---> 12 print(context) 13 14 NameError: name 'context' is not defined
静态作用域
在对待作用域这个问题,编译器确实很奇怪
<ipython-input-286-b97c2c8c9d8e> in test() 1 def test(): 2 if 0: x=10 ----> 3 print(x) 4 UnboundLocalError: local variable 'x' referenced before assignment In [288]: def test(): ...: if 0: global x ...: x = 100 ...: ...: In [289]: test() In [290]: x Out[290]: 100 In [291]: def test(): ...: if 0: global x ...: x = 'hello' ...: ...: ...: In [292]: test() In [293]: x Out[293]: 'hello' In [294]:
编译器将死代码剔除了,但对其x作用域的影响依旧存在。编译的时候,不管if条件,执行的时候才关,所以x显然不是本地变量。属于局部变量
In [294]: def test(): ...: if 0: global x ...: x = 'hello' ...: In [295]: dis.dis(test) 3 0 LOAD_CONST 1 ('hello') 2 STORE_GLOBAL 0 (x) # 作用域全局 4 LOAD_CONST 0 (None) 6 RETURN_VALUE In [296]: def test(): ...: if 0: x=10 ...: print(x) ...: In [297]: dis.dis(test) 3 0 LOAD_GLOBAL 0 (print) 2 LOAD_FAST 0 (x) # 作用域 局部 4 CALL_FUNCTION 1 6 POP_TOP 8 LOAD_CONST 0 (None) 10 RETURN_VALUE In [298]:
建议
函数最好设计为存函数,或仅依赖参数、内部变量和自身属性;依赖外部状态,会给重构和测试带来诸多麻烦。
或许可将外部依赖编程keyword-only参数,如此测试就可定义依赖环境,以确保最终结果一致。
如必须依赖外部变量,则尽可能不做修改,以返回值交由调用方决策。
纯函数(pure function)输出与输入以外的状态无关,没有任何隐式依赖。相同输入总是输出相同结果,且不对外部环境产生影响。
注意区分函数和方法的设计差异。函数以逻辑为核心,通过输入条件计算结果,尽可能避免持续状态。而方法则围绕实例状态,持续展示和连续修改。
所以,方法跟实例状态共同构成了封装边界,这个函数设计理念不同。
闭包
闭包是指函数离开生成环境后,依然可记住,并持续引用语法作用域里的外部变量。
In [298]: def make(): ...: x = [1,2] ...: return lambda: print(x) ...: In [299]: a = make() In [300]: a() [1, 2] In [301]:
如果不考虑比伯因素,这段代码有很大问题。因为x生命周期是make帧栈,调用结束后理应被销毁。
LEGB仅是执行器行为,对这个示例而言,匿名函数显然无法构成引用。
但实际结果是,锁返回的匿名含糊依然可以访问x变量,就这是所谓的必要效应。
关于闭包,业界有很多学术解释。简单一点说,其就是函数和所引用环境变量的组合体。从这点上来说,闭包不等于函数,而只是形式上返回函数而已。
因引用外部状态,闭包函数自然也不是纯函数。再加上闭包会延长环境变量的生命走起,我们理应慎重使用。
创建
尽然闭包有两部分组成,创建过程分为
1、打包环境变量
2、将环境变量作为参数,新建要返回的函数对象。
因生命走起的变量,环境变量存取区从FAST转移到了DEREF
In [302]: dis.dis(make) 2 0 LOAD_CONST 1 (1) 2 LOAD_CONST 2 (2) 4 BUILD_LIST 2 6 STORE_DEREF 0 (x) # 保存DEREF 3 8 LOAD_CLOSURE 0 (x) # 闭包环境变量 10 BUILD_TUPLE 1 12 LOAD_CONST 3 (<code object <lambda> at 0x111426540, file "<ipython-input-301-5d4320eeb86b>", line 3>) 14 LOAD_CONST 4 ('make.<locals>.<lambda>') 16 MAKE_FUNCTION 8 # 创建函数时包含闭包参数 18 RETURN_VALUE Disassembly of <code object <lambda> at 0x111426540, file "<ipython-input-301-5d4320eeb86b>", line 3>: 3 0 LOAD_GLOBAL 0 (print) 2 LOAD_DEREF 0 (x) 4 CALL_FUNCTION 1 6 RETURN_VALUE In [303]:
In [303]: f = make() In [304]: dis.dis(f) 3 0 LOAD_GLOBAL 0 (print) 2 LOAD_DEREF 0 (x) # 从DEREF载入闭包环境变量 4 CALL_FUNCTION 1 6 RETURN_VALUE In [305]:
自由变量
闭包所引起的环境变量也被称为自由变量,它被保存在函数对象__closure__属性中。
In [339]: def make(): ...: x = [1,2] ...: print(hex(id(x))) ...: return lambda:print(x) ...: In [340]: f = make() 0x112a5e4b0 In [341]: f.__closure__ Out[341]: (<cell at 0x1118bbe50: list object at 0x112a5e4b0>,) In [342]: f.__closure__[0].cell_contents Out[342]: [1, 2] In [343]: make.__code__.co_freevars Out[343]: () In [344]: make.__code__.co_cellvars # 当前函数引用外部自由变量列表 Out[344]: ('x',) In [345]: f.__code__.co_freevars # 被内部闭包函数引用的变量列表 Out[345]: ('x',) In [346]:
自由变量保存在函数对象里面,每次调用,返回的函数对象也是新建的。要知道,创建闭包等于"新建函数对象,附加自由变量。"
多个闭包函数可共享同一个自由变量
In [348]: def queue(): ...: data = [] ...: push = lambda x: data.append(x) ...: pop = lambda :data.pop(0) if data else None # 这个三元表达式写的好骚啊 ...: return push,pop ...: In [349]: push,pop=queue() In [350]: push.__closure__ Out[350]: (<cell at 0x111aa1350: list object at 0x112c58460>,) In [351]: pop.__closure__ Out[351]: (<cell at 0x111aa1350: list object at 0x112c58460>,) In [352]: push(1) In [353]: push(2) In [354]: pop() Out[354]: 1 In [355]: pop() Out[355]: 2 In [356]: pop() In [357]:
闭包让函数持有状态,其可部分实现class功能。但这应局限与特定的小范围,避免隐式状态依赖对代码测试、阅读和维护造成麻烦。
给自己提醒以下,作为闭包参数,不能当做默认参数传递给内部函数。这样的话,就失去了闭包的效果。
因为内部函数会把默认参数当成自己函数参数的一部分,这样的化,外部函数运行结束,闭包参数就会被销毁。
In [357]: def m1(): ...: x = 1 ...: def m2(arg=x): ...: print(arg) ...: return m2 ...: In [358]: m = m1() In [359]: m.__closure__ In [360]: m() 1 In [361]: m1.__closure__ In [362]: def m1(): ...: x = 1 ...: def m2(): ...: print(x) ...: return m2 ...: In [363]: m = m1() In [364]: m1.__closure__ In [365]: m.__closure__ Out[365]: (<cell at 0x11315cad0: int object at 0x10ebd1f10>,) In [366]:
自引用
在函数中引用函数自己,也可构成闭包。
当def创建函数对象后,会在当前名字空间将其与函数名字关联。所以函数实例自然也可作为自由变量。
In [366]: def make(x): ...: def test(): ...: test.x = x # 引用了自己,还引用了x所以到时候有两个闭包参数 ...: print(test.x) ...: return test ...: In [367]: a,b = make(1234), make([1,2]) In [368]: a() 1234 In [369]: b() [1, 2] In [370]: a.__closure__ Out[370]: (<cell at 0x113040cd0: function object at 0x11316cb90>, <cell at 0x1130408d0: int object at 0x111a479b0>) In [371]: b.__closure__ Out[371]: (<cell at 0x113040490: function object at 0x11316c290>, <cell at 0x1130406d0: list object at 0x1122c4730>) In [372]:
这个确实比较狗逼
In [372]: dis.dis(a) 3 0 LOAD_DEREF 1 (x) 2 LOAD_DEREF 0 (test) 4 STORE_ATTR 0 (x) 4 6 LOAD_GLOBAL 1 (print) 8 LOAD_DEREF 0 (test) 10 LOAD_ATTR 0 (x) 12 CALL_FUNCTION 1 14 POP_TOP 16 LOAD_CONST 0 (None) 18 RETURN_VALUE In [373]:
延迟绑定
闭包知识绑定自由变量,并不会立即引用内容。只有当闭包函数执行时,才访问所引用的目标对象。这就有所谓的延迟绑定(late binding)现象
In [374]: def make(n): ...: x = [] ...: for i in range(n): ...: x.append(lambda : print(i)) ...: return x ...: In [375]: a,b,c = make(3) In [376]: a() 2 In [377]: b() 2 In [378]: a.__closure__ Out[378]: (<cell at 0x10f73a8d0: int object at 0x10ebd1f30>,) In [379]: c.__closure__ Out[379]: (<cell at 0x10f73a8d0: int object at 0x10ebd1f30>,) In [380]:
整理一下执行次序
1、make创建并返回3个闭包函数,引用同一个自由变量i
2、make执行结束,i等于2
3、执行闭包函数,引用并输出i的值,自然都是2
从__closure__来看,函数并不是直接存储自由变量,而是cell包装对象,以此间接引用目标。重点,间接引用目标
每个自由变量都被打包成一个cell。循环期间虽然cell也和i一样引用不同整数对象,但这对尚未执行的闭包函数没有影响。循环结束,cell引用目标确定下来,这才是闭包函数执行时的输出结果。
改成复制后,还是没用。
In [380]: def make(n): ...: x = [] ...: for i in range(n): ...: c = i ...: x.append(lambda : print(c)) ...: return x ...: In [381]: a,b,c = make(3) In [382]: a.__closure__ Out[382]: (<cell at 0x111891310: int object at 0x10ebd1f30>,) In [383]: b.__closure__ Out[383]: (<cell at 0x111891310: int object at 0x10ebd1f30>,) In [384]:
这里未能得到预期结果。原因并不复杂,变量c的作用域史i函数,而非for语句。也就是说,不管执行多少次循环,也仅有一个c存在。如此一来,闭包函数依然绑定同一自由变量,
这与复制目标对象无法。这是不同语言作用域规则不同而导致的经验错误。
最后就是将参数传递给内部函数
In [384]: def make(n): ...: x = [] ...: for i in range(n): ...: c = i ...: x.append(lambda c=c: print(c)) ...: return x ...: In [385]: a,b,c = make(3) In [386]: a() 0 In [387]: b() 1 In [388]: c() 2 In [389]: a.__closure__ In [390]:
这样就没有闭包了,前面已经介绍了为什么
优缺点
闭包的优点
闭包具备封装特性,可实现隐式上下文状态,并减少参数。在设计上,其可部分替代全局变量,或将执行环境与调用接口分离。
缺点 对自由变量隐式依赖,会提升代码的复制度,这直接影响测试和维护,其次,自由变量生命周期的提升,会提高内存占用。
应控制隐式依赖的范围和规模,能省则省
调用
这一节不是很懂,抄书了
假设解释器(interpreter)是一台ATM取款机。当储户发出'取款'指令(字节码)时,机器触发预置功能列表中与之对应的操作,以银行卡为参数,检查并修改账户数据,然后出钞。
所谓指令不过是内部某个功能的'名字'而已,其仅作为选择条件,并不参与机器运行。
在解释器内部,每条字节码指令对应一个完全由C实现的逻辑。
解释器运行在系统线程上,那如何处理内部系统代码和用户代码数据?从反汇编结果来看,就算字节码指令被解释器为内部调用,可依然有参数和返回值需要存储。
继续上面的列子解释,这里实际有连个存储空间,机器内部(系统栈)和储户钱包(用户栈)。取款时,银行卡从钱包传递到机器,最后连同钞票放回钱包。
在操作完成后,机器准备下次交易,本次数据被清除。与用户相关的数据都在钱包内。所以说,系统栈用于机器执行,用户栈存储用户代码执行状态。
当函数被调用时,会专门为其分配用户栈内存。用户栈内存除用来存储变量外,还包括字节码参数和返回值所需的空间。对系统指令来说,这里只能存放用户指令数据。如此一来,双方各有所属,确保数据互补影响。
In [390]: def add(a,b): ...: c = a+b ...: return c ...: In [391]: dis.dis(add) 2 0 LOAD_FAST 0 (a) # 从FAST读取参数a,压入用户栈 2 LOAD_FAST 1 (b) # 从FAST读取参数b,压入用户栈 4 BINARY_ADD # 系统指令从用户栈读取操作数,执行加法操作 6 STORE_FAST 2 (c) # 将结果写回FAST 3 8 LOAD_FAST 2 (c) 10 RETURN_VALUE In [392]:
如果给一个空函数,编译器没有与函数内联,没有深度优化,所以空函数也会被编译器执行。
In [394]: code = '''def test():pass;test()''' In [395]: dis.dis(compile(code,'','exec',optimize=2)) 1 0 LOAD_CONST 0 (<code object test at 0x1131d4db0, file "", line 1>) 2 LOAD_CONST 1 ('test') 4 MAKE_FUNCTION 0 # 创建函数 6 STORE_NAME 0 (test) 8 LOAD_CONST 2 (None) 10 RETURN_VALUE Disassembly of <code object test at 0x1131d4db0, file "", line 1>: 1 0 LOAD_GLOBAL 0 (test) 2 CALL_FUNCTION 0 # 调用函数 4 POP_TOP 6 LOAD_CONST 0 (None) 8 RETURN_VALUE In [396]:
调用堆栈
我们通常将进程内存分做堆(heap)和栈(stack)两类:堆可自由申请,通过指针存储自由数据;而栈则用于指令执行,与线程绑定。
函数调用与执行都依赖线程栈存储上下文和执行状态。
在函数A内调用函数B,须确保B结束后能回转到A,并继续执行后续指令。这就要求将A的后续指令地址预先存储起来。调用堆栈(call stack)的基本用途便是如此。
除返回地址外,还须为函数提供参数、局部变量存储空间。依不同调用约定,甚至要为被调用函数提供参数和返回值内存。显然,在线程栈这块内存里,每个被调用函数都划有一块保留地。我们将其称作栈帧(stack frame)
因解释执行的缘故,字节码指令数据使用独立的用户栈空间。且与系统栈连续内存不同,用户帧栈由独立对象实现,以链表形式构成完整的调用堆栈。其好处是不受系统栈大小的制约,缺点是性能方面要差一点。
但考虑到它只存储数据,实际执行过程依然调用系统栈完成,这倒也能接受。
因栈帧使用频繁,系统会缓存200个栈帧对象,并按实际所需调整内存大小
操作系统堆线程栈大小的限制可使用ulimit -s查看,z最新84位系统通常为8MB
一旦函数执行(比如递归)内存超出限制,就会引发堆栈溢出(stack overflow)错误
In [398]: def add(x, y): ...: return x+ y ...: In [399]: def test(): ...: x = 10 ...: y = 20 ...: z = add(x,y) ...: print(z) ...: In [400]: dis.dis(test) 2 0 LOAD_CONST 1 (10) 2 STORE_FAST 0 (x) 3 4 LOAD_CONST 2 (20) 6 STORE_FAST 1 (y) 4 8 LOAD_GLOBAL 0 (add) # 将待调用函数add入栈 10 LOAD_FAST 0 (x) # 将变量x入栈 12 LOAD_FAST 1 (y) # 将变量y入栈 14 CALL_FUNCTION 2 # 调用函数 16 STORE_FAST 2 (z) # 将返回值从栈保存到变量区 5 18 LOAD_GLOBAL 1 (print) 20 LOAD_FAST 2 (z) 22 CALL_FUNCTION 1 24 POP_TOP # 清楚print返回值,确保栈平衡 26 LOAD_CONST 0 (None) 28 RETURN_VALUE In [401]:
调用堆栈常出现在调试工具中,用于检视调用过程,以及各种环境变量取值。当然,也可在代码中使用,比如获取上级函数设置的上下文信息。
函数sys._getframe可访问调用堆栈内不同层次的栈帧对象。参数0为当前函数,1为上级函数。
In [401]: def A(): ...: x = 'func A' ...: B() ...: In [402]: def B(): ...: C() ...: In [403]: import sys In [406]: def C(): ...: f = sys._getframe(2) # 向上2级,获取A栈帧 ...: print(f.f_code) # A代码对象 ...: print(f.f_locals) # A 名字空间 ...: print(f.f_lasti) # A 最后执行指令偏移量 ...: print(dir(f)) ...: ...: In [407]: A() <code object A at 0x11188d810, file "<ipython-input-401-754088d9f97c>", line 1> {'x': 'func A'} 6 ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace', 'f_trace_lines', 'f_trace_opcodes']
In [408]: dis.dis(A) 2 0 LOAD_CONST 1 ('func A') 2 STORE_FAST 0 (x) 3 4 LOAD_GLOBAL 0 (B) 6 CALL_FUNCTION 0 # A.lasti 8 POP_TOP 10 LOAD_CONST 0 (None) 12 RETURN_VALUE In [409]:
请注意,无论是在函数内调用globals,还是frame.f_flobals访问,总返回定义该函数模块名字空间,而非调用出。
另有sys._current_frame返回所有线程的当前栈帧,用来确定解释器的工作状态。只是文档里面这两个函数都标记为内部使用。可用标准补课inspect进行替代,它拥有更多操作函数。
如果只是输出调用过程,可使用traceback模块,这类似于解释器输出错误信息。
书中用了inspect.stack()方法,代码不抄写了,看不懂具体什么用。
递归
递归深度有限,可使用sys.getrecursionlimit()与sys.setrecursionlimit(50)查看与设置
import sys sys.getrecursionlimit() Out[3]: 3000 sys.setrecursionlimit(50) sys.getrecursionlimit() Out[5]: 50
递归常被用来改善循环操作,比如树状结构变量。当然,它须承担函数调用的额外卡西奥,类似栈帧创建等。在不支持尾递归优化的情况下,这种负担尤为突出。
比如,函数A的最后动作是调用B,并直接返回B的结果。那么A的栈帧状态就无须保留,其内存可直接被B覆盖使用。另外,将函数调用优化成跳转指令,可以大大提升执行性能。
比如方式,被称作尾调用消除或尾调用优化
如果A尾调用自身,那么就成了尾递归。鉴于重复使用同一栈帧内存,这可避免堆栈溢出。不过CPython因为实现方式的问题,对此并不支持。
包装
另外书中就偏函数,functools.partial
对已有函数,可通过包装形式改变其参数列表,使其符合特定调用接口
In [1]: def test(a,b,c): ...: print(locals()) ...: In [2]: import functools In [3]: t = functools.partial(test,b=2,c=2) In [4]: t(5) {'a': 5, 'b': 2, 'c': 2} In [5]:
原理书上书很简单,大神就是不一样,在调用原目标既可
实现的伪码
In [11]: def partial(func, *part_args, **part_kwargs): ...: def wrap(*call_args, **call_kwargs): ...: kwargs = part_kwargs.copy() ...: kwargs.update(call_kwargs) ...: return func(*part_args,*call_args,**kwargs) ...: return wrap ...: ...: In [12]: t= partial(test,1,2) In [13]: t(3) {'a': 1, 'b': 2, 'c': 3} In [14]:
基本合并规则
1、包装位置参数优先
2、调用键值参数覆盖包装键值参数
3、合并后不能对单个目标参数多次复制
In [12]: t= partial(test,1,2) In [13]: t(3) {'a': 1, 'b': 2, 'c': 3} In [14]: functools.partial(test,1,2)(3) {'a': 1, 'b': 2, 'c': 3} In [15]: functools.partial(test,1,c=2)(2,c=99) {'a': 1, 'b': 2, 'c': 99} In [16]: t = functools.partial(test,1,2) In [17]: t.func Out[17]: <function __main__.test(a, b, c)> In [18]: t.args Out[18]: (1, 2) In [19]: t.keywords Out[19]: {} In [20]: