python 虚拟机框架-名字、作用域和名字空间

python的虚拟机是python的核心,在.py源代码被编译器编译为字节码指令序列后,就将由python的虚拟机接手整个工作。python的虚拟机将从编译得到的PyCodeObject对象中依次读入每一条字节码指令,并在当前的上下文环境中执行这条字节码指令。如此反复运行,所有由python源代码所规定的动作都会如期望一样,一 一展开。

PyFrameObject(执行环境)

[frameobject.h]

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;      /* previous frame, or NULL */
    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;    /* points after the last local */
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
       Frame evaluation usually NULLs it, but a frame that yields sets it
       to the current stack top. */
    PyObject **f_stacktop;
    PyObject *f_trace;          /* Trace function */
    char f_trace_lines;         /* Emit per-line trace events? */
    char f_trace_opcodes;       /* Emit per-opcode trace events? */

    /* Borrowed reference to a generator, or NULL */
    PyObject *f_gen;

    int f_lasti;                /* Last instruction if called */
    /* Call PyFrame_GetLineNumber() instead of reading this field
       directly.  As of 2.3 f_lineno is only valid when tracing is
       active (i.e. when f_trace is set).  At other times we use
       PyCode_Addr2Line to calculate the line from the current
       bytecode index. */
    int f_lineno;               /* Current line number */
    int f_iblock;               /* index in f_blockstack */
    char f_executing;           /* whether the frame is still executing */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
} PyFrameObject;

从f_back我们可以看出一点,在python实际的执行中,会产生很多PyFrameObject对象,而这些对象会被链接起来,形成一条执行环境链表。

image

在Python中访问PyFrameObject对象

尽管PyFrameObject对象是一个用于Python虚拟机实现的极为隐秘的内部对象,但是Python还是提供了某种途径可以访问到PyFrameObject对象。在Python中,有一种frame object,它是对C 一级的PyFrameObject的包装。而且,非常幸运的是,Pythont提供了一个方法能方便地获取当前处于活动的frame object。这个方法就是sys 模块中的_getframe方法。

import sys

value = 3


def g():
    frame = sys._getframe()
    print('current fun is: %s' % frame.f_code.co_name)
    caller = frame.f_back
    print('caller is: %s' % caller.f_code.co_name)
    print('caller is local namespace: %s' % caller.f_locals)
    print('caller is global namespace: %s' % caller.f_globals)


def f():
    a = 1
    b = 2
    global value
    g()


def show():
    f()


show()

对于上面的代码,执行结果为:
image

从执行结果可以看到,在函数f 中我们完全获得了起调用者————函数g 的一切信息,甚至包括函数g 的各个命名空间。

有读者可能对sys._getframe 是如歌实现的很感兴趣,下面我们就利用Python 的异常机制,实现一个和sys._getframe 功能相同的代码。

import sys


def get_current_frame():
    try:
        1 / 0
    except:
        # print(1111, sys.exc_info())
        _type, _value, traceback = sys.exc_info()
        return traceback.tb_frame.f_back


r = get_current_frame()
print(f'caller is: {r}')
print(f'caller is save all character: {r.f_code.co_names}')

执行结果为:
image

名字、作用域和名字空间

在上面PyFrameObject 源码中,我们看到3个独立的名字空间:builtins, globals 与 locals。名字空间对Python来说,是一个非常核心的概念,整个Python虚拟机运行的机制与“名字空间”这个概念有非常紧密的联系。在Python 中,与名字空间这个概念紧密联系的还有“名字”,“作用域”这些概念。

Python 程序的基础结构————module

每一个.py文件被视为Python的一个module。这些moudle中有一个主module, 如果你的程序是通过 python main.py 启动的,那么这个main.py就是一个主module。

一个名字(有时也称符号)就是用于代表某些事物的一个有助于记忆的字符序列。在Python中,一个标识符就是一个名字,比如变量名、函数名、类名等等。名字最终的作用不在于名字本身,而在于名字背后的那个事物。

在Python中,要使用或执行一个module, 必须首先加载一个module。加载可以用两种方式:一种是一办module的加载,通过import动作动态地加载;一种是主module的加载,通过python main.py这样的方式完成。不管一个module是如何被加载的,在加载过程中都会执行一个动作————执行module中的表达式。

[A.py]

a = 1
a += 1
def f():
    print(a)
print(a)

在module A 被加载时,Python会执行“a=1”、“a+=1”、“def f()”、“print(a)”这四条语句。(没错,函数定义也会被执行)

在赋值语句被执行之后,从概念上将,我们实际上得到了一个(name, obj)这样的关联关系,称为约束。赋值语句就是建立约束的地方。在一个约束被创建之后,它不会立即消失,会长久影响程序的运行。约束的容身之处就是名字空间。在Python 中,名字空间就是一个PyDictObject对象实现的。

LGB规则

一个module对应的源文件定义了一个作用域,称为global作用域(对应globals名字空间);一个函数定义了一个local作用域(对应locals名字空间);Python自身还定义了一个最顶层的作用域————builtin作用域(对应builtins名字空间, 在这里定义了Python的builtin函数,比如dir、open、range等)

LGB规则:名字引用动作沿着locals作用域、globals作用域、buintins作用域的顺序查找名字对应的约束。

LEGB规则

a = 1
def f()
    a = 2
    def g():
        print(a)    # [1]
    return g
func = f()
func()   #[2]

在执行上边这个modle的时候,在执行[2]处的时候, “a=2”这个约束已经不起作用了,但是Python在执行“func=f()”时,会执行函数f中的“def g()”语句,这时Python会将约束“a=2”与函数g对应的函数对象捆绑在一起,将捆绑后的结果返回,这个捆绑起来的整体被称为“闭包”。

这里有一个相当微妙的问题,最内嵌套作用域规则是“闭包”的结果呢,还是“闭包”是最内嵌套作用域规则的实现方案?这两个问题看上去是一致的,但却隐含这谁决定谁的关系。实际上,Python实现闭包是为了实现最内嵌套作用域规则。换句话说,最内嵌套作用域规则是语言设计时的策略,即是形而上的“道”;而闭包则是实现语言时的一种方案,即是形而下的“器”。

这里显示的作用域规则通常也被称为LEGB规则,其中的E为 enclosing 的缩写,代表的正是“直接外围作用域”这个概念。

global 表达式

a = 1

def f():
    print(a)

def g():
    print(a)  # [1]
    a = 2  # [2]
    print(a)

f()
g()

上边这个代码在执行时,会在[1]处报“local variable 'a' referenced before assignment”异常。没有赋值?按照LEGB规则,应该输出1 才对,并且函数f 已经老老实实输出了,在函数g 里为什么会异常?

理解这个错误的关键在于理解深刻理解最内嵌套作用域规则。这个规则的第一句话就是这个奇怪问题的症结所在,“由一个赋值语句引进的名字在这个赋值语句所在的作用域内是可见(起作用)的”。这句话的意思是,在函数g 中,虽然a=2 是在print 函数之后建立的,但是由于它们在同一个作用域,所以在[1]处是可以,a=2 这个约束的名字a 就是可见的。按照LEGB规则,在local名字空间能找到a ,所以就使用local 名字空间中的a 。但不幸的是,虽然在[1]处能找到a , 但是在print 函数之后,也就是在[2]处才能赋值动作发生,a 才能引用一个有效的对象,所以在[1]处当然应该抛出“local variable 'a' referenced before assignment”的异常。

更为有趣的是编译之后的字节码中,我们将上边的代码编译为字节码,结果如下:
image

比较一下就会发现,同样的print(a)指令,在函数f 和 函数g 中编译后的内容却是不一样。在函数f 中 LOAD_GLOBAL 表示是从global名字空间中获取,在函数g 中,LOAD_FAST 表示是从local名字空间中获取,也就是说Python 在编译的时候就已经知道名字藏身何处了。这正说明了Python 采用的是静态作用域规则,仅仅根据程序正文就能确定名字引用策略。同时,这个现象又一次说明最内嵌套作用域规则是指导Python 实现的“道”。

但有的时候,我们就是想要在函数输出外围作用域的名字a , 并且还要对a 进行赋值,但是这个赋值操作在我们设想中应该是改变外围作用域中的名字a 对应的对象。Python 为我们精心准备了 global 关键字。当一个作用域中出现global 时,意味着我们强制命令Python 对某个名字的引用只参考global 名字空间,而不再去管LEGB规则

看到下面两个例子,你对global就了如指掌了:

a = 1


def f():
    global a
    print(a)  # 输出 1
    a = 2


f()
print(a)  # 输出 2
a = 1


def f():
    a = 2

    def g():
        global a
        print(a)  # 输出1
        a += 2

    return g


t = f()
t()
print(a)  # 输出 3

posted @ 2022-11-04 14:48  一枚码农  阅读(165)  评论(0编辑  收藏  举报