Библиотека сайта rus-linux.net
Интерпретатор языка Python, написанный на языке Python
Оригинал: A Python Interpreter Written in Python
Автор: Allison Kaptur
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: февраль 2017 г.
Это пятая часть статьи "Интерпретатор языка Python, написанный на языке Python".
Перейти к
началу.
Интерпретатор Byterun
Теперь у нас достаточно знаний, касающихся интерпретатора языка Python, с тем, чтобы можно было приступить к изучению интерпретатора Byterun.
В интерпретаторе Byterun есть четыре вида объектов:
- Класс
VirtualMachine
(виртуальная машина), который управляет структурой самого высокого уровня, в частности стеком вызовов фреймов, а также хранит отображение инструкций в операции. Это более сложный вариант объектаIntepreter
, который был описан выше. - Класс
Frame
(фрейм). Каждый экземпляр класса Frame имеет один объект кода и управляет несколькими другими необходимыми битами состояния, в частности, глобальными и локальными пространствами имен, ссылками на вызывающий фрейм и на последнюю инструкцию байт-кода, которая была выполнена. - Класс /code>Function (Функция), который будет использоваться вместо реальных функций языка Python. Напомним, что вызов функции создает новый фрейм в интерпретаторе. Мы реализуем класс
Function
так, что можем управлять созданием новых кадров. - Класс
Block
(блок), который просто оборачивает три атрибута блоков. Конкретные деталии реализации блоков не столь важны при реализации интерпретатора языка Python, так что мы на них не будем тратить много времени, но благодаря им интерпретатор Byterun может запускать реальный код на языке Python.
Класс VirtualMachine
При запуске программы каждый раз создается только один экземпляр класса VirtualMachine
, т. е. у нас только один интерпретатор языка Python. В классе VirtualMachine
хранится стек вызовов, исключительное состояние и возвращаемые значения в случаях, если они передаются между фреймами. Точка входа для выполнения кода является методом run_code
, который получает в качестве аргумента объект со скомпилированным кодом. Работа начинается с настройки и запуска фрейма. Этот фрейм может создавать другие фреймы; по мере выполнения программы стек вызовов будет увеличиваться и уменьшаться. Когда, в конце концов, управление будет возвращено в первый фрейм, выполнение кода завершается.
class VirtualMachineError(Exception): pass class VirtualMachine(object): def __init__(self): self.frames = [] # Стек вызовов фреймов. self.frame = None # Текущий фрейм. self.return_value = None self.last_exception = None def run_code(self, code, global_names=None, local_names=None): """ Точка входа выполнения кода с помощью виртуальной машины.""" frame = self.make_frame(code, global_names=global_names, local_names=local_names) self.run_frame(frame)
Класс Frame
Далее мы напишем класс Frame
. Фрейм представляет собой коллекцию атрибутов, не имеющую методов. Как уже было упомянуто выше, среди атрибутов есть объект с кодом, созданным компилятором, локальное, глобальное и встроенное в язык пространство имен, ссылка на предыдущий фрейм, стек данных, стек блоков и последняя выполненная команда. Поскольку в языке Python в разных модулях по-разному используется пространство имен, нам для того, чтобы до него добраться, потребуется выполнить некоторые дополнительные действия, но это в частности никак не влияют на работу виртуальной машины.
class Frame(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 = [] if prev_frame: self.builtin_names = prev_frame.builtin_names else: self.builtin_names = local_names['__builtins__'] if hasattr(self.builtin_names, '__dict__'): self.builtin_names = self.builtin_names.__dict__ self.last_instruction = 0 self.block_stack = []
Далее, мы добавим к виртуальной машине возможность обрабатывать фреймы. Для фреймов используются три вспомогательные функции: одна - для создания новых фреймов (которая отвечает за выбор пространства имен для нового фрейма) и по одной функции для того, чтобы поместить фрейм в стек и чтобы вытолкнуть его из стека. Четвертая функция, run_frame
, выполняет основную работу в тех случаях, когда фрейм передается на исполнение. В ближайшее время мы к этому вернемся.
class VirtualMachine(object): [... snip ...] # Работа с фреймом def make_frame(self, code, callargs={}, global_names=None, local_names=None): if global_names is not None and local_names is not None: local_names = global_names elif self.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) return frame def push_frame(self, frame): self.frames.append(frame) self.frame = frame def pop_frame(self): self.frames.pop() if self.frames: self.frame = self.frames[-1] else: self.frame = None def run_frame(self): pass # Мы вскоре сюда возвратимся
Класс Function
Реализация класса Function
слегка запутана, но большинство деталей не имеют решающего значения для понимания работы интерпретатора. Важно отметить, что при вызове функции вызывается метод __call__
, который создает новый объект Frame
и запускает его на исполнение.
class Function(object): """ Создается реальный объект function, в котором есть все, что нужно интерпретатору. """ __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): """Для того, чтобы понять, как работает интерпретатор, вам не нужно глубоко вдаваться в подробности.""" self._vm = vm self.func_code = code self.func_name = self.__name__ = name or code.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] if code.co_consts else None # Иногда нам нужно использовать настоящую функцию языка Python. # Этот фрагмент кода для таких случаев. kw = { 'argdefs': self.func_defaults, } if closure: kw['closure'] = tuple(make_cell(0) for _ in closure) self._func = types.FunctionType(code, globs, **kw) def __call__(self, *args, **kwargs): """Когда функция вызывается,то создается новый фрейм, который передается на исполнение.""" callargs = inspect.getcallargs(self._func, *args, **kwargs) # Используйте callargs для отображения аргументов # значения будут переданы в новый фрейм. frame = self._vm.make_frame( self.func_code, callargs, self.func_globals, {} ) return self._vm.run_frame(frame) def make_cell(value): """Создает реальное замыкание на языке Python и использует его.""" # Спасибо Алексу Гейнору (Alex Gaynor) за помощь в # написании этого куска кода. fn = (lambda x: lambda: x)(value) return fn.__closure__[0]
Затем, вернувшись обратно в объект VirtualMachine
, мы добавим несколько вспомогательных методов для работы со стеком данных. Байт-код, который обрабатывает этот стек, всегда работает со стеком данных текущего фрейма. Это сделает нашу реализацию инструкций POP_TOP
, LOAD_FAST
и всех других инструкций, которые работают со стеком, более удобной для чтения.
class VirtualMachine(object): [... snip ...] # Работа со стеком данных def top(self): return self.frame.stack[-1] def pop(self): return self.frame.stack.pop() def push(self, *vals): self.frame.stack.extend(vals) def popn(self, n): """Выталкивает из стека указанное количество элементов. Будет возвращен список из `n` значений, первым значением будет то, которое в стеке находилось глубже других. """ if n: ret = self.frame.stack[-n:] self.frame.stack[-n:] = [] return ret else: return []
Прежде, чем мы перейдем к исполнению фрейма, нам потребуются еще два метода.
Первый метод, parse_byte_and_args
, берет байт-код, проверяет, есть ли у него аргументы, и анализирует их, если таковые имеются. Этот метод также обновляет атрибут фрейма last_instruction
, который является ссылкой на последнюю выполненную инструкцию. Длина одной инструкции равна одному байту в случае, если у команды нет аргумента, и трем байтом в случае, если у нее есть аргумент; последние два байта являются аргументом. Смысл аргумента каждой инструкции зависит от того, какая это инструкция. Например, как уже упоминалось выше, для инструкции POP_JUMP_IF_FALSE
, аргументом инструкции является целью, куда произойдет переход. Для инструкции BUILD_LIST
, это количество элементов в списке. Для инструкции LOAD_CONST
, это указатель в списке констант.
Некоторые инструкции используют в качестве своих аргументов обычные числа. Для других, виртуальная машина должна выполнить некоторые действия для того, чтобы выяснить, что означают аргументы. Модуль dis, который есть в стандартной библиотеке, выдает описание, объясняющее, какие используются аргументы и что они означают; благодаря этому наш код будет выглядеть более компактным и более понятным. Например, список dis.hasname говорит нам о том, что аргументы в инструкциях LOAD_NAME
, IMPORT_NAME
, LOAD_GLOBAL
и еще в девяти других инструкциях означают одно и тоже: для этих инструкций, аргумент представляет собой указатель на список имен в объекте кода.
class VirtualMachine(object): [... snip ...] def parse_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] if byteCode >= dis.HAVE_ARGUMENT: # указатель на байт-код arg = f.code_obj.co_code[f.last_instruction:f.last_instruction+2] f.last_instruction += 2 # указатель на инструкцию arg_val = arg[0] + (arg[1] * 256) if byteCode in dis.hasconst: # Поиск константы arg = f.code_obj.co_consts[arg_val] elif byteCode in dis.hasname: # Поиск имени arg = f.code_obj.co_names[arg_val] elif byteCode in dis.haslocal: # Поиск локального имени arg = f.code_obj.co_varnames[arg_val] elif byteCode in dis.hasjrel: # Вычисление относительного перехода arg = f.last_instruction + arg_val else: arg = arg_val argument = [arg] else: argument = [] return byte_name, argument
Следующим методом является метод диспетчеризации dispatch
, который ищет операции для заданной инструкции и выполняет их. В интерпретаторе CPython, эта диспетчеризация осуществляется с помощью гигантского переключателя, размер которого 1500 строк! К счастью, поскольку мы пишем интерпретатор для языка Python, наш результат может быть более компактным. Мы для каждого однобайтового имени определим метод, а затем для поиска воспользуемся методом getattr
. Точно также, как и в нашем игрушечном интерпретаторе, который мы рассматривали выше, если наша инструкция называется FOO_BAR
, то соответствующий метод будет называться byte_FOO_BAR
. На данный момент, мы будем считать содержимое этих методов черным ящиком. Каждый метод будет возвращать либо None, либо строку с названием why
, которая представляет собой дополнительную информацию о состоянии интерпретатора, которая необходима в некоторых случаях. Такие возвращаемые значения методов для отдельных инструкций используются только в качестве дополнительных индикаторов состояния интерпретатора - не путайте их со значениями, возвращаемыми из исполняемых фреймов.
class VirtualMachine(object): [... snip ...] def dispatch(self, byte_name, argument): """ Привязка бата-имени к соответствующим методам. Перехват и инициация исключений происходит в виртуальной машине.""" # Когда позже стек блоков будет разматываться, # нам нужно будет помнить, почему мы это делаем. why = None try: bytecode_fn = getattr(self, 'byte_%s' % byte_name, None) if bytecode_fn is None: if byte_name.startswith('UNARY_'): self.unaryOperator(byte_name[6:]) elif byte_name.startswith('BINARY_'): self.binaryOperator(byte_name[7:]) else: raise VirtualMachineError( "unsupported bytecode type: %s" % byte_name ) else: why = bytecode_fn(*argument) except: # Обработка исключения, которое возникло при выполнении операции. self.last_exception = sys.exc_info()[:2] + (None,) why = 'exception' return why def run_frame(self, frame): """Выполнение фрейма до тех пор, пока не будет возвращено управление. Возникает исключение, возвращаемое значение получено. """ self.push_frame(frame) while True: byte_name, arguments = self.parse_byte_and_args() why = self.dispatch(byte_name, arguments) # Deal with any block management we need to do while why and frame.block_stack: why = self.manage_block_stack(why) if why: break self.pop_frame() if why == 'exception': exc, val, tb = self.last_exception e = exc(val) e.__traceback__ = tb raise e return self.return_value
Класс Block
Прежде, чем реализовывать методы для каждой инструкции, мы кратко рассмотрим блоки. Блок используется в определенных случаях для управления последовательностью действий, в частности при обработке исключений и зацикливании. На блок возлагается ответственность проверять, что после того, как выполнение операции будет закончено, стек данных будет находиться в соответствующем состоянии. Например, в случае цикла все то время, пока работает цикл, в стеке остается специальный объект итератора, но он выталкивается из стека в случае, когда цикл будет завершен. Интерпретатор должен следить за тем, продолжается ли цикл или он уже завершен.
Чтобы следить за этой дополнительной информацией, интерпретатор устанавливает флаг, который указывает его состояние. Мы реализуем этот флаг в виде переменной, называемой why
, значение которой может быть None или одним из строковых значений "continue
" ("продолжено"), "break
" ("прервано"), "exception
" ("исключение") или "return
" ("возврат"). Оно указывает на то, какие действия со стеком блоков и стеком данных нужно выполнить. Если вернуться к примеру с итератором, то если на вершина стека блоков находится блок-цикл loop, а код why
содержит значение continue
, то объект итератора должен оставаться в стеке данных, но если код why
будет содержать значение break
, то он должен быть вытолкнут.
Конкретные особенности работы с блоками довольно запутаны, и мы на них не будем больше тратить времени, а заинтересованным читателям рекомендуем внимательно посмотреть код.
Block = collections.namedtuple("Block", "type, handler, stack_height") class VirtualMachine(object): [... snip ...] # Работа со стеком блоков def push_block(self, b_type, handler=None): stack_height = len(self.frame.stack) self.frame.block_stack.append(Block(b_type, handler, stack_height)) def pop_block(self): return self.frame.block_stack.pop() def unwind_block(self, block): """Размотка значений из стека данных, соответствующих данному блоку.""" if block.type == 'except-handler': # The exception itself is on the stack as type, value, and traceback. offset = 3 else: offset = 0 while len(self.frame.stack) > block.level + offset: self.pop() if block.type == 'except-handler': traceback, value, exctype = self.popn(3) self.last_exception = exctype, value, traceback def manage_block_stack(self, why): """ """ frame = self.frame block = frame.block_stack[-1] if block.type == 'loop' and why == 'continue': self.jump(self.return_value) why = None return why self.pop_block() self.unwind_block(block) if block.type == 'loop' and why == 'break': why = None self.jump(block.handler) return why if (block.type in ['setup-except', 'finally'] and why == '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) return why elif block.type == 'finally': if why in ('return', 'continue'): self.push(self.return_value) self.push(why) why = None self.jump(block.handler) return why return why
Перейти к следующей части статьи.
Перейти к началу статьи.