Императивное программирование. Вызов функций ч2.
В предыдущей своей статье я затронул общую упрощенную механику, на основе изученного мной материала, вызова функций и нитей исполнения кода. Но вскоре, идея раскрыть тему функций более подробно, все же заставила меня написать вторую часть статьи, в которой я попытаюсь глубже описать процессы происходящие при вызове и исполнении функции, на примере изучаемого мной языка Python.
Питон – язык, использующий интерпретатор (в самом распространенном случае это CPython, реализованный на C). Вообще, тема трансляторов - это отдельная тема для изучения и освещения, в данной статье я затрону ее очень поверхностно, ровно для понимания темы текущего поста. Итак, существуют два вида трансляторов – это компилятор и интерпретатор.
Как работает компилятор я уже описывал в одной из своих первых статей.
Интерпретатор, в отличие от компилятора (он переводит исходный код сразу в машинный), переводит исходный код сначала в промежуточный байт-код, который выполняется в, так называемой, виртуальной машине и, соответственно, не привязан к конкретной платформе. Эта самая виртуальная машина, в случае с Питоном, имеет стековый тип (бывают еще регистровые виртуальные машины).
Выполняя промежуточный байт-код, в который интерпретатор перевел исходный код, интерпретатор работает со стеком.
Создание функции
По порядку сверху вниз в байт-коде выполняются операторы.
LOAD_CONST - загружает в стек code object
с адресом нашей функции (по сути это объект - в питоне все есть объект).
Далее MAKE_FUNCTION создает из code object
функцию и возвращает ее обратно в стек.
STORE_NAME - связывает полученную функцию с ее именем my_sum
, для доступа к ней.
Вызов функции
Далее в байт-код преобразуется строчка исходного кода:
result = 100 + my_sum(20, 30).
Рассмотрим правую часть выражения. Первым аргументом бинарного оператора сложения (BINARY_ADD) является константа 100
. Вторым оператором является функция my_sum
со своими фактическими параметрами 20
и 30
.
LOAD_CONST - загружает константу со значением 100
в стек.
Далее, LOAD_NAME - загружает нашу функцию my_sum
(точнее ее адрес в памяти, который связан с ее именем ссылкой).
LOAD_CONST – загружает аргументы функции (константы 20
и 30
). Т.к. структура «стек» имеет схему доступа, работающую по принципу LIFO (last in — first out, «последним вошел — первым вышел»), то на верхушке стека оказывается последний загруженный элемент, а именно второй аргумент функции – число 30
.
CALL_FUNCTION [2] – Происходит вызов функции с двумя аргументами. После чего создается еще один новый пустой стек (на рисунке стек-2) для выполнения функции.
Выполнение функции
В стек-2 (стек выполнения функции my_sum
) передаются 2 аргумента из нашего стек-1 – это константы 30
и 20
:
В байт-коде:
LOAD_FAST [‘a’] – передает значение аргумента a
.
LOAD_FAST [‘b’] – передает значение аргумента b
.
Бинарный оператор сложения – BINARY_ADD производит сложения переменных a
и b
.
Результат работы функции возвращается оператором RETURN_VALUE.
Таким образом, на место вызова функции my_sum
в стек-1 (по адресу возврата) вернется значение 50
. После чего функция завершает свою работу, все ее локальные переменные будут удалены из памяти сборщиком мусора - Garbage Collector (это также тема для отдельной статьи:)
Далее, у нас следует (по исходному коду) оператор сложения константы 100
и 50
- результата выполнения нашей функции.
Аналогичным образом выполняется BINARY_ADD с 2 аргументами 100
и 50
, и результат выполнения связывается ссылкой с именем переменной result
- выполняется оператор присваивания. Таким образом, переменная result = 150
.
Данный пример более наглядно и реалистично показывает механику вызова функций на примере Python.
Вообще, я считаю, что понятие функции (как подпрограммы) в программировании, носит фундаментальный характер. Ведь, если абстрактно взглянуть на программирование, то мы увидим, что объект это, ничто иное, как функция с внутренним состоянием (памятью). Класс – это генератор объектов, по сути, тоже функция, которая возвращает как результат - объект (который так же является функцией), у каждого такого объекта своя изолированная память – инкапсуляция. Ну и если сюда добавить концепцию наследования, то мы получаем ООП :)