定义
函数因减少依赖关系,具备良好的可测试性和可维护性,这是性能优化的关键所在。另外,我们还应遵循一个基本元祖,就是专注于左一件事,不受外在干扰和污染。
函数要短而精,使用最小作用域。如有可能,应确保其行为的一致性。如果逻辑受参数影响而有所不同,那应该将更多个逻辑分支分别重构成独立函数,使其从'变'转为'不变'.
创建
函数由两部分组成:代码对象持有的字节码和指令元数据,负责执行;函数对象则为上下文提供调用实例,并管理所需的状态数据.
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)输出与输入以外的状态无关,没有任何隐式依赖。相同输入总是输出相同结果,且不对外部环境产生影响。
注意区分函数和方法的设计差异。函数以逻辑为核心,通过输入条件计算结果,尽可能避免持续状态。而方法则围绕实例状态,持续展示和连续修改。
所以,方法跟实例状态共同构成了封装边界,这个函数设计理念不同。