Python - один из самых популярных языков программирования. Как мы писали, в январе 2022 года он во второй раз за свою историю стал лидером ежемесячного рейтинга языков программирования Tiobe. Рост популярности Python за год составил 1,86%.

Популярность языка обусловлена его относительной простотой - работать с ним может быстро начать даже новичок. Конечно, никто не говорит, что этот новичок сможет сразу же писать высоконагруженные проекты, нет. Но решать задачи базового уровня - вполне. Но все же есть проблемы даже здесь, и о них поговорим в статье. Разбираем подводные камни Python вместе с Алексеем Некрасовым, лидером направления Python в МТС, программным директором направления Python и спикером профессии “Python-разработчик” в Skillbox.

Немного о достоинствах языка

На Хабре очень много статей о достоинствах Python, поэтому повторяться не будем. Скажем только, что язык действительно знаменит своей простотой. Причем с течением времени в нём появилось много “синтаксического сахара”, который позволяет быть ему немногословным и понятным языком, похожим на псевдокод. А всё сложное убрано “под капот”, чтобы не отвлекать разработчика. Простота позволяет быстро создавать прототипы и проверять гипотезы. Благодаря этому новичкам проще всего начать изучать программирование с Python.

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

def decorator_log(func):
   def wrapper(*args, **kwargs):
       print("вызов функции с параметрами", args, kwargs)
       res = func(*args, **kwargs)
       return res
   return wrapper

def func_1():
   print('Тестовая функция')

func_1 = decorator_log(func_1)

# Вместо func_1 = decorator_log(func_1) можно теперь написать:

@decorator_log
def func_1()
   print('Тестовая функция')

Таких конструкций в языке достаточно много, они упрощают чтение кода и работу с ним. 

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

Подробно о проблемах

Кроме потерь в скорости есть и другие подводные камни, о которых далеко не всегда знают начинающие разработчики. Среди них особенно выделяются две крупные проблемы, это динамическая типизация и так называемый "новый сахар".

Динамическая типизация

Python - динамически типизированный язык программирования. Это позволяет быстро разрабатывать прототипы и писать код. Динамическая типизация, если объяснять “на пальцах”, значит, что одна и та же переменная в разное время может ссылаться на данные разного типа. Например:

data = input() or '123456'  # сейчас в переменной data лежит строка
data = int(data)  # теперь в переменной data лежит целое число

И это будет работать.

Статистические языки программирования, например C, такого сделать не позволяют. Но в динамической типизации есть подвох. Если в простых проектах все ок и проблем не возникает, то, чем масштабнее проект на Python, тем больше появляется ошибок, связанных с типом переменных. Так, где-то далеко в коде введена функция, которая на вход принимает переменную data, введенную пользователем. В функции проверили, что введенное число является двузначным:

if len(data) != 2: print("Нужно ввести двухзначное число")

При выполнении этого кода мы внезапно получаем ошибку

if len(data) != 2: TypeError: object of type 'int' has no len()

Это означает, что в каком-то участке нашего кода строка уже переведена в число, и добавлена в переменную data. Подобные ошибки - далеко не редкость. Чаще всего они возникают на проектах, где работает от двух человек. Для того, чтобы ошибок не было, необходимо писать тесты, которые проверяют, нужный ли формат данных у переменных. У статических языков программирования таких проблем нет.

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

data: str = input() or '123456'
number: int = int(data)
if len(number) != 2:
print("Нужно ввести двухзначное число")

После написания кода приходится запускать для проверки mypy. Система проверяет, что int как строка нигде не используется. И если запустить mypy, то появится ошибка о передаче неверной переменной в функцию len:

​​error: Argument 1 to "len" has incompatible type "int"; expected "Sized"

Почему здесь употребляется термин "псевдостатическая типизация"? Дело в том, что при запуске скрипта Python проверка не будет проводиться сразу же, так что разработчик не будет знать, корректно ли выполняется передача значения в функцию len или нет. Ошибка появится лишь тогда, когда будет запущена программа с передачей в нее входного значения.

Новый сахар и усложнения в языке

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

Пример - задача по очистке входных данных с приведением их к целым числам. Раньше код программы выглядел бы следующим образом:

from typing import Optional

def to_int(string: str) -> Optional[int]:
   try:
       return int(string)
   except ValueError:
       return None

data = ['sadf', '12', '1', 'a1']
filter_data = []

for i_str in data:
   i_number = to_int(i_str)
   if i_number is not None:
       filter_data.append(i_number)

print(filter_data)

Но начиная с версии 3.8 блок с for вполне можно записать следующим образом:

filter_data = [y for x in data if (y := to_int(x)) is not None]

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

Какие еще есть сложности?

В целом, их не так мало. Обучение Python можно разделить на несколько этапов, и на каждом из них будут встречаться проблемы. Что касается этапов, то вот они:

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

  • Изучение паттернов, которые реализованы уже в самом языке: декораторы, итераторы, генераторы, контекст менеджеры и т.д. На этом этапе идет осознание паттернов проектирования и как они реализованы в самом языке. Тут новичку из другой сферы будет непросто, но часто в осознании этих тем помогают наставники или хорошо подобранные статьи.

  • Изучение различных прикладных фреймворков: web (flask, FastApi, Django и др.), работа с БД (SQLAlchemy, sqlite3, Tortoise ORM и др.), работа с данными (numpy, pandas, marshmallow, pydantic и др.) и т.д. Здесь сталкиваемся со сложностью в определении направления своего развития и выстраивания своего маршрута в изучении доп. инструментов. Новичкам я бы посоветовал для этого проконсультироваться с опытным разработчиком или наставником.

  • Углублённое изучение языка: метаклассы, дескрипторы и т.д. В обычной работе разработчик с этим практически не встречается, так как 90% всех задач можно решить без этих знаний. Но если вы хотите стать первоклассным разработчиком, то этих тем вам не обойти.

В целом, обойти проблемы помогает постоянное обучение, а также более опытные коллеги и наставники. Не бойтесь обращаться к ним за помощью, как говориться: “Не тот глуп, кто не знает, но тот, кто знать не хочет.” К слову, если у вас возникли проблемы на каком-то из этих этапов, можете писать мне, постараюсь помочь.

А что насчет перспектив языка?

Если коротко, то с уверенностью могу сказать - они есть, изучать Python стоит, не сомневайтесь.

Если же расписать подробнее, то скажу, что с каждым годом требования для новичков со стороны работодателей возрастают. Все дело в том, что появляются новые фреймворки, более сложные версии языка программирования, новые инструменты и т.д.

Пять лет назад, чтобы устроиться junior python web-разработчиком нужно было уметь решать алгоритмические задачи и знать основы языка программирования. А сейчас  добавилось следующее:

  • знание одного из фреймворка Flask/Django (желательно ещё и асинхронного фреймворка, например, FastAPI);

  • знание SQL и работа с PostgreSQL;

  • знание тестовых фреймворков pytest, unittest;

  • знание принципов контейнеризации и работы с docker;

  • знание основ Linux.

В ближайшее три-пять лет спрос будет расти на middle и senior разработчиков, так как именно они выполняют основную часть задач в IT-компаниях. Если смотреть на спрос стажёров/junior, то тут всё сложнее по следующим причинам:

  • Junior разработчик приносит только убыток компании, так ему нужен наставник (уровня middle/senior). В итоге на решение определенной задачи с учеником тратится больше времени, чем если бы задачу делал сам наставник.

  • Часто через полгода-год стажер/junior уходит из компании, в которой обучался, в другую с увеличением зарплаты в среднем в 2 раза. Соответственно, терпит убытки компания, которая первой наняла молодого специалиста на работу.

  • Завышенные ожидания у стажёров/junior. Многие переходят из других сфер, где они уже привыкли к своему доходу и не готовы переходить, к примеру, со 100 тыс. руб в месяц на 50 тыс. руб.

Через три-четыре года у стажёров/junior начнёт появляться конкурент в лице ИИ. Недавно компания DeepMind (дочка Alphabet), выполнила на платформе Codeforces 10 тестов и попала в 54% лучших участников. Вполне вероятно, что в скором будущем ИИ сможет решать простые, шаблонные задачи, которые в обычной ситуации дают стажерам/junior-разработчикам.

В качестве вывода

Подводя итог, мы видим, что если вы хотите перейти в сферу IT как python-разработчик, то вам нужно:

  • Максимально сконцентрировать свои силы на обучении, учиться предстоит многому, это будет занимать время.

  • Быть готовым тому, что в течение полугода-года ваша зарплата не превысит 100 тыс. руб. в месяц.

  • Постараться устроиться на первую работу и начать учиться у более опытных коллег.

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

Комментарии (27)


  1. amarao
    28.02.2022 17:31
    +3

    На мой взгляд, самый вопиющий речекряк питона - это существование StopIteration


    1. BasicWolf
      28.02.2022 22:29

      А по-моему это гениально. Или как в других языках, будем иметь два метода вместо одного, типа Iterator.hasNext() -> boolи Iterator.next().


      1. amarao
        28.02.2022 23:51
        +2

        После того, как я перешёл на Rust... ладно, не перешёл, но плотно изучил, меня каждый раз от этого StopIteration передёргивает.

        В Rust (для сравнения) next возвращает Option<Value>, который либо Some(Value), либо None (None - это не питоновский None, это из enum'а None|Some). В рамках синтаксического сахара for вычитывает из итератора пока не получит None. Но любой желающий может взять next() сам сколько нужно раз и посмотреть содержимое.

        И правильно обработать None. В питоне "правильно обработать" можно только так:

        try:
        	foo = next(bar)
        except StopIteration:
        	foo = "something else"

        Сравните это с rust'овой версией:

        let foo = bar.next().unwrap_or("something else")

        Передёргивает меня тут не от числа строк, а от того, что exception используется не для исключительной ситуации.

        UDP задумался, есть ли дефолт у next. Есть, спасибо. Можно foo=next(bar, "something else"), хотя моя претензия по поводу эксепшена в неисключительной ситуации всё равно остаётся.


        1. sswwssww
          01.03.2022 16:52
          +2

          Какая претензия может еще оставаться? Какое должно быть поведение по умолчанию если нет следующего элемента, а ты вызываешь next()? Почему именно ваше поведение по умолчанию лучше возбуждения StopIteration? В крайнем случае, реализуйте __iter__ и __next__ у класса объекта bar задав свое поведение при случае когда последовательность закончилась.
          Что будет делать Rust если убрать .unwrap_or("something else")? Чем подход bar.next().unwrap_or("something else") лучше подхода next(bar, "something else")?


          1. amarao
            01.03.2022 17:23

            Тем, что вместо unwrap я могу написать так:

            if let Some(x) = foo.next(){
                ...
            }

            Как вы такое на Python напишете?


            1. sundmoon
              01.03.2022 17:33
              +4

              Это преимущества не Rust, а функциональной парадигмы.

              Pattern Matching до сих пор нет даже в Kotlin: ждут появления его в Java (оно задерживается). Бреслав где-то говорил, что "патмат" требует примерно столько же строк в реализации с нуля, сколько весь остальной язык.


            1. BasicWolf
              01.03.2022 18:31
              +1

              Да пожалуйста, если не хотите StopIteration:

              class Sentinel: ...
              
              if x := next(foo, Sentinel):
                   ...

              Только это не нормальный Питоний код же. Вы часто видели, чтобы next() вызывался явным образом, а не в контексте for? Или вам часто приходится кидать StopIteration вручную?

              Мне кажется, что вы не до конца рассмотрели идею со StopIteration .
              Это исключение появилось как часть протокола итераторов и генераторов. А генераторы можно соединять, даже не имея yield from.
              А как в цепочке генераторов проще всего сигнализировать на самый верх, что глубинный генератор истощён? Бросаем исключение, которое просто всплывает наверх.
              И да, этот механизм - противоречивый и далеко не самый эффективный. Мне лично больше импонирует функциональный подход Раста. А с другой стороны - этот механизм чертовски простой и проверенный временем :)


              1. amarao
                01.03.2022 20:42

                Рассказываю простой пример, где надо next вызывать вручную.

                У нас грок-подобный парсер потока строк. Мы хотим найти match1, после чего мы почти уверены, что будет match2 и match3. "Почти", потому что может оказаться, что нет, и тогда match1 тоже не валидный, и строки под ним тоже, потому что по спецификации, если match1, то match2 и match3 идут строго после.

                def parse(iterator):
                  data = []
                  while True:
                    match1 = grok1.match(next(iterator))
                    if match1 == EOF:  ## not valid
                      break
                    if match1:
                      match2 = grok2.match(next(iterator))
                      match3 = grok3.match(next(iterator))
                      if match1 and match2:
                         data.append(combine(match1, match2, match2))
                   yield from process(data)

                А теперь сделайте так, пожалуйcта, чтобы не надо было это обтыкивать try/except и покажите пример красивой реализации break.


                1. BasicWolf
                  02.03.2022 10:22

                  У вас соль в том, что парсер и токенайзер жестко связаны друг с другом. EOF должен возвращаться токенайзером. Парсер же читает и обрабатывает поток токенов. Но даже если их по какой-либо причине нельзя разоединить, тут есть над чем поработать, например:

                  def parse(iterator):
                      data = (
                          combine(*matches)
                          for matches in read_triplets(iterator)
                      )
                      yield from process(data)
                  
                  def read_triplets(iterator):
                      while all((        
                          (match_1 := grok1.match(next(iterator, EOF))) != EOF,
                          (match_2 := grok2.match(next(iterator, EOF))) != EOF,
                          (match_3 := grok3.match(next(iterator, EOF))) != EOF,
                      )):
                          yield (match_1, match_2, match_3)


                  1. amarao
                    02.03.2022 12:19
                    -1

                    Пардон, понял.

                    Но мне всё равно неприятно видеть штатную ситуацию обрабатываемую как исключительную. Исключения - для ошибок или неожиданных ситуаций.


                1. mayorovp
                  02.03.2022 11:19

                  Конкретно в вашем случае, насколько я понял, окончание входящего потока автоматически означает что вы не найдёте какой-то из своих матчей, что означает отсутствие данных для обработки.


                  А значит, тут можно обойтись ОДНИМ try/except вокруг всего цикла. Что внезапно оказалось даже лучше паттерн-матчинга, которых потребовалось бы аж три.


                  1. amarao
                    02.03.2022 12:23
                    -1

                    Данные для обработки есть (предыдущие накоплены). И, главное, я ожидаю окончания, то есть ситуация не исключительная, но почему-то я должен ждать исключения.


          1. AnthonyMikh
            01.03.2022 23:57
            +1

            Что будет делать Rust если убрать .unwrap_or("something else")?

            Код не скомпилируется, ибо в foo будет лежать значение типа Option<SomeType>, а ниже по коду будет ожидаться foo типа SomeType.


  1. Colobon
    28.02.2022 18:18

    Ничего не имею против ни Python, ни PHP, но картинку можно было бы подобрать более подходящую :)


    1. Colobon
      01.03.2022 08:33

      Upd. Картинку изменили :)


  1. splatt
    28.02.2022 21:28
    -1

    Работаю с Python уже почти 10 лет, и на мой взгляд по количеству неочевидных подводных камней, он может легко сравниться с низкоуровневыми языками.

    Классический пример, дефолтные параметры функций:

    from time import time
    
    def foo(timestamp=time()):
        print(timestamp)

    Все что связано с модулями и импортами это вообще отдельная история.


    1. unsignedchar
      01.03.2022 00:19

      Классический пример, дефолтные параметры функций:

      А можно разъяснить, что не так с дефолтными параметрами? КМК достаточно очевидно, зачем оно.


      1. Soarex16
        01.03.2022 02:30
        +2

        Дефолтные аргументы создаются один раз при первом обращении к функции. Например, если бы по-умолчанию подставлялся список, то он был бы всегда один и тот же между вызовами:

        def foo(bar=[]):
          return bar
        
        x = foo()
        x.append(4)
        y = foo()

        В результате x и y ссылаются на один и тот же список.


        1. HemulGM
          01.03.2022 07:33

          Такой проблемы нет в "низкоуровневых языках"


        1. unsignedchar
          01.03.2022 09:46

          Понятно. Ещё одна неочевидная особенность :)


        1. CheeHaveDreams
          01.03.2022 11:46

          Только не при первом обращении к функции, а при создании интерпретатором объекта функции.


          1. Soarex16
            01.03.2022 11:59

            Да, Вы правы


    1. kx13
      01.03.2022 12:40

      Про дефолтные значения в FAQ еще написано
      https://docs.python.org/3/faq/programming.html#id13
      Я вообще всем новичкам рекомендую FAQ читать, а потом еще перечитывать. А его к сожалению мало читают, а там как раз много полезного, чтобы поменьше на грабли наступать.


      1. splatt
        02.03.2022 03:49

        Да, на любую критику языка можно сказать "RTFM". Но это так не работает.

        Мне нужен инструмент для быстрого и эффективного решения бизнес-задач, в котором код легко читаем, а результат его выполнения очевиден. А не ночные сессии починки продакшна, потому что 3 сеньора которые аппрувили пулл реквест, не перечитывали в очередной раз FAQ и пропустили вроде как "простую" ошибку.


        1. kx13
          02.03.2022 09:29

          Как Вы хотите, к сожалению, тоже не работает.

          Я тоже так хочу, как вы говорите: чтобы было было все просто и очевидно.

          Но любая сложная система (к которой относится любой развитый язык программирования) полна различных "особенностей". Вопрос только в том, насколько их много и насколько они хитрые.

          Надо быть реалистами и ожидать подвоха даже от самых "простых", "удобных" и "дружелюбных" инструментов. Поэтому, чем лучше R этот самый TFM, тем меньше сюрпризов будет.


  1. Bedal
    01.03.2022 10:33
    +1

    Статистические языки программирования
    языки со статической типизацией точно называются статистическими?