python源码解析读书笔记(二)——函数特性
1.函数的性质
>>> def outer(o1, o2):
… def inner(i1 = 10, i2 = []):
… return i1+o1+o2
… return inner
…
>>> a1 = outer(50, 30)
>>> a2 = outer(50, 30)
>>> a1.func_closure
(<cell at 0xb75454f4: int object at 0x8455ddc>, <cell at 0xb7545524: int object at 0x8455cec>)
>>> a2.func_closure
(<cell at 0xb754541c: int object at 0x8455ddc>, <cell at 0xb75453a4: int object at 0x8455cec>)
两次生成的函数对象拥有不同的闭包空间。
>>> a1.func_defaults
(10, [])
>>> a2.func_defaults
(10, [])
>>> a1.func_defaults[1].append(10)
>>> a1.func_defaults
(10, [10])
>>> a2.func_defaults
(10, [])
也拥有不同的默认值空间。
>>> def default_test(d = []):
… print d
…
>>> default_test.func_defaults
([],)
>>> default_test.func_defaults[0].append(10)
>>> default_test()
[10]
然而同一次生成的默认值空间是共享的,哪怕多次运行。
\
2.参数传递
>>> def f(a,b,c,d): return a,b,c,d
…
>>> f(1,2,3,4)
(1, 2, 3, 4)
>>> f(1,2,a=3,b=4)
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
TypeError: f() got multiple values for keyword argument ‘a’
>>> f(1,2,c=3,d=4)
(1, 2, 3, 4)
参数分两种,位置参数和键值参数。具体如何传递是由调用时决定而非编译时。调用时参数必须先以位置参方式传递,再以键值参方式传递。一旦出现键值传递,再出现位置传递即出现编译时非法。调用时会先入栈所有参数,一个位置参占一个对象,一个键值参占两个对象(这是当然)。
解析的时候按照先位置后键值的方式赋值,先将所有位置参依次赋值给所有参数名。如果位置参有多,而没有扩展位置参来接收,则报错TypeError: %s expected at %s %d arguments, got %d。而后将所有键值参赋值给未赋值的参数,如果这个参数名已经赋值,则如上文,报错。如果键值参数有多,又没有扩展键值参来接受,也报错。
最后,如果有参数名尚未赋值,查看这些参数名是否有默认值。如果没有,报错。
另外,在字节码中访问本地(locals)命名空间的时候,是不通过命名空间查询的方式进行的。因为编译时可以明确一个名称是否在locals空间中,而不用理会代码段在名称空间中的位置结构。而一旦明确其在locals命名空间中,则可以直接堆栈访问位置,这样使得locals名称查询速度远高于普通名称空间。对于一个函数内频繁使用的符号,建议做一次赋值,将其引入locals命名空间。
\
3.调用堆栈
python的调用堆栈是通过PyFrameObject来实现的,每一次调用,python会产生一个新的PyFrameObject加入到栈中。而每个PyFrameObject自带一个小数据区域,用于接收参数,处理局部变量。python字节码指令中的LOAD_FAST,STORE_FAST就是操作的这个区域。
\
4.层级闭包的实现
>>> def f1():
… def f2(): return i
… i = 10
… return f2
…
>>> a = f1()
>>> a()
10
实现的还是不错的。通过计算当时名称-值的方法就无法获得i。
>>> def f1():
… def f2():
… return inet_aton
… from socket import *
… return f2
…
<stdin>:1: SyntaxWarning: import * only allowed at module level
File “<stdin>”, line 4
SyntaxError: import * is not allowed in function ‘f1’ because it is contains a nested function with free variables
这主要是因为闭包的实现是通过函数编译时名称层状传递。例子1在编译时,f2知道上层作用域中有一个名叫i的变量,于是f2的freevars属性就为i。而当f1操作i时,f2保持了一个对结果的引用。当f1返回f2函数对象时,自身的PyFrameObject消失了没错,但是f2中对结果的引用还保存在了func_closure中。当from socket import *的时候,当前locals空间名称会发生变化,从而导致动态引入的名称无法在f2中生效。