Особенности интерпретатора Python
23-03-2023Время чтения ~ 6 мин.Python 3117
У Python есть одна интересная особенность, которая его выделяет среди других языков программирования. Мы знаем, что Python — интерпретируемый язык, но его способ интерпретации несколько отличается от других похожих. В свою очередь это порождает достаточно неочевидные вещи в поведении самого Питона. Знание таких особенностей, позволяет лучше понять поведение Python.
Компилятор и интерпретатор
Для начала рассмотрим отличие компилятора от интерпретатора. Ключевое различие в том, что компилятор на выходе даёт исполняемый код. То есть исходный код программы используется для генерации другого кода — исполняемого. Именно исполняемый файл и работает. Если мы изменим исходный код, то потребуется новая компиляция, чтобы получить рабочую программу.
Интерпретатор работает непосредственно с исходным кодом. Сам по себе интерпретатор как бы сопоставляет команды из исходного файла с тем, что уж есть в интерпретаторе и построчно их выполняет. Если изменить исходный код, то интерпретатор на лету подхватит изменения.
Анализ исходного кода
Перед тем, как компилятор или интерпретатор начнут выполнение программы обычно проверяется синтаксис исходного кода. Например проверка парности скобок, кавычек, отступов и т.д. После этого нужно проверить ключевые слова, потом доступность функций, модулей и т.п. Когда этот этап пройдён, проверяются типы данных, всякие логические ошибки — то есть перед запуском программы следует убедиться, что она корректно написана и может быть выполнена.
Компилятор, очевидно, должен пройти все этапы проверки и лишь в конце происходит сборка программы. Если на любом этапе возникают ошибки, то компилятор просто откажется собирать итоговый файл.
Интерпретатор же, работает чуть по другому. Поскольку исполняется каждый исходный файл, то проверка на корректность происходит уже во время его выполнения. При этом интерпретатор может как обрабатывать файл целиком, так и построчно.
Интерпретация целого файла
Для примера рассмотрим PHP, поскольку именно в нём реализован подобный подход. Интерпретатор PHP анализирует файл целиком. Если в любой строчке файла есть ошибки, то PHP не позволит запустить данный файл. Например, такой простой файл, где мы специально допустим ошибку:
<?php print('hello'); blabla
Из за «blabla» PHP вывалится с ошибкой «PHP Fatal error: Uncaught Error: Undefined constant "blabla"». При этом первая строчка с «hello» не будет выполнена.
Теперь рассмотрим другой код:
<?php print('hello'); a(10); function a($x) { print($x); }
Он выполняется как положено, но обратите внимание, что функцию мы объявили уже после того, как её использовали. То есть интерпретатор загрузил весь файл, построил по нему карту функций и сделал их доступными как минимум в пределах данного файла.
Теперь для исследования давайте создадим ещё одну функцию с тем же именем и попробуем выполнить файл. PHP вываливается с ошибкой «PHP Fatal error: Cannot redeclare a()». То есть мы не можем создать две функции с одинаковым именем.
Запомним эти особенности и теперь посмотрим как ведёт себя Python
Построчная интерпретация
Выполним тот же код, чтобы понять отличия:
print('hello') blabla
Python вываливается с ошибкой «NameError: name 'blabla' is not defined». Но при этом первая строчка с «hello» была выполнена. То есть Python выполняет код построчно.
Теперь второй пример:
print('hello') a(10) def a(x): print(x)
Опять ошибка: «NameError: name 'a' is not defined». То есть Python ничего не знает о том, что функция объявлена ниже.
Попробуем объявить две одноимённые функции:
print('hello') def a(x): print(x) def a(x): print(x*2) a(10) # 20
Когда дело дойдёт до выполнения функции, то Python использовал последнюю объявленную.
Из этого можно сделать важный вывод: Python построчный интерпретатор, где команды выполняются сверху вниз, и это отличает его от других интерпретаторов. Наиболее близким по поведению к Python можно назвать Basic, который когда-то был крайне популярным.
Байт-код
Байт-код — это специальный файл, который является промежуточным перед непосредственным исполнением файла. Например в Java компилятор отдаёт не исполняемый файл, а байт-файл, который в свою очередь выполняется виртуально машиной (она под конкретную систему). В Python что-то подобное — исходный код трансформируется в байт-код, только в памяти и потом интерпретируется виртуальной машиной.
Но есть одна особенность — если в других языках, байт-код формируется уже после проверки на ошибки, то в Python — он формируется безусловно как есть. Для примера можно любой пример с ошибками скомпилировать в pyc-файл (это и есть байт-код) с помощью python -m compileall .
и убедиться, что файлы создаются без каких-либо проблем.
Таким образом байт-код в Python — это просто бинарная форма исходного кода без особых проверок.
Особенности Python
Они проистекают именно из-за этой технической особенности интерпретатора.
Мы можем выполнять код до строки с ошибкой
Это довольно сильно затрудняет отладку. Например если в файле какой-то ошибочный код выполняется после многочисленных циклов или условий, то мы можем никогда об этом не узнать, пока не сработают все эти условия или циклы.
Можно затереть любые другие функции
На Python можно спокойно переписать любые, даже стандартные функции. Например такой вот «дикий» код:
print1 = print def print(x): print1('hello') print(10) # hello print(20) # hello print(30) # hello
Теперь на любой print()
будет выводиться «hello».
Другой пример: можно «убить» любую функцию:
print = 10 list = 20 str = 30 dict = 40 range = 50
Вспомните как часто для имени строки используется «str»? А в Python это стандартная функция. Можно случайно затереть любую функцию и Python даже не выдаст предупреждения.
При программировании на Python нужно как-то следить за именованием функций и переменных. Можно случайно использовать одно имя для функции и переменной, а потом не понимать почему код работает неверно. Проблему можно было бы решить, если в Python можно было бы использовать какой-то символ для обозначения переменных: как в PHP символ «$». Но в Python доступны только цифры, буквы и символ подчеркивания.
Стоит ли удивляться обилию идентификаторов с подчеркиванием в Python-программах? Просто ничего другого нет...
Я бы посоветовал для переменных использовать верблюжью нотацию Pascal (т.н. CamelCase), где первый символ указывается с большой буквы:
My = 'google' MyName = 'Маша' MyAge = 26
Она наиболее близка к общепринятой (mixedCase) и избавит от использования символа подчеркивания.
Неожиданное выполнение кода
Построчное выполнение кода в Python возможно в достаточно неожиданных местах, например при описании класса:
class MyClass: print('Як справи?') k = 42 def out(self): print(10) print('Hello!') print(k)
Простой запуск кода, выведет «Як справи?», «Hello!» и переменную k
, хотя мы даже не объявляли создание объекта.
Кстати этот пример показывает особенности ООП в Python. Если в других языках инструкция class
это начало шаблона типа данных, единый блок, который не должен выполняться вне объекта, то Python всё это воспринимает «в лоб» — для него class
— максимум отдельная область видимости (scope) для методов и полей, а всё что внутри выполняется как обычный код.
Это как раз «чудеса» работы интерпретатора, а не какая-то «особенная объектная модель» Python.