Cat

Сравнение интерпретатора Python и компилятора C

В этом посте я продолжу свою рубрику о сравнении работы двух языков программирования: Python и C. Вас ждёт сравнение циклов for, а также я сравню интерпретатор Python и компилятор C. Покажу вам все фазы на этих языках: от написания кода в IDE до запуска программы на компьютере.

Сравнение Python и С Arduinum628 12 Март 2024 Просмотров: 378

Всем доброго дня! Сегодня я продолжу свою рубрику о сравнении работы двух языков программирования: Python и C. В предыдущей статье я сравнивал типизацию Python и C и показал процесс компилирования программы на C. Компиляция проводилась, чтобы посмотреть на результат работы программы C. Тут у читателя сразу могли появиться вопросы: "В Python нет компиляции, что же происходит там?" или "Где сравнение того, что происходит под капотом у этих двух языков?". Конечно, я не могу обойти эту интересную и сложную тему без освещения в своей рубрике. Сегодня я сравню интерпретатор Python и компилятор C. Поэтому возьмите себе вкусняшек пожевать и чаю – мы погружаемся ещё глубже в сравнение Python и C.

Сравнение компилятора и интерпретатора

Для того чтобы увидеть полную картину, в этой статье я сравню все фазы от написания кода в IDE до выполнения работы программы. Так вы увидите весь процесс более глубоко и вам будет проще понять какие процессы прячутся во время выполнения программ на C и Python. Я люблю показывать работу программы на конкретных примерах, а не абстрактно, и в этом нам помогут циклы for. Заодно я наглядно покажу в чём схожесть и различия по синтаксису при написании этого цикла на Python и C.

Фаза 1: написание программы в IDE

Данная фаза довольно банальна, но без неё не обходится написание ни одной программы. Начнём с идеи того, что мы реализуем в коде. Думаю, для примера нам не нужно писать что-то сложное. Давайте напишем цикл, который будет проходить по списку чисел, запишет в переменную сумму этих чисел и выдаст на экран результат. Начнём с реализации данной программы на языке Python.

nums_list = [1, 3, 4, 50, 20]
sum_nums = 0
for num in nums_list:
    sum_nums += num
print(f'Сумма чисел списка = {sum_nums}')

Пример выше на Python довольно простой. У нас есть список чисел nums_list, который мы обходим циклом for. В цикле for мы проходим по списку чисел и складываем каждое число из списка с числом из переменной sum_nums, где хранится результат. В конце функция print выдаст нам результат в виде отформатированной строки при помощи f строки, в которую мы вставляем результат суммы чисел из переменной sum_nums. Давайте теперь напишем реализацию подобного кода на языке C.

#include 
int main(){
    int arr_nums[] = {1, 3, 4, 50, 20};
    int size = sizeof(arr_nums) / sizeof(arr_nums[0]);
    int sum_nums = 0;
    for (int i=0; i < size; i++) {
        sum_nums += arr_nums[i];
    }
    printf("Сумма чисел массива = %d\n", sum_nums);
    return 0;
}

Я буду пропускать то, что вы уже знаете из предыдущего поста, и сосредоточусь на новом для вас синтаксисе. Давайте посмотрим на пример выше внимательнее. Начнём со строки int arr_nums[] = {1, 3, 4, 50, 20}; слева направо. Начало int arr_nums[] - это массив целых чисел, а далее ему присваивается его содержимое = {1, 3, 4, 50, 20};, то есть числа для массива. Читатель может задаться вопросом: а что такое массив и чем он отличается от списка? Могу сказать, что тема коллекций достаточно обширна, поэтому предлагаю раскрыть её отдельно в следующей моей статье. Пока ограничусь тем, что массив, или array, в языке С имеет фиксированную длину в отличие от списка, или list, в Python, который является динамическим. Это означает что в array C нельзя добавить новый элемент после его создания (если превышен размер массива), в то время как в list Python нет фиксированного размера и можно добавить сколько угодно новых элементов в список. Компилятор в C будет автоматически определять размер массива на основе количества его элементов. Также размер массива можно указать явно int arr_nums[5] = {1, 3, 4, 50, 20};, где цифра в квадратных скобках это количество элементов массива. В следующей главе я покажу вам, что будет если указать число меньше или больше количества чисел в массиве :) Перейдём к следующей строке кода int size = sizeof(arr_nums) / sizeof(arr_nums[0]);. Первая часть строки нам уже понятна: это целочисленная переменная int size, которая, как мы видим из названия, означает размер. Идём по строке кода далее и видим, что ей присваивается результат следующего математического выражения = sizeof(arr_nums) / sizeof(arr_nums[0]);. Давайте рассмотрим левый операнд sizeof(arr_nums). Оператор sizeof принимает array и возвращает размер списка в байтах. Размер одного элемента на большинстве операционных систем составляет 4 байта, а так как в массиве 5 элементов, то 4 * 5 = 20 в итоге получим 20 байт. Мы получили размер списка 20 байт в левом операнде. Как вы можете догадаться, далее мы делим размер списка на размер первого элемента, взяв его по индексу 0. В итоге получается математическая операция 20 / 4 = 5 , результатом которой будет количество элементов в списке – 5. Для чего это нужно, нам объяснит цикл for. Рассмотрим первую строку цикла for (int i=0; i < size; i++) {. Читателю, должно быть, сразу бросается в глаза, что мы для использования переменной индекса объявляем целочисленную переменную и присваиваем ей число 0 в int i=0;. Она нам будет необходима для взятия элемента списка по индексу, в то время как в моём примере цикла for в Python при проходе по списку я сразу получал его элемент, не используя индекс. Прилагаю строку из Python для наглядности: for num in nums_list:. Далее идёт условие i < size;, после которого произойдёт выход из цикла. Для корректной работы цикла в i++ мы будем увеличивать переменную для индекса после каждого прохода цикла for. В результате мы получим числа для индекса 0, 1, 2, 3, 4 и выйдем из цикла for, когда i станет равна 5 и условие 5 < 5 не выполнится. Это очень похоже на цикл while в Python из-за выхода из цикла for по условию. В одной из следующих статей я расскажу о while в Python подробнее и сравню его с while из C. Вернёмся к нашему циклу и посмотрим на операцию в его теле sum_nums += arr_nums[i];. Оператором присвоения += складываем переменную sum_nums c числом из arr_nums[i], которое мы берём по индексу из списка. Результат присваивается переменной sum_nums. Не забываем про {} для тела цикла и ; для обозначения конца инструкции. Для правильной работы компилятору нужно понимать, где конец инструкции. В Python мы аналогично используем оператор присвоения +=, но используем значение переменной, а не берём число по индексу из списка в sum_nums += num.

Фаза 2: препроцессинг

После того как мы написали программный код, мы обязательно захотим его выполнить и посмотреть, как работает наша программа. Для этого мы запускаем процесс компиляции в C.

В terminal

$ gcc sum_nums.c -o sum_nums

Как же с этим связан препроцессинг? Давайте разберёмся, что такое препроцессинг и для чего он нужен. В языке C есть программа-препроцессор, которая запускается автоматически перед компиляцией. Препроцессор языка C нужен для выполнения специальных команд, которые называются директивами препроцессора. Они определяют операции, которые должны быть выполнены до компиляции. В нашем коде, написанном на языке C, есть директива #include , в которой это заголовочный файл для библиотеки, хранящий в себе функции ввода/вывода (пример printf()). Директива в языке программирования C - это инструкция для препроцессора, которая выполняется до фазы компиляции. Директива #include используется для включения библиотек. Если мыслить на языке Python, то это похоже на то, что мы импортировали библиотеку в код целиком и теперь можем её использовать во время выполнения программы. На языке Python мы пока что ничего не будем делать, ибо у него нет стадии препроцессинга, так как он не требует предварительной обработки.

Фаза 3: компиляция

После того как отработал препроцессор, который обработал нам директивы, начинается процесс компиляции в C. Сначала компилятор выполняет лексический и синтаксический анализ исходного кода, чтобы проверить его на наличие ошибок. Если ошибок не обнаружено, то компилятор транслирует программу в машинный код. Машинный код — это низкоуровневый код, который может быть напрямую выполнен процессором компьютера. Он представляет собой последовательность чисел, которые являются инструкциями для процессора. Если обнаруживается ошибка, то компилятор выводит сообщение с ошибкой компиляции. Я специально уберу индекс из кода sum_nums += arr_nums[]; чтобы показать, как выглядит ошибка при компиляции.

В terminal

sum_nums.c: In function ‘main’:
sum_nums.c:9:30: error: expected expression before ‘]’ token
    9 |         sum_nums += arr_nums[];
      |                              ^

Давайте разберёмся в ошибке. Мы видим, что ошибка в функции main, в строке 9, видим саму строку с ошибкой error: expected expression before ‘]’ token. Ошибка нам говорит, что компилятор ожидает некое выражение перед закрытием ], что нам намекает на допущенную ошибку в синтаксисе: мы забыли что-то написать. В нашем случае мы не указали переменную i, которая отвечает за индекс. Мы должны были взять элемент по индексу из списка. Я исправлю данный код и продолжу процесс дальше. Машинный код пока рано показывать на данной стадии. Что же касается языка Python, то у него нет этапа компиляции, так как это не компилируемый язык. Вместо этого у него есть процесс интерпретации, до которого мы дойдём, когда, наконец, получим исходный файл языка С, чтобы сравнить процесс запуска программ.

Фаза 4: компоновка

Компоновка (или линковка) в языке C – это процесс, в котором компоновщик собирает различные модули кода, такие, как исходные файлы и библиотеки, в один исполняемый файл. Процесс компоновки делится на следующие шаги:

  1. Каждый исходный файл C компилируется в отдельный объектный файл (обычно с расширением .o или .obj), который содержит машинный код и информацию для компоновщика;
  2. Компоновщик анализирует объектные файлы и библиотеки, чтобы определить, какие внешние ссылки (например, функции и глобальные переменные, которые определены в других файлах) необходимо разрешить;
  3. Компоновщик включает в исполняемый файл библиотеки, необходимые для выполнения программы, и разрешает ссылки на функции и данные, которые определены в этих библиотеках;
  4. Если в программе используются ресурсы, такие, как изображения или звуки, компоновщик также включает их в исполняемый файл;
  5. Наконец, компоновщик генерирует исполняемый файл, который содержит все необходимые машинные инструкции, данные и ресурсы, собранные из объектных файлов и библиотек;

После процесса компоновки у нас с вами получился исполняемый файл sum_nums. Давайте посмотрим, как выглядит машинный код у него внутри.

Откроем исполняемый файл sum_nums с помощью sublime text

7f45 4c46 0201 0100 0000 0000 0000 0000
0300 3e00 0100 0000 8010 0000 0000 0000
4000 0000 0000 0000 d036 0000 0000 0000
0000 0000 4000 3800 0d00 4000 1f00 1e00
0600 0000 0400 0000 4000 0000 0000 0000
4000 0000 0000 0000 4000 0000 0000 0000
d802 0000 0000 0000 d802 0000 0000 0000
0800 0000 0000 0000 0300 0000 0400 0000
1803 0000 0000 0000 1803 0000 0000 0000
1803 0000 0000 0000 1c00 0000 0000 0000
...

Перед вами первые 10 строк файла sum_nums из 1002. Естественно, я не мог впихнуть все 1002 строки в данную, статью поэтому сократил вывод.

Каждая строка – это 64-битное значение в шестнадцатеричной системе счисления. Чтобы понять содержимое файла, нужно знать архитектуру процессора и много чего ещё. Возможно, я затрону эту тему в будущем, но пока вам достаточно того, что это машинный код, который выполняет процессор компьютера.

В языке Python нет процесса компоновки, так как это не компилируемый язык и не требуется генерировать исполняемый файл, как в языке C. Однако стоит отметить, что есть такие механизмы как Cython и ctypes. Cython - статический компилятор, который позволяет языку Python компилировать код на C и C++ и вызывать его из Python3. Ctypes - это библиотека для Python, которая позволяет Python взаимодействовать с C кодом, что позволяет Python работать на более низком уровне. Правда, это касается взаимодействия языков C и Python. Это довольно интересная тема, и в последующих статьях я обязательно её затрону. Сам же Python по-прежнему не нужно компилировать и компоновать. Это касается только C кода, который можно использовать в коде Python.

Фаза 5: загрузка

Для того чтобы программу можно было запустить, её сначала нужно загрузить в оперативную память. В языке C эту операцию выполняет загрузчик, который читает выполняемый образ с диска и копирует его в оперативную память. Также загрузчик производит загрузку необходимых разделяемых библиотек. В языке Python программный код загружается в оперативную память полностью для того, чтобы выполниться потом построчно. На этом этапе, если в коде обнаружится ошибка, программа будет прервана. Теперь самое время запустить наши программы на обоих языках, так как выйти на эту фазу невозможно без запуска программы.

В terminal

$ ./sum_nums

В terminal

$ ./sum_nums.py

Фаза 6: выполнение

Наконец, мы дошли до финальной фазы: выполнение кода. На этой фазе компьютер выполняет программу, инструкция за инструкцией, в языке C. Давайте посмотрим на вывод программы у программы на языке C.

В terminal

Сумма чисел списка = 78

Мы видим, что программа на языке C отработала как нужно. Только вот прежде чем я расскажу, что произойдёт с программой на Python, вас будет ждать небольшой сюрприз с выполнением программы.

В terminal

bash: ./sum_nums.py: Отказано в доступе

До этого мы запускали программы на Python из IDE и не испытывали проблем с запуском программ. Сейчас мы видим, что у нас нет прав на файл. Разрешим права на него.

В terminal

chmod +x sum_nums.py

Попробуем запустить программу после того, как мы выдали права на выполнение файла для всех пользователей.

В terminal

$ ./sum_nums.py
./sum_nums.py: строка 1: nums_list: команда не найдена
./sum_nums.py: строка 2: sum_nums: команда не найдена
./sum_nums.py: строка 5: синтаксическая ошибка рядом с неожиданным маркером «sum_nums»
./sum_nums.py: строка 5: `    sum_nums += num'

Почему вместо привычного вывода он выводит эти странные предупреждения, ведь у нас нет ошибок в коде? Всё очень просто. Это происходит, потому что в нашем файле sum_nums.py нет строки, которая бы указывала нам на интерпретатор Python. В Debian или Ubuntu она пытается выполниться как скрипт shell. Именно поэтому мы видим вывод неких ошибок при запуске. Поправим это, добавив на первую строчку кода #!/usr/bin/env python3. Строка #!/usr/bin/env python3 называется шебанг (shebang), которая указывает путь до интерпретатора Python. Таким образом в программе будет информация о нахождении интерпретатора Python, к которому следует обратиться для выполнения программы. Наш код теперь будет выглядеть вот так.

#!/usr/bin/env python3
nums_list = [1, 3, 4, 50, 20]
sum_nums = 0
for num in nums_list:
    sum_nums += num
print(f'Сумма чисел списка = {sum_nums}')

Давайте запустим нашу программу после изменений и посмотрим на результат её выполнения.

$ ./sum_nums.py
Сумма чисел списка = 78

Мы видим, что программа на Python выполнилась успешно, но как же это происходит в итоге?

Процесс загрузки и интерпретации кода Python включает в себя несколько этапов, которые происходят на лету:

  1. Python считывает код построчно, интерпретирует его и преобразует в байт-код;
  2. Интерпретатор Python компилирует байт-код в машинный код для выполнения;
  3. Машинный код выполняется на процессоре;

Тут мы увидели новое для себя слово байт-код. Что же такое байт-код? Байт-код в Python – это промежуточное представление исходного кода Python, которое интерпретатор Python использует для выполнения программы. Когда вы запускаете Python-скрипт, компилятор Python преобразует исходный код в байт-код, который затем интерпретируется и выполняется виртуальной машиной Python. Этот процесс позволяет Python быть интерпретируемым языком, что делает его более портативным и удобным для разработчиков, поскольку не требуется компиляция исходного кода в машинный код для каждой целевой платформы.

Байт-код Python хранится в файлах с расширением .pyc, которые создаются автоматически при первом запуске скрипта. Эти файлы содержат байт-код, который может быть быстрее загружен и выполнен, поскольку он уже был скомпилирован. Файлы.pyc обычно находятся в подкаталоге __pycache__ рядом с исходными файлами .py.

Использование байт-кода имеет несколько преимуществ:

  1. Байт-код может быть быстрее загружен и выполнен, поскольку он уже был скомпилирован;
  2. Поскольку байт-код не зависит от конкретной архитектуры процессора, Python-программы могут быть легко перенесены между различными платформами;
  3. Байт-код может быть защищен от прямого чтения исходного кода, что может быть полезно для скрытия логики приложения;

Пару слов о виртуальной машине Python, о которой я упомянул выше. Виртуальная машина в Python – это компонент, который интерпретирует байт-код Python и выполняет его. В контексте Python, виртуальная машина – это часть интерпретатора Python, которая преобразует байт-код в исполняемые инструкции.

Поздравляю, мы прошли все фазы от создания программы в ide до её выполнения на процессоре и сравнили фазы на Python и C :)

Выше в данной статье, рассказывая о цикле for, я упомянул о цикле while, пообещав вам рассказать о нём. Раз в данной статье я начал с циклов for, то в следующей статье я продолжу тему циклов. Расскажу вам, чем отличаются цикл while в C от цикла while в Python. Расскажу, для чего эти циклы нужны и покажу их применение на интересной задаче. Дальше будет ещё интереснее :)

Заключение

Вот что мы сделали в данной статье:

  1. Узнали про отличия цикла for в C от цикла for в Python;
  2. Узнали тонкости запуска скрипта на Python;
  3. Прошли и сравнили все фазы: от создания программы в IDE до её выполнения на процессоре на Python и C;

Автор

    Нет комментариев

    Реклама