离线下载
PDF版 ePub版

qyuhen · 更新于 2018-11-28 11:00:43

函数

当编译器遇到 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;

创建

包括函数在内的所有对象都是第一类对象,可作为其他函数的实参或返回值。

  • 在名字空间中,名字是唯一主键。因此函数在同一范围内不能 "重载 (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'

不同于用 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]

参数

函数的传参方式灵活多变,可按位置顺序传参,也可不关心顺序用命名实参。

>>> 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]

默认参数后面不能有其他位置参数,除非是变参。

>>> 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)
{'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

作用域

函数形参和内部变量都存储在 locals 名字空间中。

>>> def test(a, *args, **kwargs):
...     s = "Hello, World"
...     print locals()

>>> test(1, "a", "b", x = 10, y = "hi")
{
    'a': 1,
    'args': ('a', 'b'),
    '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
...
...         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 partof 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
>>> 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()
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

>>> test()
UnboundLocalError: local variable 'x' referenced before assignment

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

闭包

闭包是指:当函数离开创建环境后,依然持有其上下文状态。比如下面的 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>,)

为什么用 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():
...             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。

堆栈帧

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 表示调用堆栈的上个函数。除用于调试外,还可利用堆栈帧做些有意思的事情。

权限管理

通过调用堆栈检查函数 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 个堆栈帧复用对象,以获得更好的执行性能。整个程序跑下来,天知道要创建多少个这类对象。

包装

用 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

与函数相关内容很多,涉及虚拟机底层实现。还要分清函数和对象方法的差别,后面会详细说明。

上一篇: 表达式 下一篇: 迭代器