《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]:
浙公网安备 33010602011771号