(续)

Byterun

现在我们有足够的Python解释器的知识背景来调查Byterun。

Byterun 中有四种对象。

  • VirtualMachine类,它管理高层结构,尤其是帧调用栈,并包含了指令到操作的映射。这是一个比前面Inteprter对象更复杂的版本。

  • Frame类,每个Frame类都有一个代码对象,并且管理着其他一些必要的状态位,尤其是全局和局部命名空间、指向调用它的整的指针和最后执行的字节码指令。

  • Function类,它被用来代替真正的 Python 函数。回想一下,调用函数时会创建一个新的帧。我们自己实现了Function,以便我们控制新的Frame的创建。

  • Block类,它只是包装了块的 3 个属性。(块的细节不是解释器的核心,我们不会花时间在它身上,把它列在这里,是因为 Byterun 需要它。)

VirtualMachine

每次程序运行时只会创建一个VirtualMachine实例,因为我们只有一个 Python 解释器。VirtualMachine保存调用栈、异常状态、在帧之间传递的返回值。它的入口点是run_code方法,它以编译后的代码对象为参数,以创建一个帧为开始,然后运行这个帧。这个帧可能再创建出新的帧;调用栈随着程序的运行而增长和缩短。当第一个帧返回时,执行结束。

  1. class VirtualMachineError(Exception):

  2. pass

  3. class VirtualMachine(object):

  4. def __init__(self):

  5. = # The call stack of frames.

  6. = None # The current frame.

  7. = None

  8. = None

  9. def run_code(self, code, global_names=None, local_names=None):

  10. """ An entry point to execute code using the virtual machine."""

  11. frame = (code, global_names=global_names,

  12. local_names=local_names)

  13. (frame)

Frame

接下来,我们来写Frame对象。帧是一个属性的集合,它没有任何方法。前面提到过,这些属性包括由编译器生成的代码对象;局部、全局和内置命名空间;前一个帧的引用;一个数据栈;一个块栈;最后执行的指令指针。(对于内置命名空间我们需要多做一点工作,Python 在不同模块中对这个命名空间有不同的处理;但这个细节对我们的虚拟机不重要。)

  1. class Frame(object):

  2. def __init__(self, code_obj, global_names, local_names, prev_frame):

  3. = code_obj

  4. = global_names

  5. = local_names

  6. = prev_frame

  7. =

  8. if prev_frame:

  9. =

  10. else:

  11. = local_names['__builtins__']

  12. if hasattr(, '__dict__'):

  13. = .__dict__

  14. = 0

  15. =

接着,我们在虚拟机中增加对帧的操作。这有 3 个帮助函数:一个创建新的帧的方法(它负责为新的帧找到名字空间),和压栈和出栈的方法。第四个函数,run_frame,完成执行帧的主要工作,待会我们再讨论这个方法。

  1. class VirtualMachine(object):

  2. [... 删节 ...]

  3. # Frame manipulation

  4. def make_frame(self, code, callargs={}, global_names=None, local_names=None):

  5. if global_names is not None and local_names is not None:

  6. local_names = global_names

  7. elif :

  8. global_names = .global_names

  9. local_names = {}

  10. else:

  11. global_names = local_names = {

  12. '__builtins__': __builtins__,

  13. '__name__': '__main__',

  14. '__doc__': None,

  15. '__package__': None,

  16. }

  17. local_names.update(callargs)

  18. frame = Frame(code, global_names, local_names, )

  19. return frame

  20. def push_frame(self, frame):

  21. .append(frame)

  22. = frame

  23. def pop_frame(self):

  24. .pop

  25. if :

  26. = [-1]

  27. else:

  28. = None

  29. def run_frame(self):

  30. pass

  31. # we'll come back to this shortly

Function

Function的实现有点曲折,但是大部分的细节对理解解释器不重要。重要的是当调用函数时 —— 即调用__call__方法 —— 它创建一个新的Frame并运行它。

  1. class Function(object):

  2. """

  3. Create a realistic function object, defining the things the interpreter expects.

  4. """

  5. __slots__ = [

  6. 'func_code', 'func_name', 'func_defaults', 'func_globals',

  7. 'func_locals', 'func_dict', 'func_closure',

  8. '__name__', '__dict__', '__doc__',

  9. '_vm', '_func',

  10. ]

  11. def __init__(self, name, code, globs, defaults, closure, vm):

  12. """You don't need to follow this closely to understand the interpreter."""

  13. = vm

  14. = code

  15. = = name or code.co_name

  16. = tuple(defaults)

  17. = globs

  18. = .frame.f_locals

  19. = {}

  20. = closure

  21. = code.co_consts[0] if code.co_consts else None

  22. # Sometimes, we need a real Python function. This is for that.

  23. kw = {

  24. 'argdefs': ,

  25. }

  26. if closure:

  27. kw['closure'] = tuple(make_cell(0) for _ in closure)

  28. = (code, globs, **kw)

  29. def __call__(self, *args, **kwargs):

  30. """When calling a Function, make a new frame and run it."""

  31. callargs = in(, *args, **kwargs)

  32. # Use callargs to provide a mapping of arguments: values to pass into the new

  33. # frame.

  34. frame = .make_frame(

  35. , callargs, , {}

  36. )

  37. return .run_frame(frame)

  38. def make_cell(value):

  39. """Create a real Python closure and grab a cell."""

  40. # Thanks to Alex Gaynor for help with this bit of twistiness.

  41. fn = (lambda x: lambda: x)(value)

  42. return [0]

接着,回到VirtualMachine对象,我们对数据栈的操作也增加一些帮助方法。字节码操作的栈总是在当前帧的数据栈。这些帮助函数让我们的POP_TOPLOAD_FAST以及其他操作栈的指令的实现可读性更高。

  1. class VirtualMachine(object):

  2. [... 删节 ...]

  3. # Data stack manipulation

  4. def top(self):

  5. return .stack[-1]

  6. def pop(self):

  7. return .stack.pop

  8. def push(self, *vals):

  9. .stack.extend(vals)

  10. def popn(self, n):

  11. """Pop a number of values from the value stack.

  12. A list of `n` values is returned, the deepest value first.

  13. """

  14. if n:

  15. ret = .stack[-n:]

  16. .stack[-n:] =

  17. return ret

  18. else:

  19. return

在我们运行帧之前,我们还需两个方法。

第一个方法,parse_byte_and_args以一个字节码为输入,先检查它是否有参数,如果有,就解析它的参数。这个方法同时也更新帧的last_instruction属性,它指向最后执行的指令。一条没有参数的指令只有一个字节长度,而有参数的字节有3个字节长。参数的意义依赖于指令是什么。比如,前面说过,指令POP_JUMP_IF_FALSE,它的参数指的是跳转目标。BUILD_LIST,它的参数是列表的个数。LOAD_CONST,它的参数是常量的索引。

一些指令用简单的数字作为参数。对于另一些,虚拟机需要一点努力去发现它含意。标准库中的dis模块中有一个备忘单,它解释什么参数有什么意思,这让我们的代码更加简洁。比如,列表dis.hasname告诉我们LOAD_NAMEIMPORT_NAMELOAD_GLOBAL,以及另外的 9 个指令的参数都有同样的意义:对于这些指令,它们的参数代表了代码对象中的名字列表的索引。

  1. class VirtualMachine(object):

  2. [... 删节 ...]

  3. def parse_byte_and_args(self):

  4. f =

  5. opoffset = f.last_instruction

  6. byteCode = f.code_obj.co_code[opoffset]

  7. f.last_instruction += 1

  8. byte_name = dis.opname[byteCode]

  9. if byteCode >= dis.HAVE_ARGUMENT:

  10. # index into the bytecode

  11. arg = f.code_obj.co_code[f.last_instruction:f.last_instruction+2]

  12. f.last_instruction += 2 # advance the instruction pointer

  13. arg_val = arg[0] + (arg[1] * 256)

  14. if byteCode in dis.hasconst: # Look up a constant

  15. arg = f.code_obj.co_consts[arg_val]

  16. elif byteCode in dis.hasname: # Look up a name

  17. arg = f.code_obj.co_names[arg_val]

  18. elif byteCode in dis.haslocal: # Look up a local name

  19. arg = f.code_obj.co_varnames[arg_val]

  20. elif byteCode in dis.hasjrel: # Calculate a relative jump

  21. arg = f.last_instruction + arg_val

  22. else:

  23. arg = arg_val

  24. argument = [arg]

  25. else:

  26. argument =

  27. return byte_name, argument

下一个方法是dispatch,它查找给定的指令并执行相应的操作。在 CPython 中,这个分派函数用一个巨大的 switch 语句实现,有超过 1500 行的代码。幸运的是,我们用的是 Python,我们的代码会简洁的多。我们会为每一个字节码名字定义一个方法,然后用getattr来查找。就像我们前面的小解释器一样,如果一条指令叫做FOO_BAR,那么它对应的方法就是byte_FOO_BAR。现在,我们先把这些方法当做一个黑盒子。每个指令方法都会返回None或者一个字符串why,有些情况下虚拟机需要这个额外why信息。这些指令方法的返回值,仅作为解释器状态的内部指示,千万不要和执行帧的返回值相混淆。

  1. class VirtualMachine(object):

  2. [... 删节 ...]

  3. def dispatch(self, byte_name, argument):

  4. """ Dispatch by bytename to the corresponding methods.

  5. Exceptions are caught and set on the virtual machine."""

  6. # When later unwinding the block stack,

  7. # we need to keep track of why we are doing it.

  8. why = None

  9. try:

  10. bytecode_fn = getattr(self, 'byte_%s' % byte_name, None)

  11. if bytecode_fn is None:

  12. if by('UNARY_'):

  13. (byte_name[6:])

  14. elif by('BINARY_'):

  15. (byte_name[7:])

  16. else:

  17. raise VirtualMachineError(

  18. "unsupported bytecode type: %s" % byte_name

  19. )

  20. else:

  21. why = bytecode_fn(*argument)

  22. except:

  23. # deal with exceptions encountered while executing the op.

  24. = [:2] + (None,)

  25. why = 'exception'

  26. return why

  27. def run_frame(self, frame):

  28. """Run a frame until it returns (somehow).

  29. Exceptions are raised, the return value is returned.

  30. """

  31. (frame)

  32. while True:

  33. byte_name, arguments =

  34. why = (byte_name, arguments)

  35. # Deal with any block management we need to do

  36. while why and :

  37. why = (why)

  38. if why:

  39. break

  40. if why == 'exception':

  41. exc, val, tb =

  42. e = exc(val)

  43. e.__traceback__ = tb

  44. raise e

  45. return

Block

在我们完成每个字节码方法前,我们简单的讨论一下块。一个块被用于某种控制流,特别是异常处理和循环。它负责保证当操作完成后数据栈处于正确的状态。比如,在一个循环中,一个特殊的迭代器会存在栈中,当循环完成时它从栈中弹出。解释器需要检查循环仍在继续还是已经停止。

为了跟踪这些额外的信息,解释器设置了一个标志来指示它的状态。我们用一个变量why实现这个标志,它可以是None或者是下面几个字符串之一:"continue""break""excption"return。它们指示对块栈和数据栈进行什么操作。回到我们迭代器的例子,如果块栈的栈顶是一个loop块,why的代码是continue,迭代器就应该保存在数据栈上,而如果whybreak,迭代器就会被弹出。

块操作的细节比这个还要繁琐,我们不会花时间在这上面,但是有兴趣的读者值得仔细的看看。

  1. Block = collec("Block", "type, handler, stack_height")

  2. class VirtualMachine(object):

  3. [... 删节 ...]

  4. # Block stack manipulation

  5. def push_block(self, b_type, handler=None):

  6. level = len(.stack)

  7. .block_stack.append(Block(b_type, handler, stack_height))

  8. def pop_block(self):

  9. return .block_stack.pop

  10. def unwind_block(self, block):

  11. """Unwind the values on the data stack corresponding to a given block."""

  12. if block.type == 'except-handler':

  13. # The exception itself is on the stack as type, value, and traceback.

  14. offset = 3

  15. else:

  16. offset = 0

  17. while len(.stack) > block.level + offset:

  18. if block.type == 'except-handler':

  19. traceback, value, exctype = n(3)

  20. = exctype, value, traceback

  21. def manage_block_stack(self, why):

  22. """ """

  23. frame =

  24. block = [-1]

  25. if block.type == 'loop' and why == 'continue':

  26. ()

  27. why = None

  28. return why

  29. _block

  30. (block)

  31. if block.type == 'loop' and why == 'break':

  32. why = None

  33. )

  34. return why

  35. if in ['setup-except', 'finally'] and why == 'exception'):

  36. ('except-handler')

  37. exctype, value, tb =

  38. (tb, value, exctype)

  39. (tb, value, exctype) # yes, twice

  40. why = None

  41. )

  42. return why

  43. elif block.type == 'finally':

  44. if why in ('return', 'continue'):

  45. ()

  46. (why)

  47. why = None

  48. )

  49. return why

  50. return why

指令

剩下了的就是完成那些指令方法了:byte_LOAD_FASTbyte_BINARY_MODULO等等。而这些指令的实现并不是很有趣,完整的实现在 GitHub 上[2]。(这里包括的指令足够执行我们前面所述的所有代码了。)

动态类型:编译器不知道它是什么

你可能听过 Python 是一种动态语言 —— 它是动态类型的。在我们建造解释器的过程中,已经透露出这样的信息。

动态的一个意思是很多工作是在运行时完成的。前面我们看到 Python 的编译器没有很多关于代码真正做什么的信息。举个例子,考虑下面这个简单的函数mod。它取两个参数,返回它们的模运算值。从它的字节码中,我们看到变量ab首先被加载,然后字节码BINAY_MODULO完成这个模运算。

计算 19 % 5 得4,—— 一点也不奇怪。如果我们用不同类的参数呢?

刚才发生了什么?你可能在其它地方见过这样的语法,格式化字符串。

用符号%去格式化字符串会调用字节码BUNARY_MODULO。它取栈顶的两个值求模,不管这两个值是字符串、数字或是你自己定义的类的实例。字节码在函数编译时生成(或者说,函数定义时)相同的字节码会用于不同类的参数。

Python 的编译器关于字节码的功能知道的很少,而取决于解释器来决定BINAYR_MODULO应用于什么类型的对象并完成正确的操作。这就是为什么 Python 被描述为动态类型dynamically typed:直到运行前你不必知道这个函数参数的类型。相反,在一个静态类型语言中,程序员需要告诉编译器参数的类型是什么(或者编译器自己推断出参数的类型。)

编译器的无知是优化 Python 的一个挑战 —— 只看字节码,而不真正运行它,你就不知道每条字节码在干什么!你可以定义一个类,实现__mod__方法,当你对这个类的实例使用%时,Python 就会自动调用这个方法。所以,BINARY_MODULO其实可以运行任何代码。

看看下面的代码,第一个a % b看起来没有用。

  1. def mod(a,b):

  2. a % b

  3. return a %b

不幸的是,对这段代码进行静态分析 —— 不运行它 —— 不能确定第一个a % b没有做任何事。用%调用__mod__可能会写一个文件,或是和程序的其他部分交互,或者其他任何可以在 Python 中完成的事。很难优化一个你不知道它会做什么的函数。在 Russell Power 和 Alex Rubinsteyn 的优秀论文中写道,“我们可以用多快的速度解释 Python?”,他们说,“在普遍缺乏类型信息下,每条指令必须被看作一个INVOKE_ARBITRARY_METHOD。”

总结

Byterun 是一个比 CPython 容易理解的简洁的 Python 解释器。Byterun 复制了 CPython 的主要结构:一个基于栈的解释器对称之为字节码的指令集进行操作,它们顺序执行或在指令间跳转,向栈中压入和从中弹出数据。解释器随着函数和生成器的调用和返回,动态的创建、销毁帧,并在帧之间跳转。Byterun 也有着和真正解释器一样的限制:因为 Python 使用动态类型,解释器必须在运行时决定指令的正确行为。

我鼓励你去反汇编你的程序,然后用 Byterun 来运行。你很快会发现这个缩短版的 Byterun 所没有实现的指令。完整的实现在 ,或者你可以仔细阅读真正的 CPython 解释器ceval.c,你也可以实现自己的解释器!

致谢

感谢 Ned Batchelder 发起这个项目并引导我的贡献,感谢 Michael Arntzenius 帮助调试代码和这篇文章的修订,感谢 Leta Montopoli 的修订,以及感谢整个 Recurse Center 社区的支持和鼓励。所有的不足全是我自己没搞好。

作者: Allison Kaptur 译者:qingyunha[3] 校对:wxy[4]

本文由 LCTT[5] 原创翻译,Linux中国[6] 荣誉推出

[1]:

[2]:

[3]:

[4]:

[5]:

[6]:

推荐文章

将文章分享给朋友是对我们最好的赞赏!

1.《callstack看这里!用 Python 实现 Python 解释器》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《callstack看这里!用 Python 实现 Python 解释器》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.cxvn.com/gl/djyxgl/170232.html