company_banner

Rust глазами Python-разработчика #2

    Снова привет! Мы – @cbmw и @AndreyErmilov, часть команды разработки «Рамблер/Медиа» (портал «Рамблер»). И это вторая часть наших размышлений по поводу сравнения Python и Rust (первая часть).

    Если не хочется читать эту статью или лень возвращаться к первой части материала, можно посмотреть видео нашего выступления:

    Многопоточность

    Интересно, что использование типизации в Rust не ограничивается только тем, о чем мы говорили в первой части. Одна из областей, в которой хорошо видны ее преимущества, – многопоточное программирование.

    Для начала давайте рассмотрим достаточно классический пример:

    import threading
    
    x = 0
    
    def increment_global():
        global x
        x += 1
    
    def taskof_thread():
        for _ in range(50000):
            increment_global()
    
    def main():
        global x
        x = 0
        t1 = threading.Thread(target=taskof_thread)
        t2 = threading.Thread(target=taskof_thread)
        t1.start()
        t2.start()
        t1.join()
        t2.join()
    
    
    if __name__ == "__main__":
        for i in range(5):
            main()
            print("x = {1} after Iteration {0}".format(i, x))

    Этот код наглядно иллюстрирует ошибку «состояние гонки». Разберем чуть подробнее, что тут происходит. В коде мы видим запуск двух потоков, которые в цикле инкрементируют значение общего счетчика.

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

    Надо проверить. Запустим код:

    $ python race.py
    x = 100000 after Iteration 0
    x = 100000 after Iteration 1
    x = 86114 after Iteration 2
    x = 58422 after Iteration 3
    x = 89266 after Iteration 4

    Видно, что 2-я, 3-я и 4-я итерации потеряли часть инкрементов.

    Надо это исправить. Один из вариантов решения проблемы «состояние гонки» – это использование примитивов синхронизации, в данном случае попробуем использовать Mutex. Все, что нам нужно, – создать сам Mutex и передать его в качестве аргумента в каждый тред:

    lock = threading.Lock()
    t1 = threading.Thread(target=taskof_thread, args=(lock,))
    t2 = threading.Thread(target=taskof_thread, args=(lock,))

    Кроме того, нужно изменить саму функцию taskof_thread, выполняющуюся в каждом из потоков:

    def taskof_thread(lock):
        for _ in range(50000):
            lock.acquire()
            increment_global()
            lock.release()

    Мы обернули вызов increment_global функциями взятия и освобождения Mutex и тем самым заблокировали параллельное изменение счетчика из нескольких потоков. Теперь остальные треды будут ждать окончания выполнения инкрементации, если она уже была запущена в каком-либо потоке.

    Проверим, что все починилось:

    ❯ python race.py
    x = 100000 after Iteration 0
    x = 100000 after Iteration 1
    x = 100000 after Iteration 2
    x = 100000 after Iteration 3
    x = 100000 after Iteration 4

    Да, теперь все работает корректно. Ну и по традиции рассмотрим этот пример в Rust. Если попробовать переписать Python-код «в лоб», получится следующее:

    use std::thread;
    
    static mut X: i32 = 0;
    
    fn increment_global() {
        X += 1;
    }
    
    fn thread_task() {
        for _ in 0..50_000 {
            increment_global()
        }
    }
    
    fn main_task() {
        let t1 = thread::spawn(thread_task); 
        let t2 = thread::spawn(thread_task); 
    
        t1.join().unwrap();
        t2.join().unwrap();
    }
    
    fn main() {
        for i in 0..5 {
            main_task();
            println!("x = {} after Iteration {}", X, i);
        }
    }

    Этот код делает все то же, что и Python, за одним небольшим исключением – вы не сможете его скомпилировать.

       Compiling playground v0.0.1 (/playground)
    error[E0133]: use of mutable static is unsafe and requires unsafe function or block
     --> src/main.rs:6:5
      |
    6 |     X += 1;
      |     ^^^^^^ use of mutable static
      |
      = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
    
    error[E0133]: use of mutable static is unsafe and requires unsafe function or block
      --> src/main.rs:26:47
       |
    26 |         println!("x = {} after Iteration {}", X, i);
       |                                               ^ use of mutable static
       |
       = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
    
    error: aborting due to 2 previous errors
    
    For more information about this error, try `rustc --explain E0133`.
    error: could not compile `playground`.
    
    To learn more, run the command again with --verbose.

    Компилятор нам прямо сообщает, что изменение переменной static mut может привести к «состоянию гонки». Да, безусловно, мы можем заставить этот код скомпилироваться, обернув нужные вызовы в unsafe-блоки и переложив ответственность за проверку работы кода с компилятора на разработчика. Не будем приводить этот код тут, можете сами поэкспериментировать с этим. В таком случае код будет работать аналогично неисправленной Python-версии.

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

    use std::{sync::{Arc, Mutex}, thread};
    use lazy_static::lazy_static; // 1.4.0
    
    lazy_static! { static ref X: Mutex<i32> = Mutex::new(0); }
    
    fn increment_global(x: &Mutex<i32>) {
        let mut data = x.lock().unwrap();
        *data += 1;
    }
    
    fn thread_task(x: &Mutex<i32>) {
        for _ in 0..50_000 {
            increment_global(x)
        }
    }
    
    fn main_task(x: &'static Mutex<i32>) {
        let mut threads = vec![];
        for _ in 0..2 {
            threads.push(thread::spawn(move || thread_task(x)));
        }
        for thread in threads {
            thread.join().unwrap();
        }
    }
    
    fn main() {
        for i in 0..5 {
            main_task(&X);
            let mut data = X.lock().unwrap();
            println!("x = {} after Iteration {}", data, i);
        }
    }

    Мы использовали аналогичный подход с использованием Mutex, но в тоже время тут скрывается важное отличие. В Rust Mutex – не просто отдельный тип, никак не связанный с теми данными, к которым он блокирует доступ. В Rust он представляет из себя обертку этих данных, и получить доступ к ним можно только путем взятия этого Mutex. Он отпускается автоматически при выходе из области видимости.

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

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

    Еще стоит упомянуть, что threading.Lock в Python можно использовать в качестве контекстного менеджера, что улучшит ситуацию, но не исправит ее полностью. Как минимум, он не позволит связать его с данными, которые защищены этим локом.

    Но на этом различия в реализации многопоточности в Python и Rust не заканчиваются. Помимо примитивов, являющихся частью системы типов, компилятор предоставляет некоторые гарантии контроля возможности возникновения гонки данных и «состоянию гонки», хотя и не исключает последнее полностью.

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

    Последовательный код:

    fn main() {
        let mut arr = [0, 7, 9, 11];
        arr.iter_mut().for_each(|p| *p -= 1);
        println!("{:?}", arr);
    }

    Параллельный код:

    use rayon::prelude::*;
    
    fn main() {
        let mut arr = [0, 7, 9, 11];
        arr.par_iter_mut().for_each(|p| *p -= 1);
        println!("{:?}", arr);
    }

    Асинхронность

    Подход, описанный выше, применим и при работе с асинхронным кодом. В целом, если говорить об асинхронщине более обобщенно, можно назвать подходы в Python и Rust достаточно похожими. Рассмотрим небольшой пример кода, который (абсолютно) бесполезен на Python:

    import asyncio
    
    
    async def timers():
        futures = []
        for s in range(3):
            futures.append(asyncio.sleep(s))
        await asyncio.gather(*futures)
    
    if __name__ == "__main__":
        asyncio.run(timers())

    И на Rust:

    use tokio::time::{delay_for, Duration};
    use futures::future::join_all;
    
    async fn timers() {
        let futures = (0..2)
            .map(Duration::from_secs)
            .map(delay_for);
        join_all(futures).await;
    }
    
    #[tokio::main]
    async fn main() {
        timers().await;
    }

    По коду можно заметить сходство в использовании методов и построении асинхронного кода. Мы сознательно не будем погружаться в различия и сходства реализации асинхронщины, сейчас нам интересно рассмотреть это на более высоком уровне. В целом, создается впечатление, что перейти с Python на Rust не составляет большой проблемы, и, отчасти, это правда. Однако, так было не всегда: привычный Python разработчикам async/await появился лишь в стабильной версии 1.39.0, вышедшей 7 ноября 2019 года, то есть чуть больше года назад. До этого асинхронный код в стабильном Rust представлял из себя последовательность из комбинаторов:

    fn main() {
        let addr = "127.0.0.1:1234".parse().unwrap();
    
        let future = TcpStream::connect(&addr)
            .and_then(|socket| {
                io::write_all(socket, b"hello world")
            })
            .and_then(|(socket, _)| {
                // read exactly 11 bytes
                io::read_exact(socket, vec![0; 11])
            })
            .and_then(|(socket, buf)| {
                println!("got {:?}", buf);
                Ok(())
            })
            .map_err(|_| println!("failed"));
    
        tokio::run(future);
    }

    Что выглядит для уже привыкших к async/await Python-разработчиков, достаточно чужеродно. Однако в таком подходе есть и плюсы, и минусы, и, вероятно, многие, сталкивающиеся с функциональными языками, увидят в этом много знакомого. Стоит отметить, что комбинаторы доступны и сейчас, наряду с await вы вполне можете сочетать оба этих подхода. Помимо всего описанного выше в Rust, в отличие от Python, широко используется концепция каналов.

    Из сходств можно отметить множественность реализаций event loop-ов. Правда, ни один из языков не лишен проблем при написании кода, абстрагированного от библиотек, реализующих event loop.

    Функциональная парадигма

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

    Функции высшего порядка

    Это первая концепция, которая присутствует и в Python, и в Rust. Как уже стало привычным по ходу статьи, попробуем сравнить несколько вариантов кода:

    from typing import List, Callable
    
    
    def map_each(list: List[str], fun: Callable[[str], int]) -> List[int]:
        new_array = []
        for it in list:
            new_array.append(fun(it))
        return new_array
    
    if __name__ == '__main__':
        languages = [
            "Python",
            "Rust",
            "Go",
            "Haskell",
        ]
        out = map_each(languages, lambda it: len(it))
        print(out)  # [6, 4, 2, 7]

    Выше в примере Python мы сразу использовали модуль typing и, соответственно, описали сигнатуру функции map_each, выполняющую следующее: она принимает два аргумента, один из которых – список со строками, а второй – Callable-объект, или говоря проще, функция. Она принимает в качестве аргумента строку, а возвращает – число. Выглядит все достаточно приятно.

    Посмотрим, что в Rust:

    fn map_each(list: Vec<string>, fun: fn(&String) -> usize) -> Vec<usize> {
        let mut new_array: Vec<usize> = Vec::new();
        for it in list.iter() {
            new_array.push(fun(it));
        }
        return new_array;
    }
    
    fn main() {
        let list = vec![
            String::from("Python"),
            String::from("Rust"),
            String::from("Go"),
            String::from("Haskell"),
        ];
        let out = map_each(list, |it| it.len());
    
        println!("{:?}", out); // [6, 4, 2, 7]
    }

    Выглядит похоже, но есть одно, важное различие: Python не поддерживает многострочные lambda-функции. Это резко ограничивает выразительность и возможности всего функционального подхода. Да, безусловно, это можно пережить, пользуясь, например, обычными функциями, но удобства это определенно не добавляет.

    Кроме того, важным отличием являются принципы реализации «комбинаторов». В Rust большая часть из них – часть трейта Iterator, тогда как в Python это, как правило, самостоятельные функции. Исходя из вышеописанного и того, что трейт Iterator реализован для многих стандартных типов и может быть реализован для ваших типов, вытекает и различие в использовании этих методов. В Rust они достаточно органично объединяются в цепочки, описывая сложные трансформации данных.

    fn main() {
        let list = vec![
            Ok(42),
            Err("Error - 1"),
            Ok(81),
            Ok(88),
            Err("Error - 2")
        ];
    
        let out: Vec<_> = list
            .iter()
            .flatten()
            .collect();
    
        println!("{:?}", out); // [42, 81, 88]
    }

    Выше пример кода, отбрасывающего ошибочные результаты из вектора и преобразующего их в вектор целочисленных значений. Данный код, собственно, строится на том, что уже знакомый нам Result реализует трейт IntoIterator. Еще один пример, иллюстрирующий возможности Rust:

    fn main() {
        let output = ["42", "43", "sorokchetire"]
            .iter()
            .map(|string| {
                    string
                     .parse::<i32>()
                     .map_err(|_| println!("Parsing error"))
            })
            .flatten()
            .map(|integer| integer * 2)
            .fold(0, |base, element| base + element);
        println!("{:?}", output);
    }

    Этот код уже делает чуть больше: происходит итерация по массиву со строками, попытка преобразования каждого элемента массива в i32 и, в случае ошибки, вывода сообщения об ошибке в консоль. Далее происходит отбрасывание некорректных результатов, умножение каждого значения на два и сложение полученных элементов.

    В чем же прелесть таких цепочек? На мой взгляд, это читаемость и простота поддержки. Представим, что нам нужно добавить дополнительное преобразование в пример выше перед вызовом .fold. Очевидно, что добиться этого достаточно просто дополнительным вызовом map. Да, в Python есть концепция Comprehensions, но производить какие-то достаточно сложные манипуляции с данными внутри самого comprehension не совсем удобно.

    Замыкания

    Сама концепция известна и достаточно широко используется и в Python, и в Rust. Вот только система типов Rust позволяет делать некоторые проверки, недоступные в Python. По традиции рассмотрим пример, и на этот раз начнем с Rust:

    fn main() {
        let mut holder = vec![];
    
        let sum = |a: usize, b: usize| -> usize {
            let c = a + b;
            holder.push(a + b);
            c
        };
    
        println!("{}", sum(10, 20));
    }

    Выше мы видим код, описывающий замыкание sum, которое, в свою очередь, принимает два параметра и складывает их. Результат сложения записывается в вектор holder и возвращается из замыкания. Важно отметить, что holder является внешним по отношению к самому замыканию, то есть он олицетворяет некоторый глобальный стейт в этом примере. Попробуем скомпилировать и увидим ошибку:

    error[E0596]: cannot borrow `sum` as mutable, as it is not declared as mutable

    То есть компилятор запрещает нам изменять внешнее состояние в замыкании пока само замыкание не будет объявлено как mut. Исправляется это достаточно банально:

    fn main() {
        let mut holder = vec![];
    
        let mut sum = |a: usize, b: usize| -> usize {
            let c = a + b;
            holder.push(a + b);
            c
        };
    
        println!("{}", sum(10, 20));
    }

    Да, полностью отличить чистые функции на уровне типов это не позволяет, но зато можно понять, какие из замыканий явно изменяют внешнее состояние при своем вызове, что уже хорошо. Хотя, возможно, многим хотелось бы большего. Ситуация тут в целом схожа с ситуацией с изменяемыми и неизменяемыми типами в Rust и Python. В случае Rust компилятор явно просит указывать, какие конкретные наборы данных будут изменяемыми, а какие – нет. Что кажется чуть более гибким подходом, чем тот, что используется в Python, где изменяемость определяется самим типом данных и, по сути, изменить ее нельзя.

    Рекурсия

    И это последняя концепция, которую мы возьмем на рассмотрение. В целом ситуация с рекурсией схожа в обоих языках. Отсутствие оптимизации хвостовой рекурсии характерно и Rust, и Python. Однако llvm (backend rust) позволяет генерировать код с учетом этой оптимизации. Вполне возможно, что однажды в Rust появится и эта оптимизация с гарантированным применением при соблюдении всех условий. Кроме того, различия кроются непосредственно и в ошибках при рекурсивных вызовах. В Python RecursionLimit является обычным исключением и позволяет при необходимости перехватить его и продолжить выполнение приложения. В Rust при превышении уровня вложенности рекурсивных вызовов может произойти ошибка переполнения стека.

    Заключение: Зачем-же питонисту Rust

    Как мы уже говорили, питонисту очень многое приходится делать самому — проверять типы, держать в голове, где будет None, а где число, искать в документации, какие исключения может выбросить функция. Это сложно и поэтому возникает большая часть ошибок, о которых мы говорили в начале. Mypy помогает избавиться только от их части, и компилятор Rust сильно выигрывает в этом у mypy, но при этом можно столкнуться с другими подводными камнями, которые мы пока не рассматривали.

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

    В итоге мы пришли к следующему выводу: разработка приложения на Python действительно привлекательна своей высокой скоростью. Но потом, когда разработанное приложение нужно поддерживать, вылезают все те проблемы, которые были проигнорированы во время разработки. В то же время Rust не позволяет игнорировать возможные варианты, требуя больше времени на разработку, но и благодаря этому потом тратится гораздо меньше усилий на поддержку и рефакторинг.

    Only registered users can participate in poll. Log in, please.

    После публикации первой статьи, нам стало очень интересно ваше мнение по явно вытекающему из нее вопросу. Cчитаете ли вы, полезным, использование опциональной статической типизации в python?

    • 83.8%Да67
    • 16.2%Нет13

    Comments 40

      +2
      Добрый день, спасибо за статью, было интересно взглянуть на ваши примеры своими глазами. От себя я хотел бы добавить комментарий. Я нисколько не профессиональный разработчик Rust, просто решал Адвент календарь, чтобы немножечко выучить язык (я до этого его не знал совсем).

      Так вот, если вы не против, я бы посоветовал свой вариант для примера с функциями высшего порядка:
      немного кода
      use std::iter::FromIterator;
      
      fn map_each(list: &[String], fun: fn(&String) -> usize) -> Vec<usize> {
          Vec::from_iter(list.iter()
                             .map(fun))
      }
      
      fn main() {
          let list = vec![
              String::from("Python"),
              String::from("Rust"),
              String::from("Go"),
              String::from("Haskell"),
          ];
          let out = map_each(&list, String::len);
      
          println!("{:?}", out); // [6, 4, 2, 7]
      }
      Здесь есть несколько интересных вещей:
      1. вектор можно создавать из итератора;
      2. можно использовать вот такой подход.

      А если вы не против, то ещё один комментарий по поводу следующего примера (не знаю, насколько это сильно отличается от вашей идеи), но я могу предложить такой вот вариант:
      Здесь мы не отображаем ошибки
      fn main() {
          let output = ["42", "43", "sorokchetire"]
              .iter()
              .map(ToOwned::to_owned)
              .map(&str::parse::<i32>)
              .filter_map(Result::ok)
              .map(|integer| integer * 2)
              .fold(0, |base, element| base + element);
              
          println!("{:?}", output);
      }

        +1

        Спасибо большое за комментарий! Ваши примеры выглядят более "rust"-way, если можно так сказать, и, безусловно, имеют место быть.
        В самой статье, примеры на rust иногда, в некоторых местах, сознательно старались приблизить к python версии, чтоб сконцентрировать внимание на определённых деталях и упростить понимание того, что происходит в коде для тех кто с rust не знаком.

          0

          В этом плане вы правы. Просто когда я работал с одними и теми же действиями (разбей на строки, обрежь пробелы и преврати в число), то хотелось бы это сделать лаконичнее :) а потом уже без создания вектора, лишь на чистых итераторах.

          +1

          Ссылка на parse во втором map не нужна. Ну и первый map делает ненужную аллокацию:


          let output: i32 = ["42", "43", "sorokchetire"]
              .iter()
              .map(|value| value.parse::<i32>())
              .filter_map(Result::ok)
              .map(|integer| integer * 2)
              .sum();
            0

            Спасибо. Я пробовал как-нибудь избежать использования ToOwned, но не смог подобрать нужное описание функции (я хотел не использовать замыкание в этом случае), но не смог избежать &&str, когда мне нужен был &str.


            А по поводу sum, я смею предположить, что вы используете itertools крейт.

              +3

              "std::iter::Sum — Rust" https://doc.rust-lang.org/std/iter/trait.Sum.html#tymethod.sum
              Он вполне себе в стд либе

                –1

                Упс. Значит я лох :( Я подумал об этом методе, но писал код в интернете (без автодополнения) и не поискал заранее.


                Извините.

                +1

                Проблему с двойной ссылкой нельзя обойти из-за того, что массивы в Rust пока нельзя преобразовать в итератор по-значению, а не по-ссылке. Но вскоре это будет исправлено. Тогда можно будет писать так:


                let output: i32 = ["42", "43", "sorokchetire"]
                    .into_iter()
                    .map(str::parse::<i32>)
                    .filter_map(Result::ok)
                    .map(|integer| integer * 2)
                    .sum();
                  0
                  Проблему с двойной ссылкой нельзя обойти из-за того, что массивы в Rust пока нельзя преобразовать в итератор по-значению, а не по-ссылке.

                  В данном случае — можно, достаточно после .iter() воткнуть .copied()

            +2
            Нельзя сравнивать несравнимое, а тем более противопоставлять.
            Бессмысленно превозносить компилируемые языки за то, что они выполняют проверку всего кода программы в процессе сборки. У скриптовых языков этот момент просто отсутствует.
            Бессмысленно превозносить поддержку многопоточности в раст, она конечно круче питонячей, потому что питон изначально не создавался для потоков. Пока vm питона не научат атомарно исполнять его байткод и нормально масштабироваться по ядрам процессора его потоки так и останутся приделанным сбоку.
            На счет функциональщины и замыканий — не убедили. Писать в функциональной манере можно, коли пришла такая охота, и кому это интересно, те знают про functools. Другое дело — это вопрос целесообразности. Питон, все таки ОО язык, и профит от чуждой парадигмы, лично для меня, не очевиден.

            Финальная ремарка вообще убила: «Зачем-же питонисту Rust». Затем, зачем и все другие компилируемые языки, реализовать то, что сам питон делает плохо, например, распараллелить и/или ускорить какую нибудь задачу.
              +8

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

                0
                на Python получается быстрее разрабатывать с нуля, но дорабатывать и поддерживать систему, тем более крупную, быстрее и легче на Rust

                Это справедливо вообще для любой пары из скриптового и компилируемого языка
                  +1

                  Как вы думаете, какую систему будет проще дорабатывать и поддерживать, написанную на Python или на C?

                    –2
                    Судя по тому, что C прилично больше в продакшене, чем Python — всё-таки C
                    • UFO just landed and posted this here
                        0
                        И в чем же преимущество возраста С?

                        Понятия не имею, вы откуда-то взяли возраст — вы и отвечайте.

                        UPD:
                        Это же не вино или коньяк

                        Знаете, COBOL тоже не алкогольный напиток, но от него избавляться не хотят / не могут, в частности из-за того, что программы многолетней «выдержки» очень уж хорошо отлажены, а заново написанные — неизвестно.
                        • UFO just landed and posted this here
                            0
                            О, разница существенна.
                            Я в продакшене (в самом широком смысле) с Python-скриптами (в любом виде) сталкиваюсь прилично реже, чем с C-кодом. Беру телефон в руки — там Python'ом вообще не пахнет (правда и C не так много, но хотя бы в самой ОС имеется), смотрю видео или слушаю музыку в браузере не на Python, а на C (а ещё кодеки, кодеки уж точно на C или вовсе ассемблерные!), как редактор какой запустить — там и LibreOffice, и Notepad++, и VS Code, и тем более Qt Creator в основном на плюсах… На работе Python видел только в форме небольших утилитарных скриптов (по уровню функционала — как раз прототипы), всё остальное — C. Хотя вру — изредка приходится обучать нейронку с помощью TensorFlow — но потом всё равно веса экспортируем в C-хедер, потому что бекенд-то Сишный
                              0

                              Тем не менее, большая часть сайтов в Интернете написана на PHP. А в браузере у вас выполняется в основном JavaScript. А вот на чем написан типичный браузер: https://4e6.github.io/firefox-lang-stats/ Доля именно C там не подавляющая. Мобильные приложения — это Java и Kotlin (или Swift).

                                0
                                У нас же JS магическим образом нативно выполняется, совсем не в JS-машине, корторая написана на компилируемых языках типа C/C++/Rust (то же самое про PHP).
                                А ещё по вашей ссылке выходит, что в Firefox больше используется HTML, чем C — вы тоже скажете, что он «выполняется»?
                                  –1

                                  Вы с темы не съезжайте. Речь шла именно про язык С. Если вам нужно написать приложение для браузера, вы все еще возьмете JavaScript (ну или в недалеком будущем Rust, но сейчас не об этом) и вам все равно, на каком языке написан его интерпретатор. Я не умоляю значения языка С, но я сильно сомневаюсь в вашем утверждении, что любой компилируемый язык (для примера С) подходит лучше для разработки сложных систем любого другого интерпретируемого языка (Python, PHP, JavaScript).

                                    0
                                    WebAssembly.
                                    Окей, расшифрую. Внезапно, WebAssembly позволяет писать фронтенд на C.
                                    А ещё, браузерный фронтенд — это не весь-весь-весь продакшен-код, есть и бекенд, и всё, что вообще не связано с браузерами — и там JS не очень любят (ну, есть, конечно, NodeJS, но её используют в основном фронтендеры для сборки своего проекта, или кто «АААА, бекенд надо, а я кроме JS не знаю ничего»).
                                    К счастью, подавляющее большинство софта всё ещё написано на компилиуемых языках — на своём домашнем компьютере я явно ни одно приложение на скриптовых языках не запускаю. А вот C/C++/.Net/Java приложения — запускаю, по многу раз каждый день. Вы опять скажете про браузеры — ну что поделать, если W3C не добавляет поддержку компилируемых модулей (кроме того же WebAssembly).
                            0

                            Но чем тут хорош именно сам COBOL — непонятно

                              0
                              Ничем не хорош, просто слишком уж важное наследие. Сегодня на нём, конечно, никто не захочет писать кроме совсем лютых гиков.
                              0
                              Знаете, COBOL тоже не алкогольный напиток, но от него избавляться не хотят / не могут, в частности из-за того, что программы многолетней «выдержки» очень уж хорошо отлажены, а заново написанные — неизвестно.

                              Практически не встречал программ, которые не требуют изменений. Точнее, один раз на одном предприятии видел пару PDP-11, которые не выключались десятилетиями, потому что там запускалась какая-то важная программа, уже не помню для чего, а исходных лент/дисков с ней не осталось. У сотрудников предприятия были обоснованные опасения, что если выключить компьютеры, то больше они не включатся, а если включатся, то неоткуда будет загрузить программу. На мониторах были выжжены какие-то стандартные приглашения ОС для PDP-11, хотя разглядеть хоть что-либо на этих мониторах было непросто.


                              Но это один вырожденный случай, честно говоря, в тот момент не догадался выпытать, что это за программа, в которой за несколько десятилетий не понадобилось внести ни одной правки. Она была запущена в одной стране, а продолжала выполняться в другой.


                              Думаю, что таких случаев исключительно мало. Всё вокруг нас непрерывно меняется. Законы, налоги, курсы валют, да всё подряд. Как в этом непрерывном хаосе можно 20-30-40 лет использовать один и тот же код — почти не представляю. Разве что для каких-то совершенно тривиальных задач.

                      0
                      Скриптовые языки для того и придумывались, чтобы на них было быстрее разрабатывать, вынося все тяжелые вещи на плечи, в первую очередь, С. Выступая клеем, который позволяет легко собирать и комбинировать реализацию необходимых алгоритмов на «серьезных» языках, скриптовые языки дают гибкость в реагировании на конечные требования.
                      Оглядываясь на обе статьи вместе, у меня сложилось впечатление, что автор просто неправильно выбрал инструмент, и теперь очень расстроен, что он оказался не достаточно, для него, универсален.

                      PS Всех с наступающим новым годом.
                        0

                        Быстрее разрабатывать именно крупные системы, заметьте. Делать a + b вообще говоря все равно, на каком языке. А вот когда вы можете на высоком уровне оперировать множеством компонентов и легко связывать их в нечто более крупное… Но потом это крупное нужно поддерживать. Но потом это крупное требует особого тюнинга и нужно спускаться на уровень ниже. И та легкость и незамутненность типами, которая помогала на первом этапе, вдруг становится серьезным тормозом дальнейшего развития системы.

                  • UFO just landed and posted this here
                      +1

                      Вообще по отношению к опенсорсу и величине вклада в него вполне можно судить об адекватности и прогрессивности ИТ-компании сегодня, я думаю.

                        0
                        Это типичное рейдерство. Странно, что законы против рейдерства у нас почему-то не работают.
                        –3
                        Попрограммируйте подольше на JavaScript, и проблемы с типами переменных вас беспокоить больше не будут. Я понимаю, трудно перестраивать типизированное мышление классического программиста. Но в динамических типах есть своя прелесть, поверьте.
                          +4
                          Именно поэтому всё больше проектов используют TypeScript, ага.
                            –1
                            Просто эти люди начинали (как и я когда-то) изучение программирования с языков со строгой типизацией, и не могут перестроить мышление.
                            Вообще, JS — это не язык для надёжного программирования и серьёзных проектов. Это вспомогательный язык-скрипт. И очень плохо, что его пытаются переделать в Java вместо того, чтобы создать с нуля для своих целей другой язык, например на основе чего-то типа WebAssebmler — тот же TypeScript или Java-поверх-WebAsm, а не ограничение и уродование изумительного скриптового языка на основе прототипов, которым является (являлся?) JavaScript. Вместо создания нужных языков с нуля, их пытаются сделать поверх JavaScript, для чего сам JavaScript неудачно допиливают напильниками и ограничивают, чтобы не дай бог чего не вышло. Вот toSource(), например, зачем убрали? Удобная штука, и для совместимости со старым кодом нужная. Ломать — не строить…
                          +1
                          Как мы уже говорили, питонисту очень многое приходится делать самому — проверять типы, держать в голове, где будет None, а где число, искать в документации, какие исключения может выбросить функция.

                          Задам этот вопрос ещё раз — а вы точно python разработчик? Дело в том что разработчики на ДТ языках не думают так как вы описали, такие проблемы просто не встают.
                          Больше похоже как будто вы после Java/C#/C++ решили поработать с Python и перенесли практики в него которые естественно не заработали.

                            +1
                            Не очень понимаю, что вы имеете ввиду. Разработчики на ДТ языках также, как и все остальные разработчики, работают с типами и, в большом количестве случаев, пытаются избежать ошибок в продакшн. Но, возможно, что есть и другая точка зрения – поясните тогда, пожалуйста
                              +1
                              Разработчик и на ДТ используют другие шаблоны. А когда люди приходят из Java например, они иногда пытаются пспользовать подходы оттуда и получают все недостатки Java плюс недостатки из-за написания питон кода с подходом Java, а преимуществ питона не получают.
                              0
                              Можно вопрос? Спасибо. А что такое «ДТ языки»? Языки Дизельного Топлива? Языки Дульного Тормоза? Языки Детского Творчества? Я не издеваюсь, так что минусовать сразу же не надо, я правда не в курсе что такое «ДТ» в контексте языка программирования.
                                0
                                В контексте статьи, думаю, что stalkerg имеет ввиду динамически типизированные языки программирования.
                                  0
                                  Теперь понял, спасибо!
                                0
                                Дело в том что разработчики на ДТ языках не думают так как вы описали

                                А как именно они думают? Или они не думают, а тупо исправляют баги, когда те проявят себя?

                              Only users with full accounts can post comments. Log in, please.