python解释器网页版_用 Python 实现 Python 解释器

原标题:用 Python 实现 Python 解释器

(接上篇)

Byterun

现在我们有足够的 Python 解释器的知识背景去考察 Byterun。

Byterun 中有四种对象。

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

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

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

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

VirtualMachine 类

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

classVirtualMachineError(Exception):

pass

classVirtualMachine(object):

def__init__(self):

self.frames=[]#Thecall stack of frames.

self.frame=None#Thecurrent frame.

self.return_value=None

self.last_exception=None

defrun_code(self,code,global_names=None,local_names=None):

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

frame=self.make_frame(code,global_names=global_names,

local_names=local_names)

self.run_frame(frame)

Frame 类

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

classFrame(object):

def__init__(self,code_obj,global_names,local_names,prev_frame):

self.code_obj=code_obj

self.global_names=global_names

self.local_names=local_names

self.prev_frame=prev_frame

self.stack=[]

ifprev_frame:

self.builtin_names=prev_frame.builtin_names

else:

self.builtin_names=local_names['__builtins__']

ifhasattr(self.builtin_names,'__dict__'):

self.builtin_names=self.builtin_names.__dict__

self.last_instruction=0

self.block_stack=[]

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

classVirtualMachine(object):

[...删节...]

#Framemanipulation

defmake_frame(self,code,callargs={},global_names=None,local_names=None):

ifglobal_namesisnotNoneandlocal_namesisnotNone:

local_names=global_names

elifself.frames:

global_names=self.frame.global_names

local_names={}

else:

global_names=local_names={

'__builtins__':__builtins__,

'__name__':'__main__',

'__doc__':None,

'__package__':None,

}

local_names.update(callargs)

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

returnframe

defpush_frame(self,frame):

self.frames.append(frame)

self.frame=frame

defpop_frame(self):

self.frames.pop()

ifself.frames:

self.frame=self.frames[-1]

else:

self.frame=None

defrun_frame(self):

pass

#we'll come back to this shortly

Function 类

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

classFunction(object):

"""

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

"""

__slots__=[

'func_code','func_name','func_defaults','func_globals',

'func_locals','func_dict','func_closure',

'__name__','__dict__','__doc__',

'_vm','_func',

]

def__init__(self,name,code,globs,defaults,closure,vm):

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

self._vm=vm

self.func_code=code

self.func_name=self.__name__=nameorcode.co_name

self.func_defaults=tuple(defaults)

self.func_globals=globs

self.func_locals=self._vm.frame.f_locals

self.__dict__={}

self.func_closure=closure

self.__doc__=code.co_consts[0]ifcode.co_constselseNone

#Sometimes,we need a realPythonfunction.Thisisforthat.

kw={

'argdefs':self.func_defaults,

}

ifclosure:

kw['closure']=tuple(make_cell(0)for_inclosure)

self._func=types.FunctionType(code,globs,**kw)

def__call__(self,*args,**kwargs):

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

callargs=inspect.getcallargs(self._func,*args,**kwargs)

#Usecallargs to provide a mapping of arguments:values topassinto thenew

#frame.

frame=self._vm.make_frame(

self.func_code,callargs,self.func_globals,{}

)

returnself._vm.run_frame(frame)

defmake_cell(value):

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

#ThankstoAlexGaynorforhelpwiththisbit of twistiness.

fn=(lambdax:lambda:x)(value)

returnfn.__closure__[0]

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

classVirtualMachine(object):

[...删节...]

#Datastack manipulation

deftop(self):

returnself.frame.stack[-1]

defpop(self):

returnself.frame.stack.pop()

defpush(self,*vals):

self.frame.stack.extend(vals)

defpopn(self,n):

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

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

"""

ifn:

ret=self.frame.stack[-n:]

self.frame.stack[-n:]=[]

returnret

else:

return[]

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

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

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

classVirtualMachine(object):

[...删节...]

defparse_byte_and_args(self):

f=self.frame

opoffset=f.last_instruction

byteCode=f.code_obj.co_code[opoffset]

f.last_instruction+=1

byte_name=dis.opname[byteCode]

ifbyteCode>=dis.HAVE_ARGUMENT:

#index into the bytecode

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

f.last_instruction+=2#advance the instruction pointer

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

ifbyteCodeindis.hasconst:#Lookup a constant

arg=f.code_obj.co_consts[arg_val]

elifbyteCodeindis.hasname:#Lookup a name

arg=f.code_obj.co_names[arg_val]

elifbyteCodeindis.haslocal:#Lookup alocalname

arg=f.code_obj.co_varnames[arg_val]

elifbyteCodeindis.hasjrel:#Calculatea relative jump

arg=f.last_instruction+arg_val

else:

arg=arg_val

argument=[arg]

else:

argument=[]

returnbyte_name,argument

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

classVirtualMachine(object):

[...删节...]

defdispatch(self,byte_name,argument):

""" Dispatch by bytename to the corresponding methods.

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

#Whenlater unwinding the block stack,

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

why=None

try:

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

ifbytecode_fnisNone:

ifbyte_name.startswith('UNARY_'):

self.unaryOperator(byte_name[6:])

elifbyte_name.startswith('BINARY_'):

self.binaryOperator(byte_name[7:])

else:

raiseVirtualMachineError(

"unsupported bytecode type: %s"%byte_name

)

else:

why=bytecode_fn(*argument)

except:

#dealwithexceptions encounteredwhileexecuting the op.

self.last_exception=sys.exc_info()[:2]+(None,)

why='exception'

returnwhy

defrun_frame(self,frame):

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

Exceptions are raised, the return value is returned.

"""

self.push_frame(frame)

whileTrue:

byte_name,arguments=self.parse_byte_and_args()

why=self.dispatch(byte_name,arguments)

#Dealwithany block management we need todo

whilewhyandframe.block_stack:

why=self.manage_block_stack(why)

ifwhy:

break

self.pop_frame()

ifwhy=='exception':

exc,val,tb=self.last_exception

e=exc(val)

e.__traceback__=tb

raisee

returnself.return_value

Block 类

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

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

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

Block=collections.namedtuple("Block","type, handler, stack_height")

classVirtualMachine(object):

[...删节...]

#Blockstack manipulation

defpush_block(self,b_type,handler=None):

level=len(self.frame.stack)

self.frame.block_stack.append(Block(b_type,handler,stack_height))

defpop_block(self):

returnself.frame.block_stack.pop()

defunwind_block(self,block):

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

ifblock.type=='except-handler':

#Theexceptionitselfison the stackastype,value,andtraceback.

offset=3

else:

offset=0

whilelen(self.frame.stack)>block.level+offset:

self.pop()

ifblock.type=='except-handler':

traceback,value,exctype=self.popn(3)

self.last_exception=exctype,value,traceback

defmanage_block_stack(self,why):

""" """

frame=self.frame

block=frame.block_stack[-1]

ifblock.type=='loop'andwhy=='continue':

self.jump(self.return_value)

why=None

returnwhy

self.pop_block()

self.unwind_block(block)

ifblock.type=='loop'andwhy=='break':

why=None

self.jump(block.handler)

returnwhy

if(block.typein['setup-except','finally']andwhy=='exception'):

self.push_block('except-handler')

exctype,value,tb=self.last_exception

self.push(tb,value,exctype)

self.push(tb,value,exctype)#yes,twice

why=None

self.jump(block.handler)

returnwhy

elifblock.type=='finally':

ifwhyin('return','continue'):

self.push(self.return_value)

self.push(why)

why=None

self.jump(block.handler)

returnwhy

returnwhy

指令

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

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

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

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

>>>defmod(a,b):

...returna%b

>>>dis.dis(mod)

20LOAD_FAST0(a)

3LOAD_FAST1(b)

6BINARY_MODULO

7RETURN_VALUE

>>>mod(19,5)

4

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

>>>mod("by%sde","teco")

'bytecode'

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

>>> print("by%sde" % "teco") bytecode

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

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

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

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

defmod(a,b):

a%b

returna%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 社区的支持和鼓励。所有的不足全是我自己没搞好。

via:

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

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

[1]: http://akaptur.com

[2]: https://github.com/nedbat/byterun

[3]: https://github.com/qingyunha

[4]: https://github.com/wxy

[5]: https://github.com/LCTT/TranslateProject

[6]: http://linux.cn/

推荐文章

点击标题或输入文章 ID 直达该文章

4283

4301

4392

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

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值