python 函数

当编译器遇到 def,会⽣生成创建函数对象指令。也就是说 def 是执⾏行指令,⽽而不仅仅是个语法关键
字。可以在任何地⽅方动态创建函数对象。
⼀一个完整的函数对象由函数和代码两部分组成。其中,PyCodeObject 包含了字节码等执⾏行数据,
⽽而 PyFunctionObject 则为其提供了状态信息。
函数声明:
def name([arg,... arg = value,... *arg, **kwarg]):
suite
结构定义:
typedef struct {
PyObject_HEAD
PyObject *func_code;? ? ? // PyCodeObject
PyObject *func_globals;? ? // 所在模块的全局名字空间
PyObject *func_defaults;? ? // 参数默认值列表
PyObject *func_closure;? ? // 闭包列表
PyObject *func_doc;? ? ? // __doc__
PyObject *func_name;? ? ? // __name__
PyObject *func_dict;? ? ? // __dict__
PyObject *func_weakreflist;? ? // 弱引⽤用链表
PyObject *func_module;? ? ? // 所在 Module

} PyFunctionObject;


1 创建
包括函数在内的所有对象都是第⼀一类对象,可作为其他函数的实参或返回值。
• 在名字空间中,名字是唯⼀一主键。因此函数在同⼀一范围内不能 "重载 (overload)"。
• 函数总是有返回值。就算没有 return,默认也会返回 None。
• ⽀支持递归调⽤用,但不进⾏行尾递归优化。最⼤大深度 sys.getrecursionlimit()。
>>> def test(name):
... if name == "a":
... def a(): pass
... return a
... else:
... def b(): pass
... return b
>>> test("a").__name__
'a'
61
不同于⽤用 def 定义复杂函数,lambda 只能是有返回值的简单的表达式。使⽤用赋值语句会引发语法
错误,可以考虑⽤用函数代替。
>>> add = lambda x, y = 0: x + y
>>> add(1, 2)
3
>>> add(3)? ? ? # 默认参数
3
>>> map(lambda x: x % 2 and None or x, range(10))

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


2 参数
函数的传参⽅方式灵活多变,可按位置顺序传参,也可不关⼼心顺序⽤用命名实参。
>>> def test(a, b):
... print a, b
>>> test(1, "a")? ? ? # 位置参数
1 a
>>> test(b = "x", a = 100)? ? # 命名参数
100 x
⽀支持参数默认值。不过要⼩小⼼心,默认值对象在创建函数时⽣生成,所有调⽤用都使⽤用同⼀一对象。如果该
默认值是可变类型,那么就如同 C 静态局部变量。
>>> def test(x, ints = []):
... ints.append(x)
... return ints
>>> test(1)
[1]
>>> test(2)? ? ? ? # 保持了上次调⽤用状态。
[1, 2]
>>> test(1, [])? ? ? # 显式提供实参,不使⽤用默认值。
[1]
>>> test(3)? ? ? ? # 再次使⽤用默认值。
[1, 2, 3]
62
默认参数后⾯面不能有其他位置参数,除⾮非是变参。
>>> def test(a, b = 0, c): pass
SyntaxError: non-default argument follows default argument
>>> def test(a, b = 0, *args, **kwargs): pass
⽤用 *args 收集 "多余" 的位置参数,**kwargs 收集 "额外" 的命名参数。这两个名字只是惯例,可
⾃自由命名。
>>> def test(a, b, *args, **kwargs):
... print a, b
... print args
... print kwargs
>>> test(1, 2, "a", "b", "c", x = 100, y = 200)
1 2
('a', 'b', 'c')
{'y': 200, 'x': 100}
变参只能放在所有参数定义的尾部,且 **kwargs 必须是最后⼀一个。
>>> def test(*args, **kwargs):? ? ? # 可以接收任意参数的函数。
... print args
... print kwargs
>>> test(1, "a", x = "x", y = "y")? ? # 位置参数,命名参数。
(1, 'a')
{'y': 'y', 'x': 'x'}
>>> test(1)? ? ? ? ? ? # 仅传位置参数。
(1,)
{}
>>> test(x = "x")? ? ? ? ? # 仅传命名参数。
()
{'x': 'x'}
可 "展开" 序列类型和字典,将全部元素当做多个实参使⽤用。如不展开的话,那仅是单个实参对象。
>>> def test(a, b, *args, **kwargs):
... print a, b
... print args
... print kwargs
>>> test(*range(1, 5), **{"x": "Hello", "y": "World"})
1 2
(3, 4)
63
{'y': 'World', 'x': 'Hello'}
单个 "*" 展开序列类型,或者仅是字典的主键列表。"**" 展开字典键值对。但如果没有变参收集,
展开后多余的参数将引发异常。
>>> def test(a, b):
... print a
... print b
>>> d = dict(a = 1, b = 2)
>>> test(*d)?? ? ? ? # 仅展开 keys(),test("a"、"b")。
a
b
>>> test(**d)? ? ? ? ? # 展开 items(),test(a = 1, b = 2)。
1
2
>>> d = dict(a = 1, b = 2, c = 3)
>>> test(*d)?? ? ? ? # 因为没有位置变参收集多余的 "c",导致出错。
TypeError: test() takes exactly 2 arguments (3 given)
>>> test(**d)? ? ? ? ? # 因为没有命名变参收集多余的 "c = 3",导致出错。
TypeError: test() got an unexpected keyword argument 'c'
lambda 同样⽀支持默认值和变参,使⽤用⽅方法完全⼀一致。
>>> test = lambda a, b = 0, *args, **kwargs: ?? ? \
... sum([a, b] + list(args) + kwargs.values())
>>> test(1, *[2, 3, 4], **{"x": 5, "y": 6})

21


3 作⽤用域
函数形参和内部变量都存储在 locals 名字空间中。
>>> def test(a, *args, **kwargs):
... s = "Hello, World!"
... print locals()
>>> test(1, "a", "b", x = 10, y = "hi")
{
? 'a': 1,
? 'args': ('a', 'b'),
64
? 'kwargs': {'y': 'hi', 'x': 10}
? 's': 'Hello, World!',
}
除⾮非使⽤用 global、nonlocal 特别声明,否则在函数内部使⽤用赋值语句,总是在 locals 名字空间中
新建⼀一个对象关联。注意:"赋值" 是指名字指向新的对象,⽽而⾮非通过名字改变对象状态。
>>> x = 10
>>> hex(id(x))
'0x7fb8e04105e0'
>>> def test():
... x = "hi"
... print hex(id(x)), x
>>> test()? ? ? ? # 两个 x 指向不同的对象。
0x10af2b490 hi
>>> x? ? ? ? ? # 外部变量没有被修改。
10
如果仅仅是引⽤用外部变量,那么按 LEGB 顺序在不同作⽤用域查找该名字。
名字查找顺序: locals -> enclosing function -> globals -> __builtins__
• locals: 函数内部名字空间,包括局部变量和形参。
• enclosing function: 外部嵌套函数的名字空间。
• globals: 函数定义所在模块的名字空间。
• __builtins__: 内置模块的名字空间。
想想看,如果将对象引⼊入 __builtins__ 名字空间,那么就可以在任何模块中直接访问,如同内置函
数那样。不过鉴于 __builtins__ 的特殊性,这似乎不是个好主意。
>>> __builtins__.b = "builtins"
>>> g = "globals"
>>> def enclose():
... e = "enclosing"
... def test():
... l = "locals"
... print l
... print e
... print g
... print b
...
65
... return test
>>> t = enclose()
>>> t()
locals
enclosing
globals
builtins
通常内置模块 __builtin__ 在本地名字空间的名字是 __builtins__ (多了个 s 结尾)。但要记住这说
法⼀一点也不靠谱,某些时候它⼜又会莫名其妙地指向 __builtin__.__dict__。如实在要操作该模块,
建议显式 import __builtin__。
27.3. __builtin__ — Built-in objects
CPython implementation detail: Most modules have the name __builtins__ (note the 's') made available as part
of their globals. The value of __builtins__ is normally either this module or the value of this modules’s __dict__
attribute. Since this is an implementation detail, it may not be used by alternate implementations of Python.
现在,获取外部空间的名字没问题了,但如果想将外部名字关联到⼀一个新对象,就需要使⽤用 global
关键字,指明要修改的是 globals 名字空间。Python 3 还提供了 nonlocal 关键字,⽤用来修改外部
嵌套函数名字空间,可惜 2.7 没有。
>>> x = 100
>>> hex(id(x))
0x7f9a9264a028
>>> def test():
... global x, y? ? ? # 声明 x, y 是 globals 名字空间中的。
... x = 1000? ? ? # globals()["x"] = 1000
... y = "Hello, World!"? # globals()["y"] = "..."。 新建名字。
... print hex(id(x))
>>> test()? ? ? ? # 可以看到 test.x 引⽤用的是外部变量 x。
0x7fdfba4abb30
>>> print x, hex(id(x))? ? # x 被修改。外部 x 指向新整数对象 1000。
1000 0x7fdfba4abb30
>>> x, y? ? ? ? # globals 名字空间中出现了 y。
(1000, 'Hello, World!')
没有 nonlocal 终归有点不太⽅方便,要实现类似功能稍微有点⿇麻烦。
>>> from ctypes import pythonapi, py_object
66
>>> from sys import _getframe
>>> def nonlocal(**kwargs):
... f = _getframe(2)
... ns = f.f_locals
... ns.update(kwargs)
... pythonapi.PyFrame_LocalsToFast(py_object(f), 0)
>>> def enclose():
... x = 10
...
... def test():
... nonlocal(x = 1000)
...
... test()
... print x
>>> enclose()
1000
这种实现通过 _getframe() 来获取外部函数堆栈帧名字空间,存在⼀一些限制。因为拿到是调⽤用者,
⽽而不⼀一定是函数创建者。
需要注意,名字作⽤用域是在编译时确定的。⽐比如下⾯面例⼦子的结果,会和设想的有很⼤大差异。究其原
因,是编译时并不存在 locals x 这个名字。
>>> def test():
... locals()["x"] = 10
... print x
>>> test()
NameError: global name 'x' is not defined
要解决这个问题,可动态访问名字,或使⽤用 exec 语句,解释器会做动态化处理。
>>> def test():
... exec ""? ? ? ? # 空语句。
... locals()["x"] = 10
... print x
>>> test()
10
>>> def test():
... exec "x = 10"? ? ? # exec 默认使⽤用当前名字空间。
... print x
>>> test()
67
10
如果函数中包含 exec 语句,编译器⽣生成的名字指令会依照 LEGB 规则搜索。继续看下⾯面的例⼦子。
>>> x = "abc"
>>> def test():
... print x
... exec "x = 10"
... print x
>>> test()
abc
10
解释器会将 locals 名字复制到 FAST 区域来优化访问速度,因此直接修改 locals 名字空间并不会
影响该区域。解决⽅方法还是⽤用 exec。
>>> def test():
... x = 10
...
... locals()["x"] = 100? # 该操作不会影响 FAST 区域,只不过指向⼀一个新对象。
... print x? ? ? # 使⽤用 LOAD_FAST 访问 FAST 区域名字,依然是原对象。
...
... exec "x = 100"? ? # 同时刷新 locals 和 FAST。
... print x
>>> test()
10
100
另外,编译期作⽤用域不受执⾏行期条件影响。
>>> def test():
... if False:
... global x? ? # 尽管此语句永不执⾏行,但编译器依然会将 x 当做 globals 名字。
... x = 10
... print globals()["x"] is x
>>> test()
True
>>> x
10
>>> def test():
... if False:
... x = 10? ? ? # 同理,x 是 locals 名字。后⾯面出错也就很正常了。
... print x
68
>>> test()
UnboundLocalError: local variable 'x' referenced before assignment

其中细节,可以⽤用 dis 反编译查看⽣生成的字节指令。


4 闭包

闭包是指:当函数离开创建环境后,依然持有其上下⽂文状态。⽐比如下⾯面的 a 和 b,在离开 test 函数
后,依然持有 test.x 对象。
>>> def test():
... x = [1, 2]
... print hex(id(x))
...
... def a():
... x.append(3)
... print hex(id(x))
...
... def b():
... print hex(id(x)), x
...
... return a, b
>>> a, b = test()
0x109b925a8? ? ? ? ? # test.x
>>> a()
0x109b925a8? ? ? ? ? # 指向 test.x
>>> b()
0x109b925a8 [1, 2, 3]
实现⽅方式很简单,以上例来解释:
test 在创建 a 和 b 时,将它们所引⽤用的外部对象 x 添加到 func_closure 列表中。因为 x 引⽤用计
数增加了,所以就算 test 堆栈帧没有了,x 对象也不会被回收。
>>> a.func_closure
(<cell at 0x109e0aef8: list object at 0x109b925a8>,)
>>> b.func_closure
(<cell at 0x109e0aef8: list object at 0x109b925a8>,)
69
为什么⽤用 function.func_closure,⽽而不是堆栈帧的名字空间呢?那是因为 test 仅仅返回两个函数
对象,并没有调⽤用它们,⾃自然不可能为它们创建堆栈帧。这样⼀一来,就导致每次返回的 a 和 b 都是
新建对象,否则这个闭包状态就被覆盖了。
>>> def test(x):
... def a():
... print x
...
... print hex(id(a))
... return a
>>> a1 = test(100)? ? ? ? # 每次创建 a 都提供不同的参数。
0x109c700c8
>>> a2 = test("hi")? ? ? ? # 可以看到两次返回的函数对象并不相同。
0x109c79f50
>>> a1()? ? ? ? ? # a1 的状态没有被 a2 破坏。
100
>>> a2()
hi
>>> a1.func_closure? ? ? ? # a1、a2 持有的闭包列表是不同的。
(<cell at 0x109e0cf30: int object at 0x7f9a92410ce0>,)
>>> a2.func_closure
(<cell at 0x109d3ead0: str object at 0x109614490>,)
>>> a1.func_code is a2.func_code?? # 这个很好理解,字节码没必要有多个。
True
通过 func_code,可以获知闭包所引⽤用的外部名字。
• co_cellvars: 被内部函数引⽤用的名字列表。
• co_freevars: 当前函数引⽤用外部的名字列表。
>>> test.func_code.co_cellvars? ? # 被内部函数 a 引⽤用的名字。
('x',)
>>> a.func_code.co_freevars? ? # a 引⽤用外部函数 test 中的名字。
('x',)
使⽤用闭包,还需注意 "延迟获取" 现象。看下⾯面的例⼦子:
>>> def test():
... for i in range(3):
... def a():
70
... print i
... yield a
>>> a, b, c = test()
>>> a(), b(), c()
2
2
2
为啥输出的都是 2 呢?
⾸首先,test 只是返回函数对象,并没有执⾏行。其次,test 完成 for 循环时,i 已经等于 2,所以执

⾏行 a、b、c 时,它们所持有 i ⾃自然也就等于 2。


5 堆栈帧
Python 堆栈帧基本上就是对 x86 的模拟,⽤用指针对应 BP、SP、IP 寄存器。堆栈帧成员包括函数
执⾏行所需的名字空间、调⽤用堆栈链表、异常状态等。
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back;? ? // 调⽤用堆栈 (Call Stack) 链表
PyCodeObject *f_code;?? // PyCodeObject
PyObject *f_builtins;?? // builtins 名字空间
PyObject *f_globals;? ? // globals 名字空间
PyObject *f_locals;? ? // locals 名字空间
PyObject **f_valuestack;? // 和 f_stacktop 共同维护运⾏行帧空间,相当于 BP 寄存器。
PyObject **f_stacktop;? ? // 运⾏行栈顶,相当于 SP 寄存器的作⽤用。
PyObject *f_trace;? ? // Trace function
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback; // 记录当前栈帧的异常信息
PyThreadState *f_tstate;? // 所在线程状态
int f_lasti;? ? ? // 上⼀一条字节码指令在 f_code 中的偏移量,类似 IP 寄存器。
int f_lineno;? ? ? // 与当前字节码指令对应的源码⾏行号
... ...
PyObject *f_localsplus[1]; ? // 动态申请的⼀一段内存,⽤用来模拟 x86 堆栈帧所在内存段。
} PyFrameObject;
可使⽤用 sys._getframe(0) 或 inspect.currentframe() 获取当前堆栈帧。其中 _getframe() 深度参
数为 0 表⽰示当前函数,1 表⽰示调⽤用堆栈的上个函数。除⽤用于调试外,还可利⽤用堆栈帧做些有意思的
事情。

71


权限管理
通过调⽤用堆栈检查函数 Caller,以实现权限管理。
>>> def save():
... f = _getframe(1)
... if not f.f_code.co_name.endswith("_logic"): ? # 检查 Caller 名字,限制调⽤用者⾝身份。
... raise Exception("Error!")? ? ? # 还可以检查更多信息。
... print "ok"
>>> def test(): save()
>>> def test_logic(): save()
>>> test()
Exception: Error!
>>> test_logic()

ok


上下⽂文
通过调⽤用堆栈,我们可以隐式向整个执⾏行流程传递上下⽂文对象。 inspect.stack ⽐比 frame.f_back
更⽅方便⼀一些。
>>> import inspect
>>> def get_context():
... for f in inspect.stack():? ? ? # 循环调⽤用堆栈列表。
... context = f[0].f_locals.get("context")? # 查看该堆栈帧名字空间中是否有⺫⽬目标。
... if context: return context?? ? # 找到了就返回,并终⽌止查找循环。
>>> def controller():
... context = "ContextObject"? ? ? # 将 context 添加到 locals 名字空间。
... model()
>>> def model():
... print get_context()? ? ? ? # 通过调⽤用堆栈查找 context。
>>> controller()? ? ? ? ? ? # 测试通过。
ContextObject
sys._current_frames 返回所有线程的当前堆栈帧对象。
虚拟机会缓存 200 个堆栈帧复⽤用对象,以获得更好的执⾏行性能。整个程序跑下来,天知道要创建
多少个这类对象。

72


6 包装
⽤用 functools.partial() 可以将函数包装成更简洁的版本。
>>> from functools import partial
>>> def test(a, b, c):
... print a, b, c
>>> f = partial(test, b = 2, c = 3)? # 为后续参数提供命名默认值。
>>> f(1)
1 2 3
>>> f = partial(test, 1, c = 3)? ? # 为前⾯面的位置参数和后⾯面的命名参数提供默认值。
>>> f(2)
1 2 3
partial 会按下⾯面的规则合并参数。
def partial(func, *d_args, **d_kwargs):
def wrap(*args, **kwargs):
new_args = d_args + args?? ? # 合并位置参数,partial 提供的默认值优先。
new_kwargs = d_kwargs.copy()? ? # 合并命名参数,partial 提供的会被覆盖。
new_kwargs.update(kwargs)
return func(*new_args, **new_kwargs)
return wrap
???
与函数相关内容很多,涉及虚拟机底层实现。还要分清函数和对象⽅方法的差别,后⾯面会详细说明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值