Python的名字空间

名字空间

Python中赋值语句的两个步骤:
1. 创建一个对象obj
2. 将Obj赋值给一个名字name
由此可见,名字是Python中访问对象的唯一方式。而名字和对象是通过(name,obj)进行关联的。这个关联关系叫做约束,而约束的容身之处,便是名字空间

  要理解Python的名字空间,我想最好的方式去CPython的源代码中去寻找它的实现机制。
  CPython虚拟机是对X86栈帧的模拟,而在CPython的内部,是通过一个结构体来实现的:PyFrameObject,可以说,这个结构体是CPython字节码真正被执行的地方,代码位于Include/frameobject.h中:

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;
    PyCodeObject *f_code;   /* code segment */
    PyObject *f_builtins;   /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;    /* global symbol table (PyDictObject) */
    PyObject *f_locals;     /* local symbol table (any mapping) */
    PyObject **f_valuestack;
    PyObject **f_stacktop;
   //...
} PyFrameObject;
  • f_back:栈链表指针,用来指向上一个执行的栈帧
  • f_code:当前栈执行的代码信息,存放在一个PyCodeObject
  • f_builtins:内建名字空间
  • f_globals:全局名字空间
  • f_locals:局部名字空间

当一个符号出现的时候,Python会沿着locals->globals->builtins的路径搜索下去。

  由此可以看出,一个栈帧(执行环境)中,维护了三个名字空间:builtins、globals、locals,我们可以通过sys._getframe()获取到当前执行的栈帧,并将这三个名字空间输出:

>>> import sys
>>> fr = sys._getframe()
>>> fr
<frame object at 0x01F49C70>
>>> fr.f_locals
{'__name__': '__main__', '__spec__': None, 'sys': <module 'sys' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, 'fr': <frame object at 0x01F49C70>, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__doc__': None}
>>> fr.f_globals
{'__name__': '__main__', '__spec__': None, 'sys': <module 'sys' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, 'fr': <frame object at 0x01F49C70>, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__doc__': None}
>>> fr.f_builtins

  当然,我们也可以用内置函数locals()globals()分别来获取局部名字空间和全局名字空间中的符号。需要注意的是,这两种获取方式会出现一定的区别,下面我们会看到。
  最后,我们可以得到栈帧的一个运行时状态图:

LGB规则

  在之前的名字空间的讲述中,我说:当一个符号出现的时候,Python会沿着locals->globals->builtins的路径搜索下去。我们把这个规则叫做,LGB规则。在学习LEGB规则之前,我们很有必要看一看LGB规则是什么?

a = 1
def f():     
    a = 2
    print(a)    //[1]
print(a)        //[2]

  按照LGB规则,很明显[1]处输出的a值为2,[2]处输出的a值为1。

最内嵌套作用域规则(LEGB)

由一个赋值语句引进的名字在这个赋值语句所在的作用域里是可见的,而且在其内部嵌套的每一个作用域也是可见的,除非它被嵌套于内部的,引进相同名字的另一条赋值语句所覆盖。

  我们在Python中经常会见到这样的代码:

i = 100
def f():
    i = 10
    def g():
        def h():
            print(i)
        return h
    return g

  以上代码进行了函数的嵌套定义,我通过如下代码进行执行:

gg = f()
hh = gg()
hh()

最后的hh()调用了嵌套定义的最内层函数,其作用是输出变量i的值。我们看到在代码中一共有两个变量i,一个定义在了函数外,一个定义在了函数f()的内部。那么,输出值是多少呢?答案是10,即输出值为函数f()中定义的i

作用域是一个空间概念,它仅仅由源代码的文本上下文决定。

  上述代码之所以会输出10,就是因为在寻找i的时候,遵循了最内嵌套规则(E:enclosing,直接外围作用域),我们从源代码print(i)层层向外寻找,最先寻找到的就是i=10
  代码中像是将i值和函数进行了捆绑,这就是闭包

  1. 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
  2. 最内嵌套作用域规则是语言设计时的设计策略;而闭包则是实现该策略的一种方案。

闭包初探

  闭包是引入了自由变量的函数,那么这个自由变量和函数会一同被CPython维护,以满足LEGB规则,我们把上述代码稍作更改:

i = 100
def f():
    i = 10
    print(locals())
    def g():
        def h():
            print(locals())
            print(i)
        print(locals())
        return h
    return g

  每步调用的输出结果如下:

>>> gg = f()
{'i': 10}
>>> hh = gg()
{'h': <function f.<locals>.g.<locals>.h at 0x01EBF270>, 'i': 10}
>>> hh()
{'i': 10}
10

  有上述代码可见:确实i的值在函数调用的过程中被层层传递进入了函数h()中,locals()函数的作用是输出局部名字空间中维护的符号。但i真的是被维护在了f_locals的栈帧中了吗?我们把上面的代码继续做一个改进:

import sys
i = 100
def f():
    i = 10
    fr = sys._getframe()
    print(fr.f_locals)
    def g():
        def h():
            hr = sys._getframe()
            print(hr.f_locals)
            print(i)
        gr = sys._getframe()
        print(gr.f_locals)
        return h
    return g

  结果如下:

>>> gg = f()
{'i': 10, 'fr': <frame object at 0x01822B70>}
>>> hh = gg()
{'i': 10, 'gr': <frame object at 0x01822E40>, 'h': <function f.<locals>.g.<locals>.h at 0x0187F198>}
>>> hh()
{'i': 10, 'hr': <frame object at 0x0180C710>}
10

  关于闭包的具体实现,我们会在后面详细介绍。