Найти тему
Nuances of programming

Почему я перехожу с Python на Rust

Источник: Nuances of Programming

Предыстория

Поскольку я буду выражать собственное мнение о том, “почему не Python”, немного расскажу о себе. 4 месяца назад я уволился с последнего места работы и сейчас являюсь соучредителем компании MotivEdge. Наиболее часто используемым мной языком программирования был Python, так как я в основном разрабатывал бэкенд Django и скрипты ROS на Python. Я начал изучать этот язык около 10 лет назад и сразу же полюбил его, поскольку, в отличие от C ++ или Java, он давал мне ощущение “свободы”.

Кроме того, я использовал довольно много пакетов, связанных с машинным обучением/анализом: PyTorch, numpy, pandas, scikit-learn, jupyter и другие. Эти пакеты впечатляли своим удобством и простотой в использовании.

Но теперь я переключаюсь на Rust, так как Python стал доставлять мне слишком много проблем при разработке больших проектов. Теперь обо всем по порядку.

Истоки проблем с Python

1. Обезьяний патч

Monkey patch (обезьяний патч)  —  это способ расширить или изменить код среды выполнения динамических языков без изменения оригинального исходного кода.

Эта функция дает разработчику большую свободу, позволяя править код, чтобы заставить его работать. Самым показательным примером использования обезьяньего патча является написание тестов, с которыми мы не можем справиться “корректно” во время выполнения.

Но эта функция слишком динамична и может привести к непредсказуемым ошибкам, если программист не будет достаточно внимателен. Например, иногда переменной с одинаковым именем могут быть присвоены два разных типа. Подобные “отклонения” в поведении кода происходят, когда проект растет и никто не может запомнить всех имен использованных переменных. Члену команды, не посвященному в курс дела, будет трудно понять, “почему код присваивает новый тип”.

2. Типы параметров функций

Эта проблема, как и обезьяний патч, вызвана динамикой языка. Например, у нас есть функция для сложения двух чисел:

def add(num1: int, num2: int) -> int:
return num1 + num2

Но при ее использовании компилятор Python не контролирует тип параметра. Подсказки по проверке типов больше похожи на медвежьи услуги. Можно вызвать функцию, как показано ниже:

add(“123”, 3)

Конечно, это вызовет ошибку. Чтобы добиться выполнения необходимых действий  —  сложения двух чисел, придется ввести дополнительный код перед возвратом:

def add(num1: int, num2: int) -> int:

if type(num1) != int or type(num2) != int:

# Обработка ошибок или выявление конкретной ошибки!
raise IntNumberError()

return num1 + num2

Теперь код выглядит лучше, так как в нем обработан неправильный тип ввода. Но это чревато дополнительной работой  —  обновлением тестового кода для обработки ветви if. Добавляются, по крайней мере, еще две функции тестирования: num1 не int и num2 не int.

3. Область видимости

Допустим, у нас есть код:

for i in range(3):
pass

print(i)

После выполнения мы видим, что выведено число 2. Но i должно использоваться только в области видимости цикла for! Такой неправильный параметр области видимости делает код сложным для сопровождения и отладки.

Для меня это самая запутанная конструкция в Python. Я видел много примеров кода, в котором программист определяет переменную только в области видимости if, но использует эту переменную после выражений if-else.

4. Скорость исполнения

Очевидно, что Python работает медленно. Если программа объемная и сложная, ее выполнение займет больше времени, чем в других языках программирования. Приходится использовать PyPy или другие компиляторы, чтобы немного увеличить скорость.

5. Контролируемость

При передаче в функцию списка list и строки string оба параметра будут работать по-разному. Например, у нас есть код:

def change(lst, st):
lst.append(4)
st = “new string”

x = [1, 2, 3]
s = “old string”

print(x, s)

change(x, s)
print(x, s)

Вывод будет следующим:

[1, 2, 3] old string

[1, 2, 3, 4] old string

Как видим, list дополнился новым элементом в конце, но параметр string не изменился. Причина в том, что Python передал в функцию ссылку на list и значение string. Оба параметра работают совершенно по-разному.

Такая особенность иногда сбивает с толку: трудно отследить, где изменилось значение переменной. Это увеличивает время отладки при разработке.

Запуск Rust

Проект Rust стартовал еще в 2010 году. Rust  —  это мультипарадигмальный высокоуровневый язык программирования общего назначения, разработанный для обеспечения производительности и безопасности, в том числе безопасного параллелизма. Rust не похож на C и C++, которые могут выдавать нулевой указатель. Компилятор помогает автоматически проверять срок службы и владельца переменной. В большинстве случаев, если код скомпилирован на Rust, то это на 80-90% успешный код.

Вот как Rust решил вышеуказанные проблемы Python.

1. Обезьяний патч

Rust  —  это статический язык программирования. Он не допускает динамических исправлений по методу обезьяньего патча. Во-первых, в нем нельзя назначать разные типы данных для одной переменной. Во-вторых, определенная переменная по умолчанию неизменяема. Если нужно создать изменяемую переменную, необходимо определить ее с помощью явно указанного ключевого слова mut.

2. Типы параметров функций

Как было упомянуто выше, Rust статичен и требует определения типа при передаче параметров функции. Передача некорректных типов вызовет возражение компилятора. В этом языке компилятор помогает устранять потенциальные ошибки, избавляя от применения бесполезного предложения if для проверки типа и сокращая время модульного тестирования. Это значит, что вы можете сосредоточиться на логике и алгоритмах тестового кода.

3. Область видимости

У Rust нет GC (автоматического управления памятью). Он удаляет переменную, когда заканчивается ее срок службы. Тем самым переменная выводится за пределы области видимости. И хотя это не на 100% верно, так как срок службы не то же самое, что область видимости, но для простого кода можно считать, что они совпадают.

Таким образом, компилятор выдаст ошибку, если в коде будет указанная выше переменная i вне цикла:

for i in 0..3 {
println!(“{}”, i);
}

println!(“{}”, i); // Здесь возникает ошибка

4. Скорость исполнения

Совершенно очевидно, что Rust обладает почти такой же скоростью, как C/C++. Один универсальный совет: не забывайте --release при сборке кода. Избегайте этой довольно распространенной “ошибки”.

5. Контролируемость

В Rust много “магии”, но она не сбивает с толку, как в Python. “Магия” здесь заключается в rust-анализаторе и компиляторе. В Rust компилятор является партнером разработчика, помогая устранять потенциальные ошибки при программировании.

“Магия”, наблюдаемая в приведенном выше коде Python, никогда не произойдет в Rust. И на это есть несколько причин.

  • При определении функции в Rust необходимо определить тип параметра. Если нужно передать в функцию указатель/ссылку, перед параметром добавляется &.
  • Если нужно изменить что-то в параметре при передаче его в функцию, требуется явно определить mut.
  • Если ссылка в функцию не передается, то право собственности на параметр переходит к функции. Это означает, что переменная будет “удалена” после вызова этой функции.

Итак, в Rust нет “магии” Python. Мы знаем, как данные передаются и применяются в основном коде и функциях. То, как мы используем переменные или параметры, полностью контролируется нами.

Заключение

Rust моложе, чем Python. Большинство крейтов находятся в стадии разработки. Но сообщество растет и активно действует. Создается много отличных крейтов, многие компании расширяют свою команду Rust.

Я какое-то время программировал на Rust, и мне не просто понравилось  —  я наслаждался этим процессом. Правда, иногда я чувствовал, что компилятор “убьет” меня из-за слишком большого количества ошибок.

Конечно, мы все еще можем использовать Python и другие языки, поскольку их библиотеки помогают выполнять многие задачи. У Rust может не быть соответствующего крейта. Но почему бы нам не создать его? Давайте внесем свой вклад в Rust!

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Marshal SHI: Why I started Rust instead of stick to Python